diff options
862 files changed, 22727 insertions, 11780 deletions
diff --git a/config/preloaded-classes-denylist b/config/preloaded-classes-denylist index e3e929cb00d9..a6a1d1680b7b 100644 --- a/config/preloaded-classes-denylist +++ b/config/preloaded-classes-denylist @@ -1,5 +1,4 @@ android.content.AsyncTaskLoader$LoadTask -android.media.MediaCodecInfo$CodecCapabilities$FeatureList android.net.ConnectivityThread$Singleton android.os.FileObserver android.os.NullVibrator diff --git a/core/api/current.txt b/core/api/current.txt index 6707c15de682..19f68eb0c787 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16896,6 +16896,7 @@ package android.graphics { method public android.graphics.Paint.FontMetricsInt getFontMetricsInt(); method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void getFontMetricsIntForLocale(@NonNull android.graphics.Paint.FontMetricsInt); method public float getFontSpacing(); + method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") @Nullable public String getFontVariationOverride(); method public String getFontVariationSettings(); method public int getHinting(); method public float getLetterSpacing(); @@ -16974,6 +16975,7 @@ package android.graphics { method public void setFilterBitmap(boolean); method public void setFlags(int); method public void setFontFeatureSettings(String); + method @FlaggedApi("com.android.text.flags.typeface_redesign_readonly") public boolean setFontVariationOverride(@Nullable String); method public boolean setFontVariationSettings(String); method public void setHinting(int); method public void setLetterSpacing(float); @@ -38276,7 +38278,6 @@ package android.provider { field public static final String ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS = "android.settings.NOTIFICATION_LISTENER_DETAIL_SETTINGS"; field public static final String ACTION_NOTIFICATION_LISTENER_SETTINGS = "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"; field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS"; - field @FlaggedApi("android.provider.system_regional_preferences_api_enabled") public static final String ACTION_NUMBERING_SYSTEM_SETTINGS = "android.settings.NUMBERING_SYSTEM_SETTINGS"; field public static final String ACTION_PRINT_SETTINGS = "android.settings.ACTION_PRINT_SETTINGS"; field public static final String ACTION_PRIVACY_SETTINGS = "android.settings.PRIVACY_SETTINGS"; field public static final String ACTION_PROCESS_WIFI_EASY_CONNECT_URI = "android.settings.PROCESS_WIFI_EASY_CONNECT_URI"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 22af517900d2..ae5542be7548 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3405,12 +3405,12 @@ package android.companion.virtual { public final class VirtualDeviceManager { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams); - method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @NonNull public java.util.Set<java.lang.String> getAllPersistentDeviceIds(); - method @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String); + method @NonNull public java.util.Set<java.lang.String> getAllPersistentDeviceIds(); + method @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String); field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2 field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1 field public static final int LAUNCH_SUCCESS = 0; // 0x0 - field @FlaggedApi("android.companion.virtual.flags.persistent_device_id_api") public static final String PERSISTENT_DEVICE_ID_DEFAULT = "default:0"; + field public static final String PERSISTENT_DEVICE_ID_DEFAULT = "default:0"; } public static interface VirtualDeviceManager.ActivityListener { @@ -3432,7 +3432,7 @@ package android.companion.virtual { public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable { method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener); - method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void addActivityPolicyExemption(@NonNull android.content.ComponentName); + method public void addActivityPolicyExemption(@NonNull android.content.ComponentName); method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void addActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption); method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener); method public void close(); @@ -3448,7 +3448,7 @@ package android.companion.virtual { method @Deprecated @NonNull public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method @NonNull public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig); method @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") @NonNull public android.hardware.input.VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull android.hardware.input.VirtualRotaryEncoderConfig); - method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig); + method @NonNull public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig); method @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig); method @Deprecated @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method public int getDeviceId(); @@ -3458,10 +3458,10 @@ package android.companion.virtual { method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); method public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback); method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener); - method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void removeActivityPolicyExemption(@NonNull android.content.ComponentName); + method public void removeActivityPolicyExemption(@NonNull android.content.ComponentName); method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void removeActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption); method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener); - method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void setDevicePolicy(int, int); + method public void setDevicePolicy(int, int); method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void setDevicePolicy(int, int, int); method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") public void setDisplayImePolicy(int, int); method public void setShowPointerIcon(boolean); @@ -3481,7 +3481,7 @@ package android.companion.virtual { method @Deprecated public int getDefaultNavigationPolicy(); method public int getDevicePolicy(int); method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public java.time.Duration getDimDuration(); - method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @Nullable public android.content.ComponentName getHomeComponent(); + method @Nullable public android.content.ComponentName getHomeComponent(); method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @Nullable public android.content.ComponentName getInputMethodComponent(); method public int getLockState(); method @Nullable public String getName(); @@ -3498,11 +3498,11 @@ package android.companion.virtual { field public static final int LOCK_STATE_DEFAULT = 0; // 0x0 field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0 field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1 - field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3 + field public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3 field public static final int POLICY_TYPE_AUDIO = 1; // 0x1 field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; // 0x6 field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5 - field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4 + field public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4 field @FlaggedApi("android.companion.virtualdevice.flags.default_device_camera_access_policy") public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7; // 0x7 field public static final int POLICY_TYPE_RECENTS = 2; // 0x2 field public static final int POLICY_TYPE_SENSORS = 0; // 0x0 @@ -3520,7 +3520,7 @@ package android.companion.virtual { method @Deprecated @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setBlockedCrossTaskNavigations(@NonNull java.util.Set<android.content.ComponentName>); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDevicePolicy(int, int); method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setDimDuration(@NonNull java.time.Duration); - method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName); + method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setHomeComponent(@Nullable android.content.ComponentName); method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setInputMethodComponent(@Nullable android.content.ComponentName); method @NonNull @RequiresPermission(value=android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY, conditional=true) public android.companion.virtual.VirtualDeviceParams.Builder setLockState(int); method @NonNull public android.companion.virtual.VirtualDeviceParams.Builder setName(@NonNull String); @@ -5332,13 +5332,13 @@ package android.hardware.display { public final class VirtualDisplayConfig implements android.os.Parcelable { method @FlaggedApi("android.companion.virtualdevice.flags.virtual_display_insets") @Nullable public android.view.DisplayCutout getDisplayCutout(); - method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") public boolean isHomeSupported(); + method public boolean isHomeSupported(); method @FlaggedApi("com.android.window.flags.vdm_force_app_universal_resizable_api") public boolean isIgnoreActivitySizeRestrictions(); } public static final class VirtualDisplayConfig.Builder { method @FlaggedApi("android.companion.virtualdevice.flags.virtual_display_insets") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCutout(@Nullable android.view.DisplayCutout); - method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setHomeSupported(boolean); + method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setHomeSupported(boolean); method @FlaggedApi("com.android.window.flags.vdm_force_app_universal_resizable_api") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setIgnoreActivitySizeRestrictions(boolean); } @@ -5970,13 +5970,13 @@ package android.hardware.input { method @NonNull public android.hardware.input.VirtualRotaryEncoderScrollEvent.Builder setScrollAmount(@FloatRange(from=-1.0F, to=1.0f) float); } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable { + public class VirtualStylus implements java.io.Closeable { method public void close(); method public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent); method public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent); } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable { + public final class VirtualStylusButtonEvent implements android.os.Parcelable { method public int describeContents(); method public int getAction(); method public int getButtonCode(); @@ -5989,7 +5989,7 @@ package android.hardware.input { field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusButtonEvent> CREATOR; } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusButtonEvent.Builder { + public static final class VirtualStylusButtonEvent.Builder { ctor public VirtualStylusButtonEvent.Builder(); method @NonNull public android.hardware.input.VirtualStylusButtonEvent build(); method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setAction(int); @@ -5997,7 +5997,7 @@ package android.hardware.input { method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setEventTimeNanos(long); } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable { + public final class VirtualStylusConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable { method public int describeContents(); method public int getHeight(); method public int getWidth(); @@ -6005,12 +6005,12 @@ package android.hardware.input { field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusConfig> CREATOR; } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualStylusConfig.Builder> { + public static final class VirtualStylusConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualStylusConfig.Builder> { ctor public VirtualStylusConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int); method @NonNull public android.hardware.input.VirtualStylusConfig build(); } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusMotionEvent implements android.os.Parcelable { + public final class VirtualStylusMotionEvent implements android.os.Parcelable { method public int describeContents(); method public int getAction(); method public long getEventTimeNanos(); @@ -6029,7 +6029,7 @@ package android.hardware.input { field public static final int TOOL_TYPE_STYLUS = 2; // 0x2 } - @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusMotionEvent.Builder { + public static final class VirtualStylusMotionEvent.Builder { ctor public VirtualStylusMotionEvent.Builder(); method @NonNull public android.hardware.input.VirtualStylusMotionEvent build(); method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setAction(int); @@ -11462,6 +11462,12 @@ package android.os { method @RequiresPermission(android.Manifest.permission.SET_LOW_POWER_STANDBY_PORTS) public void release(); } + @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public final class PowerMonitorReadings { + method @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public int getGranularity(); + field @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public static final int GRANULARITY_FINE = 1; // 0x1 + field @FlaggedApi("android.permission.flags.fine_power_monitor_permission") public static final int GRANULARITY_UNSPECIFIED = 0; // 0x0 + } + @Deprecated public class PowerWhitelistManager { method @Deprecated @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public void addToWhitelist(@NonNull String); method @Deprecated @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public void addToWhitelist(@NonNull java.util.List<java.lang.String>); @@ -18570,13 +18576,13 @@ package android.telephony.satellite { @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int[] getSatelliteDisallowedReasons(); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.List<java.lang.String> getSatellitePlmnsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatellite(@NonNull java.util.List<android.telephony.satellite.SatelliteSubscriberInfo>, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telephony.satellite.SatelliteManager.SatelliteException>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); method @FlaggedApi("com.android.internal.telephony.flags.satellite_system_apis") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForCommunicationAccessStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCommunicationAccessStateCallback); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a988acf1f4a9..a352d9d2ea06 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3415,6 +3415,10 @@ package android.telecom { method public void onBindClient(@Nullable android.content.Intent); } + public class TelecomManager { + method @FlaggedApi("com.android.server.telecom.flags.voip_call_monitor_refactor") public boolean hasForegroundServiceDelegation(@Nullable android.telecom.PhoneAccountHandle); + } + } package android.telephony { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 999db18a1229..6151b8e2ef0a 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -142,6 +142,15 @@ public abstract class ActivityManagerInternal { String processName, String abiOverride, int uid, Runnable crashHandler); /** + * Called when a user is being deleted. This can happen during normal device usage + * or just at startup, when partially removed users are purged. Any state persisted by the + * ActivityManager should be purged now. + * + * @param userId The user being cleaned up. + */ + public abstract void onUserRemoving(@UserIdInt int userId); + + /** * Called when a user has been deleted. This can happen during normal device usage * or just at startup, when partially removed users are purged. Any state persisted by the * ActivityManager should be purged now. diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index af6978a6b70c..82c746a8ad4c 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -1846,6 +1846,7 @@ public class ActivityOptions extends ComponentOptions { } /** @hide */ + @WindowConfiguration.WindowingMode public int getLaunchWindowingMode() { return mLaunchWindowingMode; } @@ -1855,7 +1856,7 @@ public class ActivityOptions extends ComponentOptions { * @hide */ @TestApi - public void setLaunchWindowingMode(int windowingMode) { + public void setLaunchWindowingMode(@WindowConfiguration.WindowingMode int windowingMode) { mLaunchWindowingMode = windowingMode; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 1f3e6559a695..717a2acb4b4a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -105,7 +105,6 @@ import android.content.pm.ServiceInfo; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; -import android.content.res.ResourceTimer; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.loader.ResourcesLoader; @@ -5254,7 +5253,6 @@ public final class ActivityThread extends ClientTransactionHandler Resources.dumpHistory(pw, ""); pw.flush(); - ResourceTimer.dumpTimers(info.fd.getFileDescriptor(), "-refresh"); if (info.finishCallback != null) { info.finishCallback.sendResult(null); } diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 67f7bee4028e..b5ac4e78c7ad 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -70,7 +70,6 @@ import com.android.internal.widget.VerifyCredentialResponse; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -1064,7 +1063,7 @@ public class KeyguardManager { Log.e(TAG, "Save lock exception", e); success = false; } finally { - Arrays.fill(password, (byte) 0); + LockPatternUtils.zeroize(password); } return success; } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 3d85ea6a1fca..ffd235f91e09 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1129,6 +1129,10 @@ public final class LoadedApk { @UnsupportedAppUsage public ClassLoader getClassLoader() { + ClassLoader ret = mClassLoader; + if (ret != null) { + return ret; + } synchronized (mLock) { if (mClassLoader == null) { createOrUpdateClassLoaderLocked(null /*addedPaths*/); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 614e2aaf42e8..3c37b449c3a8 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1968,6 +1968,13 @@ public class Notification implements Parcelable @SystemApi public static final int SEMANTIC_ACTION_CONVERSATION_IS_PHISHING = 12; + /** + * {@link #extras} key to a boolean defining if this action requires special visual + * treatment. + * @hide + */ + public static final String EXTRA_IS_MAGIC = "android.extra.IS_MAGIC"; + private final Bundle mExtras; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Icon mIcon; @@ -5856,7 +5863,9 @@ public class Notification implements Parcelable return null; } final int size = mContext.getResources().getDimensionPixelSize( - R.dimen.notification_badge_size); + Flags.notificationsRedesignTemplates() + ? R.dimen.notification_2025_badge_size + : R.dimen.notification_badge_size); Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); badge.setBounds(0, 0, size, size); @@ -5982,6 +5991,15 @@ public class Notification implements Parcelable } setHeaderlessVerticalMargins(contentView, p, hasSecondLine); + // Update margins to leave space for the top line (but not for headerless views like + // HUNS, which use a different layout that already accounts for that). + if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) { + int margin = getContentMarginTop(mContext, + R.dimen.notification_2025_content_margin_top); + contentView.setViewLayoutMargin(R.id.notification_main_column, + RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX); + } + return contentView; } @@ -6205,7 +6223,7 @@ public class Notification implements Parcelable int textColor = Colors.flattenAlpha(getPrimaryTextColor(p), pillColor); contentView.setInt(R.id.expand_button, "setDefaultTextColor", textColor); contentView.setInt(R.id.expand_button, "setDefaultPillColor", pillColor); - // Use different highlighted colors for e.g. unopened groups + // Use different highlighted colors for conversations' unread count if (p.mHighlightExpander) { pillColor = Colors.flattenAlpha( getColors(p).getTertiaryFixedDimAccentColor(), bgColor); @@ -6454,16 +6472,6 @@ public class Notification implements Parcelable big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor); big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor); - // Update margins to leave space for the top line (but not for HUNs, which use a - // different layout that already accounts for that). - if (Flags.notificationsRedesignTemplates() - && p.mViewType != StandardTemplateParams.VIEW_TYPE_HEADS_UP) { - int margin = getContentMarginTop(mContext, - R.dimen.notification_2025_content_margin_top); - big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP, - margin, TypedValue.COMPLEX_UNIT_PX); - } - boolean validRemoteInput = false; // In the UI, contextual actions appear separately from the standard actions, so we @@ -6805,8 +6813,6 @@ public class Notification implements Parcelable public RemoteViews makeNotificationGroupHeader() { return makeNotificationHeader(mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_GROUP_HEADER) - // Highlight group expander until the group is first opened - .highlightExpander(Flags.notificationsRedesignTemplates()) .fillTextsFrom(this)); } @@ -6936,6 +6942,12 @@ public class Notification implements Parcelable public RemoteViews makePublicContentView(boolean isLowPriority) { if (mN.publicVersion != null) { final Builder builder = recoverBuilder(mContext, mN.publicVersion); + // copy non-sensitive style fields to the public style + if (mStyle instanceof Notification.MessagingStyle privateStyle) { + if (builder.mStyle instanceof Notification.MessagingStyle publicStyle) { + publicStyle.mConversationType = privateStyle.mConversationType; + } + } return builder.createContentView(); } Bundle savedBundle = mN.extras; @@ -6982,14 +6994,12 @@ public class Notification implements Parcelable * @param useRegularSubtext uses the normal subtext set if there is one available. Otherwise * a new subtext is created consisting of the content of the * notification. - * @param highlightExpander whether the expander should use the highlighted colors * @hide */ - public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext, - boolean highlightExpander) { + public RemoteViews makeLowPriorityContentView(boolean useRegularSubtext) { StandardTemplateParams p = mParams.reset() .viewType(StandardTemplateParams.VIEW_TYPE_MINIMIZED) - .highlightExpander(highlightExpander) + .highlightExpander(false) .fillTextsFrom(this); if (!useRegularSubtext || TextUtils.isEmpty(p.mSubText)) { p.summaryText(createSummaryText()); diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 08bd854525ec..aede8aa70ede 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -17,6 +17,7 @@ package android.app; import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.notification.Flags.notificationClassification; @@ -50,6 +51,7 @@ import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.os.IpcDataCache; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -71,6 +73,8 @@ import android.util.LruCache; import android.util.Slog; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.InstantSource; @@ -1202,12 +1206,20 @@ public class NotificationManager { * package (see {@link Context#createPackageContext(String, int)}).</p> */ public NotificationChannel getNotificationChannel(String channelId) { - INotificationManager service = service(); - try { - return service.getNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getChannelFromList(channelId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1222,13 +1234,21 @@ public class NotificationManager { */ public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId, @NonNull String conversationId) { - INotificationManager service = service(); - try { - return service.getConversationNotificationChannel(mContext.getOpPackageName(), - mContext.getUserId(), mContext.getPackageName(), channelId, true, - conversationId); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return getConversationChannelFromList(channelId, conversationId, + mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId()))); + } else { + INotificationManager service = service(); + try { + return service.getConversationNotificationChannel(mContext.getOpPackageName(), + mContext.getUserId(), mContext.getPackageName(), channelId, true, + conversationId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } @@ -1241,15 +1261,62 @@ public class NotificationManager { * {@link Context#createPackageContext(String, int)}).</p> */ public List<NotificationChannel> getNotificationChannels() { - INotificationManager service = service(); - try { - return service.getNotificationChannels(mContext.getOpPackageName(), - mContext.getPackageName(), mContext.getUserId()).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfCacheChannels()) { + return mNotificationChannelListCache.query(new NotificationChannelQuery( + mContext.getOpPackageName(), + mContext.getPackageName(), + mContext.getUserId())); + } else { + INotificationManager service = service(); + try { + return service.getNotificationChannels(mContext.getOpPackageName(), + mContext.getPackageName(), mContext.getUserId()).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } + // channel list assumed to be associated with the appropriate package & user id already. + private static NotificationChannel getChannelFromList(String channelId, + List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + for (NotificationChannel channel : channels) { + if (channelId.equals(channel.getId())) { + return channel; + } + } + return null; + } + + private static NotificationChannel getConversationChannelFromList(String channelId, + String conversationId, List<NotificationChannel> channels) { + if (channels == null) { + return null; + } + if (channelId == null) { + channelId = DEFAULT_CHANNEL_ID; + } + if (conversationId == null) { + return getChannelFromList(channelId, channels); + } + NotificationChannel parent = null; + for (NotificationChannel channel : channels) { + if (conversationId.equals(channel.getConversationId()) + && channelId.equals(channel.getParentChannelId())) { + return channel; + } else if (channelId.equals(channel.getId())) { + parent = channel; + } + } + return parent; + } + /** * Deletes the given notification channel. * @@ -1328,6 +1395,71 @@ public class NotificationManager { } } + private static final String NOTIFICATION_CHANNEL_CACHE_API = "getNotificationChannel"; + private static final String NOTIFICATION_CHANNEL_LIST_CACHE_NAME = "getNotificationChannels"; + private static final int NOTIFICATION_CHANNEL_CACHE_SIZE = 10; + + private final IpcDataCache.QueryHandler<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListQueryHandler = new IpcDataCache.QueryHandler<>() { + @Override + public List<NotificationChannel> apply(NotificationChannelQuery query) { + INotificationManager service = service(); + try { + return service.getNotificationChannels(query.callingPkg, + query.targetPkg, query.userId).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public boolean shouldBypassCache(@NonNull NotificationChannelQuery query) { + // Other locations should also not be querying the cache in the first place if + // the flag is not enabled, but this is an extra precaution. + if (!Flags.nmBinderPerfCacheChannels()) { + Log.wtf(TAG, + "shouldBypassCache called when nm_binder_perf_cache_channels off"); + return true; + } + return false; + } + }; + + private final IpcDataCache<NotificationChannelQuery, List<NotificationChannel>> + mNotificationChannelListCache = + new IpcDataCache<>(NOTIFICATION_CHANNEL_CACHE_SIZE, IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API, NOTIFICATION_CHANNEL_LIST_CACHE_NAME, + mNotificationChannelListQueryHandler); + + private record NotificationChannelQuery( + String callingPkg, + String targetPkg, + int userId) {} + + /** + * @hide + */ + public static void invalidateNotificationChannelCache() { + if (Flags.nmBinderPerfCacheChannels()) { + IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM, + NOTIFICATION_CHANNEL_CACHE_API); + } else { + // if we are here, we have failed to flag something + Log.wtf(TAG, "invalidateNotificationChannelCache called without flag"); + } + } + + /** + * For testing only: running tests with a cache requires marking the cache's property for + * testing, as test APIs otherwise cannot invalidate the cache. This must be called after + * calling PropertyInvalidatedCache.setTestMode(true). + * @hide + */ + @VisibleForTesting + public void setChannelCacheToTestMode() { + mNotificationChannelListCache.testPropertyName(); + } + /** * @hide */ diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a2fddb045179..c50452157d74 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -4354,20 +4354,24 @@ public class DevicePolicyManager { } /** - * Indicates that app functions are not controlled by policy. + * Indicates that {@link android.app.appfunctions.AppFunctionManager} is not controlled by + * policy. * * <p>If no admin set this policy, it means appfunctions are enabled. */ @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) public static final int APP_FUNCTIONS_NOT_CONTROLLED_BY_POLICY = 0; - /** Indicates that app functions are controlled and disabled by a policy. */ + /** Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and + * disabled by policy, i.e. no apps in the current user are allowed to expose app functions. + */ @FlaggedApi(android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) public static final int APP_FUNCTIONS_DISABLED = 1; /** - * Indicates that app functions are controlled and disabled by a policy for cross profile - * interactions only. + * Indicates that {@link android.app.appfunctions.AppFunctionManager} is controlled and + * disabled by a policy for cross profile interactions only, i.e. app functions exposed by apps + * in the current user can only be invoked within the same user. * * <p>This is different from {@link #APP_FUNCTIONS_DISABLED} in that it only disables cross * profile interactions (even if the caller has permissions required to interact across users). @@ -4388,7 +4392,9 @@ public class DevicePolicyManager { public @interface AppFunctionsPolicy {} /** - * Sets the app functions policy which controls app functions operations on the device. + * Sets the {@link android.app.appfunctions.AppFunctionManager} policy which controls app + * functions operations on the device. An app function is a piece of functionality that apps + * expose to the system for cross-app orchestration. * * <p>This function can only be called by a device owner, a profile owner or holders of the * permission {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_APP_FUNCTIONS}. @@ -4414,7 +4420,7 @@ public class DevicePolicyManager { } /** - * Returns the current app functions policy. + * Returns the current {@link android.app.appfunctions.AppFunctionManager} policy. * * <p>The returned policy will be the current resolved policy rather than the policy set by the * calling admin. diff --git a/core/java/android/app/backup/BackupManagerInternal.java b/core/java/android/app/backup/BackupManagerInternal.java new file mode 100644 index 000000000000..ceb5ae01f177 --- /dev/null +++ b/core/java/android/app/backup/BackupManagerInternal.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.annotation.UserIdInt; +import android.os.IBinder; + +/** + * Local system service interface for {@link com.android.server.backup.BackupManagerService}. + * + * @hide Only for use within the system server. + */ +public interface BackupManagerInternal { + + /** + * Notifies the Backup Manager Service that an agent has become available. This + * method is only invoked by the Activity Manager. + */ + void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent); + + /** + * Notify the Backup Manager Service that an agent has unexpectedly gone away. + * This method is only invoked by the Activity Manager. + */ + void agentDisconnectedForUser(String packageName, @UserIdInt int userId); +} diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 041c2a7c09f4..5d01d72c35a0 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -93,38 +93,6 @@ interface IBackupManager { IBackupObserver observer); /** - * Notifies the Backup Manager Service that an agent has become available. This - * method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has become available. - */ - void agentConnectedForUser(int userId, String packageName, IBinder agent); - - /** - * {@link android.app.backup.IBackupManager.agentConnected} for the calling user id. - */ - void agentConnected(String packageName, IBinder agent); - - /** - * Notify the Backup Manager Service that an agent has unexpectedly gone away. - * This method is only invoked by the Activity Manager. - * - * If {@code userId} is different from the calling user id, then the caller must hold the - * android.permission.INTERACT_ACROSS_USERS_FULL permission. - * - * @param userId User id for which an agent has unexpectedly gone away. - */ - void agentDisconnectedForUser(int userId, String packageName); - - /** - * {@link android.app.backup.IBackupManager.agentDisconnected} for the calling user id. - */ - void agentDisconnected(String packageName); - - /** * Notify the Backup Manager Service that an application being installed will * need a data-restore pass. This method is only invoked by the Package Manager. * diff --git a/core/java/android/app/time/TimeZoneCapabilities.java b/core/java/android/app/time/TimeZoneCapabilities.java index 4dee15988795..929f66079a3d 100644 --- a/core/java/android/app/time/TimeZoneCapabilities.java +++ b/core/java/android/app/time/TimeZoneCapabilities.java @@ -55,7 +55,8 @@ public final class TimeZoneCapabilities implements Parcelable { * The user the capabilities are for. This is used for object equality and debugging but there * is no accessor. */ - @NonNull private final UserHandle mUserHandle; + @NonNull + private final UserHandle mUserHandle; private final @CapabilityState int mConfigureAutoDetectionEnabledCapability; /** @@ -69,6 +70,7 @@ public final class TimeZoneCapabilities implements Parcelable { private final @CapabilityState int mConfigureGeoDetectionEnabledCapability; private final @CapabilityState int mSetManualTimeZoneCapability; + private final @CapabilityState int mConfigureNotificationsEnabledCapability; private TimeZoneCapabilities(@NonNull Builder builder) { this.mUserHandle = Objects.requireNonNull(builder.mUserHandle); @@ -78,6 +80,8 @@ public final class TimeZoneCapabilities implements Parcelable { this.mConfigureGeoDetectionEnabledCapability = builder.mConfigureGeoDetectionEnabledCapability; this.mSetManualTimeZoneCapability = builder.mSetManualTimeZoneCapability; + this.mConfigureNotificationsEnabledCapability = + builder.mConfigureNotificationsEnabledCapability; } @NonNull @@ -88,6 +92,7 @@ public final class TimeZoneCapabilities implements Parcelable { .setUseLocationEnabled(in.readBoolean()) .setConfigureGeoDetectionEnabledCapability(in.readInt()) .setSetManualTimeZoneCapability(in.readInt()) + .setConfigureNotificationsEnabledCapability(in.readInt()) .build(); } @@ -98,6 +103,7 @@ public final class TimeZoneCapabilities implements Parcelable { dest.writeBoolean(mUseLocationEnabled); dest.writeInt(mConfigureGeoDetectionEnabledCapability); dest.writeInt(mSetManualTimeZoneCapability); + dest.writeInt(mConfigureNotificationsEnabledCapability); } /** @@ -117,8 +123,8 @@ public final class TimeZoneCapabilities implements Parcelable { * * Not part of the SDK API because it is intended for use by SettingsUI, which can display * text about needing it to be on for location-based time zone detection. - * @hide * + * @hide */ public boolean isUseLocationEnabled() { return mUseLocationEnabled; @@ -148,6 +154,18 @@ public final class TimeZoneCapabilities implements Parcelable { } /** + * Returns the capability state associated with the user's ability to modify the time zone + * notification setting. The setting can be updated via {@link + * TimeManager#updateTimeZoneConfiguration(TimeZoneConfiguration)}. + * + * @hide + */ + @CapabilityState + public int getConfigureNotificationsEnabledCapability() { + return mConfigureNotificationsEnabledCapability; + } + + /** * Tries to create a new {@link TimeZoneConfiguration} from the {@code config} and the set of * {@code requestedChanges}, if {@code this} capabilities allow. The new configuration is * returned. If the capabilities do not permit one or more of the requested changes then {@code @@ -174,6 +192,12 @@ public final class TimeZoneCapabilities implements Parcelable { newConfigBuilder.setGeoDetectionEnabled(requestedChanges.isGeoDetectionEnabled()); } + if (requestedChanges.hasIsNotificationsEnabled()) { + if (this.getConfigureNotificationsEnabledCapability() < CAPABILITY_NOT_APPLICABLE) { + return null; + } + newConfigBuilder.setNotificationsEnabled(requestedChanges.areNotificationsEnabled()); + } return newConfigBuilder.build(); } @@ -197,13 +221,16 @@ public final class TimeZoneCapabilities implements Parcelable { && mUseLocationEnabled == that.mUseLocationEnabled && mConfigureGeoDetectionEnabledCapability == that.mConfigureGeoDetectionEnabledCapability - && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability; + && mSetManualTimeZoneCapability == that.mSetManualTimeZoneCapability + && mConfigureNotificationsEnabledCapability + == that.mConfigureNotificationsEnabledCapability; } @Override public int hashCode() { return Objects.hash(mUserHandle, mConfigureAutoDetectionEnabledCapability, - mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability); + mConfigureGeoDetectionEnabledCapability, mSetManualTimeZoneCapability, + mConfigureNotificationsEnabledCapability); } @Override @@ -216,6 +243,8 @@ public final class TimeZoneCapabilities implements Parcelable { + ", mConfigureGeoDetectionEnabledCapability=" + mConfigureGeoDetectionEnabledCapability + ", mSetManualTimeZoneCapability=" + mSetManualTimeZoneCapability + + ", mConfigureNotificationsEnabledCapability=" + + mConfigureNotificationsEnabledCapability + '}'; } @@ -226,11 +255,13 @@ public final class TimeZoneCapabilities implements Parcelable { */ public static class Builder { - @NonNull private UserHandle mUserHandle; + @NonNull + private UserHandle mUserHandle; private @CapabilityState int mConfigureAutoDetectionEnabledCapability; private Boolean mUseLocationEnabled; private @CapabilityState int mConfigureGeoDetectionEnabledCapability; private @CapabilityState int mSetManualTimeZoneCapability; + private @CapabilityState int mConfigureNotificationsEnabledCapability; public Builder(@NonNull UserHandle userHandle) { mUserHandle = Objects.requireNonNull(userHandle); @@ -240,12 +271,14 @@ public final class TimeZoneCapabilities implements Parcelable { Objects.requireNonNull(capabilitiesToCopy); mUserHandle = capabilitiesToCopy.mUserHandle; mConfigureAutoDetectionEnabledCapability = - capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability; + capabilitiesToCopy.mConfigureAutoDetectionEnabledCapability; mUseLocationEnabled = capabilitiesToCopy.mUseLocationEnabled; mConfigureGeoDetectionEnabledCapability = - capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability; + capabilitiesToCopy.mConfigureGeoDetectionEnabledCapability; mSetManualTimeZoneCapability = - capabilitiesToCopy.mSetManualTimeZoneCapability; + capabilitiesToCopy.mSetManualTimeZoneCapability; + mConfigureNotificationsEnabledCapability = + capabilitiesToCopy.mConfigureNotificationsEnabledCapability; } /** Sets the value for the "configure automatic time zone detection enabled" capability. */ @@ -274,6 +307,14 @@ public final class TimeZoneCapabilities implements Parcelable { return this; } + /** + * Sets the value for the "configure time notifications enabled" capability. + */ + public Builder setConfigureNotificationsEnabledCapability(@CapabilityState int value) { + this.mConfigureNotificationsEnabledCapability = value; + return this; + } + /** Returns the {@link TimeZoneCapabilities}. */ @NonNull public TimeZoneCapabilities build() { @@ -283,7 +324,9 @@ public final class TimeZoneCapabilities implements Parcelable { verifyCapabilitySet(mConfigureGeoDetectionEnabledCapability, "configureGeoDetectionEnabledCapability"); verifyCapabilitySet(mSetManualTimeZoneCapability, - "mSetManualTimeZoneCapability"); + "setManualTimeZoneCapability"); + verifyCapabilitySet(mConfigureNotificationsEnabledCapability, + "configureNotificationsEnabledCapability"); return new TimeZoneCapabilities(this); } diff --git a/core/java/android/app/time/TimeZoneConfiguration.java b/core/java/android/app/time/TimeZoneConfiguration.java index 7403c129f4dc..68c090f6dde3 100644 --- a/core/java/android/app/time/TimeZoneConfiguration.java +++ b/core/java/android/app/time/TimeZoneConfiguration.java @@ -62,7 +62,8 @@ public final class TimeZoneConfiguration implements Parcelable { * * @hide */ - @StringDef({ SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED }) + @StringDef({SETTING_AUTO_DETECTION_ENABLED, SETTING_GEO_DETECTION_ENABLED, + SETTING_NOTIFICATIONS_ENABLED}) @Retention(RetentionPolicy.SOURCE) @interface Setting {} @@ -74,6 +75,10 @@ public final class TimeZoneConfiguration implements Parcelable { @Setting private static final String SETTING_GEO_DETECTION_ENABLED = "geoDetectionEnabled"; + /** See {@link TimeZoneConfiguration#areNotificationsEnabled()} for details. */ + @Setting + private static final String SETTING_NOTIFICATIONS_ENABLED = "notificationsEnabled"; + @NonNull private final Bundle mBundle; private TimeZoneConfiguration(Builder builder) { @@ -98,7 +103,8 @@ public final class TimeZoneConfiguration implements Parcelable { */ public boolean isComplete() { return hasIsAutoDetectionEnabled() - && hasIsGeoDetectionEnabled(); + && hasIsGeoDetectionEnabled() + && hasIsNotificationsEnabled(); } /** @@ -128,8 +134,7 @@ public final class TimeZoneConfiguration implements Parcelable { /** * Returns the value of the {@link #SETTING_GEO_DETECTION_ENABLED} setting. This * controls whether the device can use geolocation to determine time zone. This value may only - * be used by Android under some circumstances. For example, it is not used when - * {@link #isGeoDetectionEnabled()} is {@code false}. + * be used by Android under some circumstances. * * <p>See {@link TimeZoneCapabilities#getConfigureGeoDetectionEnabledCapability()} for how to * tell if the setting is meaningful for the current user at this time. @@ -150,6 +155,32 @@ public final class TimeZoneConfiguration implements Parcelable { return mBundle.containsKey(SETTING_GEO_DETECTION_ENABLED); } + /** + * Returns the value of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. This controls + * whether the device can send time and time zone related notifications. This value may only + * be used by Android under some circumstances. + * + * <p>See {@link TimeZoneCapabilities#getConfigureNotificationsEnabledCapability()} ()} for how + * to tell if the setting is meaningful for the current user at this time. + * + * @throws IllegalStateException if the setting is not present + * + * @hide + */ + public boolean areNotificationsEnabled() { + enforceSettingPresent(SETTING_NOTIFICATIONS_ENABLED); + return mBundle.getBoolean(SETTING_NOTIFICATIONS_ENABLED); + } + + /** + * Returns {@code true} if the {@link #areNotificationsEnabled()} setting is present. + * + * @hide + */ + public boolean hasIsNotificationsEnabled() { + return mBundle.containsKey(SETTING_NOTIFICATIONS_ENABLED); + } + @Override public int describeContents() { return 0; @@ -244,6 +275,17 @@ public final class TimeZoneConfiguration implements Parcelable { return this; } + /** + * Sets the state of the {@link #SETTING_NOTIFICATIONS_ENABLED} setting. * + * + * @hide + */ + @NonNull + public Builder setNotificationsEnabled(boolean enabled) { + this.mBundle.putBoolean(SETTING_NOTIFICATIONS_ENABLED, enabled); + return this; + } + /** Returns the {@link TimeZoneConfiguration}. */ @NonNull public TimeZoneConfiguration build() { diff --git a/core/java/android/app/wallpaper.aconfig b/core/java/android/app/wallpaper.aconfig index f750a844f4ff..be9e286f5eb7 100644 --- a/core/java/android/app/wallpaper.aconfig +++ b/core/java/android/app/wallpaper.aconfig @@ -40,3 +40,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_connected_displays_wallpaper" + namespace: "lse_desktop_experience" + description: "Enable wallpaper support in connected displays" + bug: "366461618" +} diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 316d129bd6b9..971d402569c0 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -62,10 +62,11 @@ import java.util.concurrent.Executor; * * <p> * If the companion application has requested observing device presence (see - * {@link CompanionDeviceManager#startObservingDevicePresence(String)}) the system will - * <a href="https://developer.android.com/guide/components/bound-services"> bind the service</a> - * when it detects the device nearby (for BLE devices) or when the device is connected - * (for Bluetooth devices). + * {@link CompanionDeviceManager#stopObservingDevicePresence(ObservingDevicePresenceRequest)}) + * the system will <a href="https://developer.android.com/guide/components/bound-services"> + * bind the service</a> when one of the {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * * <p> * The system binding {@link CompanionDeviceService} elevates the priority of the process that @@ -102,15 +103,25 @@ public abstract class CompanionDeviceService extends Service { /** * An intent action for a service to be bound whenever this app's companion device(s) - * are nearby. + * are nearby or self-managed device(s) report app appeared. * - * <p>The app will be kept alive for as long as the device is nearby or companion app reports - * appeared. - * If the app is not running at the time device gets connected, the app will be woken up.</p> + * <p>The app will be kept bound by the system when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified. * - * <p>Shortly after the device goes out of range or the companion app reports disappeared, - * the service will be unbound, and the app will be eligible for cleanup, unless any other - * user-visible components are running.</p> + * If the app is not running when one of the + * {@link DevicePresenceEvent#EVENT_BLE_APPEARED}, + * {@link DevicePresenceEvent#EVENT_BT_CONNECTED}, + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_APPEARED} event is notified, the app will be + * kept bound by the system.</p> + * + * <p>Shortly, the service will be unbound if both + * {@link DevicePresenceEvent#EVENT_BLE_DISAPPEARED} and + * {@link DevicePresenceEvent#EVENT_BT_DISCONNECTED} are notified, or + * {@link DevicePresenceEvent#EVENT_SELF_MANAGED_DISAPPEARED} event is notified. + * The app will be eligible for cleanup, unless any other user-visible components are + * running.</p> * * If running in background is not essential for the devices that this app can manage, * app should avoid declaring this service.</p> diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index 42c74414ecd9..3ef78affb7a5 100644 --- a/core/java/android/companion/virtual/VirtualDeviceInternal.java +++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java @@ -32,7 +32,6 @@ import android.companion.virtual.audio.VirtualAudioDevice; import android.companion.virtual.camera.VirtualCamera; import android.companion.virtual.camera.VirtualCameraConfig; import android.companion.virtual.sensor.VirtualSensor; -import android.companion.virtualdevice.flags.Flags; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -83,7 +82,6 @@ import java.util.function.IntConsumer; public class VirtualDeviceInternal { private final Context mContext; - private final IVirtualDeviceManager mService; private final IVirtualDevice mVirtualDevice; private final Object mActivityListenersLock = new Object(); @GuardedBy("mActivityListenersLock") @@ -206,7 +204,6 @@ public class VirtualDeviceInternal { Context context, int associationId, VirtualDeviceParams params) throws RemoteException { - mService = service; mContext = context.getApplicationContext(); mVirtualDevice = service.createVirtualDevice( new Binder(), @@ -217,11 +214,7 @@ public class VirtualDeviceInternal { mSoundEffectListener); } - VirtualDeviceInternal( - IVirtualDeviceManager service, - Context context, - IVirtualDevice virtualDevice) { - mService = service; + VirtualDeviceInternal(Context context, IVirtualDevice virtualDevice) { mContext = context.getApplicationContext(); mVirtualDevice = virtualDevice; try { @@ -479,14 +472,12 @@ public class VirtualDeviceInternal { @Nullable VirtualAudioDevice.AudioConfigurationChangeCallback callback) { if (mVirtualAudioDevice == null) { try { - Context context = mContext; - if (Flags.deviceAwareRecordAudioPermission()) { - // When using a default policy for audio device-aware RECORD_AUDIO permission - // should not take effect, thus register policies with the default context. - if (mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM) { - context = mContext.createDeviceContext(getDeviceId()); - } - } + // When using a default policy for audio, the device-aware RECORD_AUDIO permission + // should not take effect, thus register policies with the default context. + final Context context = + mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM + ? mContext.createDeviceContext(getDeviceId()) + : mContext; mVirtualAudioDevice = new VirtualAudioDevice(context, mVirtualDevice, display, executor, callback, () -> mVirtualAudioDevice = null); } catch (RemoteException e) { diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index ed2fd99c55c5..91ea673ab6f9 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -170,7 +170,6 @@ public final class VirtualDeviceManager { * @hide */ @SystemApi - @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API) public static final String PERSISTENT_DEVICE_ID_DEFAULT = "default:" + Context.DEVICE_ID_DEFAULT; @@ -393,7 +392,6 @@ public final class VirtualDeviceManager { * @hide */ // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId() - @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API) @SystemApi @Nullable public CharSequence getDisplayNameForPersistentDeviceId(@NonNull String persistentDeviceId) { @@ -416,7 +414,6 @@ public final class VirtualDeviceManager { * @hide */ // TODO(b/315481938): Link @see VirtualDevice#getPersistentDeviceId() - @FlaggedApi(Flags.FLAG_PERSISTENT_DEVICE_ID_API) @SystemApi @NonNull public Set<String> getAllPersistentDeviceIds() { @@ -577,9 +574,8 @@ public final class VirtualDeviceManager { } /** @hide */ - public VirtualDevice(IVirtualDeviceManager service, Context context, - IVirtualDevice virtualDevice) { - mVirtualDeviceInternal = new VirtualDeviceInternal(service, context, virtualDevice); + public VirtualDevice(Context context, IVirtualDevice virtualDevice) { + mVirtualDeviceInternal = new VirtualDeviceInternal(context, virtualDevice); } /** @@ -781,7 +777,6 @@ public final class VirtualDeviceManager { * @see VirtualDeviceParams#POLICY_TYPE_RECENTS * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY */ - @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType, @VirtualDeviceParams.DevicePolicy int devicePolicy) { mVirtualDeviceInternal.setDevicePolicy(policyType, devicePolicy); @@ -803,7 +798,6 @@ public final class VirtualDeviceManager { * @see #removeActivityPolicyExemption(ComponentName) * @see #setDevicePolicy */ - @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) public void addActivityPolicyExemption(@NonNull ComponentName componentName) { addActivityPolicyExemption(new ActivityPolicyExemption.Builder() .setComponentName(componentName) @@ -826,7 +820,6 @@ public final class VirtualDeviceManager { * @see #addActivityPolicyExemption(ComponentName) * @see #setDevicePolicy */ - @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) public void removeActivityPolicyExemption(@NonNull ComponentName componentName) { removeActivityPolicyExemption(new ActivityPolicyExemption.Builder() .setComponentName(componentName) @@ -1038,9 +1031,7 @@ public final class VirtualDeviceManager { * @param config the touchscreen configurations for the virtual stylus. */ @NonNull - @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) - public VirtualStylus createVirtualStylus( - @NonNull VirtualStylusConfig config) { + public VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) { return mVirtualDeviceInternal.createVirtualStylus(config); } diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index 2be27dabcf90..761e75bd9076 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -248,7 +248,6 @@ public final class VirtualDeviceParams implements Parcelable { */ // TODO(b/333443509): Update the documentation of custom policy and link to the new policy // POLICY_TYPE_BLOCKED_ACTIVITY - @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) public static final int POLICY_TYPE_ACTIVITY = 3; /** @@ -264,7 +263,6 @@ public final class VirtualDeviceParams implements Parcelable { * * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED */ - @FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD) public static final int POLICY_TYPE_CLIPBOARD = 4; /** @@ -431,7 +429,6 @@ public final class VirtualDeviceParams implements Parcelable { * @see Builder#setHomeComponent * @see VirtualDisplayConfig#isHomeSupported() */ - @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @Nullable public ComponentName getHomeComponent() { return mHomeComponent; @@ -926,7 +923,6 @@ public final class VirtualDeviceParams implements Parcelable { * * @see VirtualDisplayConfig#isHomeSupported() */ - @FlaggedApi(Flags.FLAG_VDM_CUSTOM_HOME) @NonNull public Builder setHomeComponent(@Nullable ComponentName homeComponent) { mHomeComponent = homeComponent; @@ -1282,33 +1278,31 @@ public final class VirtualDeviceParams implements Parcelable { mVirtualSensorDirectChannelCallback); } - if (Flags.dynamicPolicy()) { - switch (mDevicePolicies.get(POLICY_TYPE_ACTIVITY, -1)) { - case DEVICE_POLICY_DEFAULT: - if (mDefaultActivityPolicyConfigured - && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) { - throw new IllegalArgumentException( - "DEVICE_POLICY_DEFAULT is explicitly configured for " - + "POLICY_TYPE_ACTIVITY, which is exclusive with " - + "setAllowedActivities."); - } - break; - case DEVICE_POLICY_CUSTOM: - if (mDefaultActivityPolicyConfigured - && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_ALLOWED) { - throw new IllegalArgumentException( - "DEVICE_POLICY_CUSTOM is explicitly configured for " - + "POLICY_TYPE_ACTIVITY, which is exclusive with " - + "setBlockedActivities."); - } - break; - default: - if (mDefaultActivityPolicyConfigured - && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) { - mDevicePolicies.put(POLICY_TYPE_ACTIVITY, DEVICE_POLICY_CUSTOM); - } - break; - } + switch (mDevicePolicies.get(POLICY_TYPE_ACTIVITY, -1)) { + case DEVICE_POLICY_DEFAULT: + if (mDefaultActivityPolicyConfigured + && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) { + throw new IllegalArgumentException( + "DEVICE_POLICY_DEFAULT is explicitly configured for " + + "POLICY_TYPE_ACTIVITY, which is exclusive with " + + "setAllowedActivities."); + } + break; + case DEVICE_POLICY_CUSTOM: + if (mDefaultActivityPolicyConfigured + && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_ALLOWED) { + throw new IllegalArgumentException( + "DEVICE_POLICY_CUSTOM is explicitly configured for " + + "POLICY_TYPE_ACTIVITY, which is exclusive with " + + "setBlockedActivities."); + } + break; + default: + if (mDefaultActivityPolicyConfigured + && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) { + mDevicePolicies.put(POLICY_TYPE_ACTIVITY, DEVICE_POLICY_CUSTOM); + } + break; } if (mDimDuration.compareTo(mScreenOffTimeout) > 0) { @@ -1319,10 +1313,6 @@ public final class VirtualDeviceParams implements Parcelable { mScreenOffTimeout = INFINITE_TIMEOUT; } - if (!Flags.crossDeviceClipboard()) { - mDevicePolicies.delete(POLICY_TYPE_CLIPBOARD); - } - if (!Flags.virtualCamera()) { mDevicePolicies.delete(POLICY_TYPE_CAMERA); } diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 84af84072f1b..6da2a073ec19 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -19,14 +19,6 @@ flag { flag { namespace: "virtual_devices" - name: "device_aware_record_audio_permission" - description: "Enable device-aware RECORD_AUDIO permission" - bug: "291737188" - is_fixed_read_only: true -} - -flag { - namespace: "virtual_devices" name: "media_projection_keyguard_restrictions" description: "Auto-stop MP when the device locks" bug: "348335290" diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index e271cf4f60ec..4e292d0b7d18 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -221,6 +221,12 @@ public class ClipData implements Parcelable { // if the data is obtained from {@link #copyForTransferWithActivityInfo} private ActivityInfo mActivityInfo; + private boolean mTokenVerificationEnabled; + + void setTokenVerificationEnabled() { + mTokenVerificationEnabled = true; + } + /** * A builder for a ClipData Item. */ @@ -398,7 +404,9 @@ public class ClipData implements Parcelable { * Retrieve the raw Intent contained in this Item. */ public Intent getIntent() { - Intent.maybeMarkAsMissingCreatorToken(mIntent); + if (mTokenVerificationEnabled) { + Intent.maybeMarkAsMissingCreatorToken(mIntent); + } return mIntent; } @@ -1353,6 +1361,12 @@ public class ClipData implements Parcelable { } } + void setTokenVerificationEnabled() { + for (int i = 0; i < mItems.size(); ++i) { + mItems.get(i).setTokenVerificationEnabled(); + } + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e3e10388754c..885a2dbc471e 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -4215,6 +4215,17 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_USER_INFO_CHANGED = "android.intent.action.USER_INFO_CHANGED"; + + /** + * Broadcast sent to the system when a user's information changes. Carries an extra + * {@link #EXTRA_USER_HANDLE} to indicate which user's information changed. + * This is only sent to permission protected manifest receivers. It is sent to all users. + * @hide + */ + @BroadcastBehavior(includeBackground = true) + public static final String ACTION_USER_INFO_CHANGED_BACKGROUND = + "android.intent.action.USER_INFO_CHANGED_BACKGROUND"; + /** * Broadcast sent to the primary user when an associated managed profile is added (the profile * was created and is ready to be used). Carries an extra {@link #EXTRA_USER} that specifies @@ -5460,7 +5471,7 @@ public class Intent implements Parcelable, Cloneable { /** * Activities that can be safely invoked from a browser must support this * category. For example, if the user is viewing a web page or an e-mail - * and clicks on a link in the text, the Intent generated execute that + * and clicks on a link in the text, the Intent generated to execute that * link will require the BROWSABLE category, so that only activities * supporting this category will be considered as possible actions. By * supporting this category, you are promising that there is nothing @@ -12394,14 +12405,30 @@ public class Intent implements Parcelable, Cloneable { * @hide */ public void collectExtraIntentKeys() { + collectExtraIntentKeys(false); + } + + /** + * Collects keys in the extra bundle whose value are intents. + * With these keys collected on the client side, the system server would only unparcel values + * of these keys and create IntentCreatorToken for them. + * This method could also be called from the system server side as a catch all safty net in case + * these keys are not collected on the client side. In that case, call it with forceUnparcel set + * to true since everything is parceled on the system server side. + * + * @param forceUnparcel if it is true, unparcel everything to determine if an object is an + * intent. Otherwise, do not unparcel anything. + * @hide + */ + public void collectExtraIntentKeys(boolean forceUnparcel) { if (preventIntentRedirect()) { - collectNestedIntentKeysRecur(new ArraySet<>()); + collectNestedIntentKeysRecur(new ArraySet<>(), forceUnparcel); } } - private void collectNestedIntentKeysRecur(Set<Intent> visited) { + private void collectNestedIntentKeysRecur(Set<Intent> visited, boolean forceUnparcel) { addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED); - if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) { + if (mExtras != null && (forceUnparcel || !mExtras.isParcelled()) && !mExtras.isEmpty()) { for (String key : mExtras.keySet()) { Object value; try { @@ -12410,23 +12437,25 @@ public class Intent implements Parcelable, Cloneable { // It is okay to not collect a parceled intent since it would have been // coming from another process and collected by its containing intent already // in that process. - if (!mExtras.isValueParceled(key)) { + if (forceUnparcel || !mExtras.isValueParceled(key)) { value = mExtras.get(key); } else { value = null; } } catch (BadParcelableException e) { - // This probably would never happen. But just in case, simply ignore it since - // it is not an intent anyway. + // This may still happen if the keys are collected on the system server side, in + // which case, we will try to unparcel everything. If this happens, simply + // ignore it since it is not an intent anyway. value = null; } if (value instanceof Intent intent) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL, key, 0), + forceUnparcel); } else if (value instanceof Parcelable[] parcelables) { - handleParcelableArray(parcelables, key, visited); + handleParcelableArray(parcelables, key, visited, forceUnparcel); } else if (value instanceof ArrayList<?> parcelables) { - handleParcelableList(parcelables, key, visited); + handleParcelableList(parcelables, key, visited, forceUnparcel); } } } @@ -12436,13 +12465,15 @@ public class Intent implements Parcelable, Cloneable { Intent intent = mClipData.getItemAt(i).mIntent; if (intent != null && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_CLIP_DATA, null, i), + forceUnparcel); } } } } - private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key) { + private void handleNestedIntent(Intent intent, Set<Intent> visited, NestedIntentKey key, + boolean forceUnparcel) { if (mCreatorTokenInfo == null) { mCreatorTokenInfo = new CreatorTokenInfo(); } @@ -12452,24 +12483,28 @@ public class Intent implements Parcelable, Cloneable { mCreatorTokenInfo.mNestedIntentKeys.add(key); if (!visited.contains(intent)) { visited.add(intent); - intent.collectNestedIntentKeysRecur(visited); + intent.collectNestedIntentKeysRecur(visited, forceUnparcel); } } - private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited) { + private void handleParcelableArray(Parcelable[] parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.length; i++) { if (parcelables[i] instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_ARRAY, key, i), + forceUnparcel); } } } - private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited) { + private void handleParcelableList(ArrayList<?> parcelables, String key, Set<Intent> visited, + boolean forceUnparcel) { for (int i = 0; i < parcelables.size(); i++) { if (parcelables.get(i) instanceof Intent intent && !visited.contains(intent)) { handleNestedIntent(intent, visited, new NestedIntentKey( - NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i)); + NestedIntentKey.NESTED_INTENT_KEY_TYPE_EXTRA_PARCEL_LIST, key, i), + forceUnparcel); } } } @@ -12482,6 +12517,9 @@ public class Intent implements Parcelable, Cloneable { if (intent.mExtras != null) { intent.mExtras.enableTokenVerification(); } + if (intent.mClipData != null) { + intent.mClipData.setTokenVerificationEnabled(); + } }; /** @hide */ @@ -12493,6 +12531,9 @@ public class Intent implements Parcelable, Cloneable { // mark trusted creator token present. mExtras.enableTokenVerification(); } + if (mClipData != null) { + mClipData.setTokenVerificationEnabled(); + } } /** @hide */ diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index a0c0f122497f..1724d9ff0deb 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -1932,6 +1932,9 @@ public class LauncherApps { * caller should have normal {@link android.Manifest.permission#ACCESS_HIDDEN_PROFILES} * permission and the {@link android.app.role.RoleManager#ROLE_HOME} role. * + * <p>This callback will also receive changes to the {@link LauncherUserInfo#getUserConfig()}, + * allowing clients to monitor updates to the user-specific configuration. + * * @param callback The callback to register. */ // Alternatively, a system app can access this api for private profile if they've been granted @@ -1950,6 +1953,9 @@ public class LauncherApps { * caller should have normal {@link android.Manifest.permission#ACCESS_HIDDEN_PROFILES} * permission and the {@link android.app.role.RoleManager#ROLE_HOME} role. * + * <p>This callback will also receive changes to the {@link LauncherUserInfo#getUserConfig()}, + * allowing clients to monitor updates to the user-specific configuration. + * * @param callback The callback to register. * @param handler that should be used to post callbacks on, may be null. */ diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index c16582f19c9b..8c7e93a834b7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -4649,6 +4649,7 @@ public abstract class PackageManager { * the Android Keystore backed by an isolated execution environment. The version indicates * which features are implemented in the isolated execution environment: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: Hardware support for Curve 25519 (including both Ed25519 signature generation and @@ -4682,6 +4683,7 @@ public abstract class PackageManager { * StrongBox</a>. If this feature has a version, the version number indicates which features are * implemented in StrongBox: * <ul> + * <li>400: Inclusion of module information (via tag MODULE_HASH) in the attestation record. * <li>300: Ability to include a second IMEI in the ID attestation record, see * {@link android.app.admin.DevicePolicyManager#ID_TYPE_IMEI}. * <li>200: No new features for StrongBox (the Android Keystore environment backed by an diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index b938aac811fd..075457885586 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -25,7 +25,6 @@ import android.content.res.loader.ResourcesProvider; import android.ravenwood.annotation.RavenwoodClassLoadHook; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.text.TextUtils; -import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -51,7 +50,6 @@ import java.util.Objects; @RavenwoodKeepWholeClass @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class ApkAssets { - private static final boolean DEBUG = false; /** * The apk assets contains framework resource values specified by the system. @@ -136,17 +134,6 @@ public final class ApkAssets { @Nullable private final AssetsProvider mAssets; - @NonNull - private String mName; - - private static final int UPTODATE_FALSE = 0; - private static final int UPTODATE_TRUE = 1; - private static final int UPTODATE_ALWAYS_TRUE = 2; - - // Start with the only value that may change later and would force a native call to - // double check it. - private int mPreviousUpToDateResult = UPTODATE_TRUE; - /** * Creates a new ApkAssets instance from the given path on disk. * @@ -317,7 +304,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets, path); + this(format, flags, assets); Objects.requireNonNull(path, "path"); mNativePtr = nativeLoad(format, path, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); @@ -326,7 +313,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets, friendlyName); + this(format, flags, assets); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets); @@ -336,7 +323,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets, friendlyName); + this(format, flags, assets); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets); @@ -344,17 +331,16 @@ public final class ApkAssets { } private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) { - this(FORMAT_APK, flags, assets, "empty"); + this(FORMAT_APK, flags, assets); mNativePtr = nativeLoadEmpty(flags, assets); mStringBlock = null; } private ApkAssets(@FormatType int format, @PropertyFlags int flags, - @Nullable AssetsProvider assets, @NonNull String name) { + @Nullable AssetsProvider assets) { mFlags = flags; mAssets = assets; mIsOverlay = format == FORMAT_IDMAP; - if (DEBUG) mName = name; } @UnsupportedAppUsage @@ -367,7 +353,7 @@ public final class ApkAssets { /** @hide */ public @NonNull String getDebugName() { synchronized (this) { - return nativeGetDebugName(mNativePtr); + return mNativePtr == 0 ? "<destroyed>" : nativeGetDebugName(mNativePtr); } } @@ -435,41 +421,13 @@ public final class ApkAssets { } } - private static double intervalMs(long beginNs, long endNs) { - return (endNs - beginNs) / 1000000.0; - } - /** * Returns false if the underlying APK was changed since this ApkAssets was loaded. */ public boolean isUpToDate() { - // This function is performance-critical - it's called multiple times on every Resources - // object creation, and on few other cache accesses - so it's important to avoid the native - // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE - // and FALSE). - if (mPreviousUpToDateResult != UPTODATE_TRUE) { - return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE; - } - final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs; - if (DEBUG) beforeTs = System.nanoTime(); - final int res; synchronized (this) { - if (DEBUG) afterLockTs = System.nanoTime(); - res = nativeIsUpToDate(mNativePtr); - if (DEBUG) afterNativeTs = System.nanoTime(); - } - if (DEBUG) { - afterUnlockTs = System.nanoTime(); - if (afterUnlockTs - beforeTs >= 10L * 1000000) { - Log.d("ApkAssets", "isUpToDate(" + mName + ") took " - + intervalMs(beforeTs, afterUnlockTs) - + " ms: " + intervalMs(beforeTs, afterLockTs) - + " / " + intervalMs(afterLockTs, afterNativeTs) - + " / " + intervalMs(afterNativeTs, afterUnlockTs)); - } + return nativeIsUpToDate(mNativePtr); } - mPreviousUpToDateResult = res; - return res != UPTODATE_FALSE; } public boolean isSystem() { @@ -529,7 +487,7 @@ public final class ApkAssets { private static native @NonNull String nativeGetAssetPath(long ptr); private static native @NonNull String nativeGetDebugName(long ptr); private static native long nativeGetStringBlock(long ptr); - @CriticalNative private static native int nativeIsUpToDate(long ptr); + @CriticalNative private static native boolean nativeIsUpToDate(long ptr); private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException; private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr, String overlayableName) throws IOException; diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java index 2d1bf4d9d296..d51f64ce8106 100644 --- a/core/java/android/content/res/ResourceTimer.java +++ b/core/java/android/content/res/ResourceTimer.java @@ -17,10 +17,13 @@ package android.content.res; import android.annotation.NonNull; +import android.annotation.Nullable; + import android.app.AppProtoEnums; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; @@ -30,7 +33,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; -import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.Arrays; @@ -275,40 +277,38 @@ public final class ResourceTimer { * Update the metrics information and dump it. * @hide */ - public static void dumpTimers(@NonNull FileDescriptor fd, String... args) { - try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) { - pw.println("\nDumping ResourceTimers"); - - final boolean enabled; - synchronized (sLock) { - enabled = sEnabled && sConfig != null; - } - if (!enabled) { + public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) { + FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor()); + PrintWriter pw = new FastPrintWriter(fout); + synchronized (sLock) { + if (!sEnabled || (sConfig == null)) { pw.println(" Timers are not enabled in this process"); + pw.flush(); return; } + } - // Look for the --refresh switch. If the switch is present, then sTimers is updated. - // Otherwise, the current value of sTimers is displayed. - boolean refresh = Arrays.asList(args).contains("-refresh"); - - synchronized (sLock) { - update(refresh); - long runtime = sLastUpdated - sProcessStart; - pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); - for (int i = 0; i < sTimers.length; i++) { - Timer t = sTimers[i]; - if (t.count != 0) { - String name = sConfig.timers[i]; - pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " - + "largest=%s\n", - name, t.count, t.total / t.count, t.mintime, t.maxtime, - packedString(t.percentile), - packedString(t.largest)); - } + // Look for the --refresh switch. If the switch is present, then sTimers is updated. + // Otherwise, the current value of sTimers is displayed. + boolean refresh = Arrays.asList(args).contains("-refresh"); + + synchronized (sLock) { + update(refresh); + long runtime = sLastUpdated - sProcessStart; + pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); + for (int i = 0; i < sTimers.length; i++) { + Timer t = sTimers[i]; + if (t.count != 0) { + String name = sConfig.timers[i]; + pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " + + "largest=%s\n", + name, t.count, t.total / t.count, t.mintime, t.maxtime, + packedString(t.percentile), + packedString(t.largest)); } } } + pw.flush(); } // Enable (or disabled) the runtime timers. Note that timers are disabled by default. This diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index 71702d996883..25cdc508fdce 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -137,6 +137,8 @@ public class HubEndpoint { serviceDescriptor, mLifecycleCallback.onSessionOpenRequest( initiator, serviceDescriptor))); + } else { + invokeCallbackFinished(); } } @@ -163,6 +165,8 @@ public class HubEndpoint { + result.getReason()); rejectSession(sessionId); } + + invokeCallbackFinished(); } private void acceptSession( @@ -249,7 +253,12 @@ public class HubEndpoint { activeSession.setOpened(); if (mLifecycleCallback != null) { mLifecycleCallbackExecutor.execute( - () -> mLifecycleCallback.onSessionOpened(activeSession)); + () -> { + mLifecycleCallback.onSessionOpened(activeSession); + invokeCallbackFinished(); + }); + } else { + invokeCallbackFinished(); } } @@ -278,7 +287,10 @@ public class HubEndpoint { synchronized (mLock) { mActiveSessions.remove(sessionId); } + invokeCallbackFinished(); }); + } else { + invokeCallbackFinished(); } } @@ -323,8 +335,17 @@ public class HubEndpoint { e.rethrowFromSystemServer(); } } + invokeCallbackFinished(); }); } + + private void invokeCallbackFinished() { + try { + mServiceToken.onCallbackFinished(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } }; /** Binder returned from system service, non-null while registered. */ diff --git a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl index 44f80c819e83..eb1255c06094 100644 --- a/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl +++ b/core/java/android/hardware/contexthub/IContextHubEndpoint.aidl @@ -94,4 +94,10 @@ interface IContextHubEndpoint { */ @EnforcePermission("ACCESS_CONTEXT_HUB") void sendMessageDeliveryStatus(int sessionId, int messageSeqNumber, byte errorCode); + + /** + * Invoked when a callback from IContextHubEndpointCallback finishes. + */ + @EnforcePermission("ACCESS_CONTEXT_HUB") + void onCallbackFinished(); } diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig index 6230f4dbf6f4..44d662f2a4fc 100644 --- a/core/java/android/hardware/devicestate/feature/flags.aconfig +++ b/core/java/android/hardware/devicestate/feature/flags.aconfig @@ -38,4 +38,16 @@ flag { description: "Enables Rear Display Mode V2, where the inner display shows the user a UI affordance for exiting the state" bug: "372486634" is_fixed_read_only: true -}
\ No newline at end of file +} + +flag { + name: "device_state_configuration_flag" + is_exported: true + namespace: "windowing_sdk" + description: "Re-add flag parsing for device_state_configuration.xml configuration for devices that didn't update vendor images." + bug: "388366842" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index 72570553f78a..2a9ee7f07934 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -237,10 +237,9 @@ public final class VirtualDisplayConfig implements Parcelable { * @see Builder#setHomeSupported * @hide */ - @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME) @SystemApi public boolean isHomeSupported() { - return android.companion.virtual.flags.Flags.vdmCustomHome() && mIsHomeSupported; + return mIsHomeSupported; } /** @@ -605,7 +604,6 @@ public final class VirtualDisplayConfig implements Parcelable { * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY * @hide */ - @FlaggedApi(android.companion.virtual.flags.Flags.FLAG_VDM_CUSTOM_HOME) @SystemApi @NonNull public Builder setHomeSupported(boolean isHomeSupported) { diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java index d84d29292ed5..8fbe05c4e9eb 100644 --- a/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java +++ b/core/java/android/hardware/fingerprint/FingerprintSensorPropertiesInternal.java @@ -121,15 +121,20 @@ public class FingerprintSensorPropertiesInternal extends SensorPropertiesInterna /** * Returns if sensor type is ultrasonic Udfps - * @return true if sensor is ultrasonic Udfps, false otherwise */ public boolean isUltrasonicUdfps() { return sensorType == TYPE_UDFPS_ULTRASONIC; } /** + * Returns if sensor type is optical Udfps + */ + public boolean isOpticalUdfps() { + return sensorType == TYPE_UDFPS_OPTICAL; + } + + /** * Returns if sensor type is side-FPS - * @return true if sensor is side-fps, false otherwise */ public boolean isAnySidefpsType() { switch (sensorType) { diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index ed510e467f82..2bb28a1b6b0b 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -266,6 +266,11 @@ interface IInputManager { @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + AidlInputGestureData getInputGesture(int userId, in AidlInputGestureData.Trigger trigger); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") int addCustomInputGesture(int userId, in AidlInputGestureData data); @PermissionManuallyEnforced diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java index f41550f6061e..75c652c973e4 100644 --- a/core/java/android/hardware/input/InputGestureData.java +++ b/core/java/android/hardware/input/InputGestureData.java @@ -48,27 +48,7 @@ public final class InputGestureData { /** Returns the trigger information for this input gesture */ public Trigger getTrigger() { - switch (mInputGestureData.trigger.getTag()) { - case AidlInputGestureData.Trigger.Tag.key: { - AidlInputGestureData.KeyTrigger trigger = mInputGestureData.trigger.getKey(); - if (trigger == null) { - throw new RuntimeException("InputGestureData is corrupted, null key trigger!"); - } - return createKeyTrigger(trigger.keycode, trigger.modifierState); - } - case AidlInputGestureData.Trigger.Tag.touchpadGesture: { - AidlInputGestureData.TouchpadGestureTrigger trigger = - mInputGestureData.trigger.getTouchpadGesture(); - if (trigger == null) { - throw new RuntimeException( - "InputGestureData is corrupted, null touchpad trigger!"); - } - return createTouchpadTrigger(trigger.gestureType); - } - default: - throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!"); - - } + return createTriggerFromAidlTrigger(mInputGestureData.trigger); } /** Returns the action to perform for this input gesture */ @@ -147,18 +127,7 @@ public final class InputGestureData { "No app launch data for system action launch application"); } AidlInputGestureData data = new AidlInputGestureData(); - data.trigger = new AidlInputGestureData.Trigger(); - if (mTrigger instanceof KeyTrigger keyTrigger) { - data.trigger.setKey(new AidlInputGestureData.KeyTrigger()); - data.trigger.getKey().keycode = keyTrigger.getKeycode(); - data.trigger.getKey().modifierState = keyTrigger.getModifierState(); - } else if (mTrigger instanceof TouchpadTrigger touchpadTrigger) { - data.trigger.setTouchpadGesture(new AidlInputGestureData.TouchpadGestureTrigger()); - data.trigger.getTouchpadGesture().gestureType = - touchpadTrigger.getTouchpadGestureType(); - } else { - throw new IllegalArgumentException("Invalid trigger type!"); - } + data.trigger = mTrigger.getAidlTrigger(); data.gestureType = mKeyGestureType; if (mAppLaunchData != null) { if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) { @@ -198,6 +167,7 @@ public final class InputGestureData { } public interface Trigger { + AidlInputGestureData.Trigger getAidlTrigger(); } /** Creates a input gesture trigger based on a key press */ @@ -210,85 +180,128 @@ public final class InputGestureData { return new TouchpadTrigger(touchpadGestureType); } + public static Trigger createTriggerFromAidlTrigger(AidlInputGestureData.Trigger aidlTrigger) { + switch (aidlTrigger.getTag()) { + case AidlInputGestureData.Trigger.Tag.key: { + AidlInputGestureData.KeyTrigger trigger = aidlTrigger.getKey(); + if (trigger == null) { + throw new RuntimeException("aidlTrigger is corrupted, null key trigger!"); + } + return new KeyTrigger(trigger); + } + case AidlInputGestureData.Trigger.Tag.touchpadGesture: { + AidlInputGestureData.TouchpadGestureTrigger trigger = + aidlTrigger.getTouchpadGesture(); + if (trigger == null) { + throw new RuntimeException( + "aidlTrigger is corrupted, null touchpad trigger!"); + } + return new TouchpadTrigger(trigger); + } + default: + throw new RuntimeException("aidlTrigger is corrupted, invalid trigger type!"); + + } + } + /** Key based input gesture trigger */ public static class KeyTrigger implements Trigger { - private static final int SHORTCUT_META_MASK = - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON - | KeyEvent.META_SHIFT_ON; - private final int mKeycode; - private final int mModifierState; + + AidlInputGestureData.KeyTrigger mAidlKeyTrigger; + + private KeyTrigger(@NonNull AidlInputGestureData.KeyTrigger aidlKeyTrigger) { + mAidlKeyTrigger = aidlKeyTrigger; + } private KeyTrigger(int keycode, int modifierState) { if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) { throw new IllegalArgumentException("Invalid keycode = " + keycode); } - mKeycode = keycode; - mModifierState = modifierState; + mAidlKeyTrigger = new AidlInputGestureData.KeyTrigger(); + mAidlKeyTrigger.keycode = keycode; + mAidlKeyTrigger.modifierState = modifierState; } public int getKeycode() { - return mKeycode; + return mAidlKeyTrigger.keycode; } public int getModifierState() { - return mModifierState; + return mAidlKeyTrigger.modifierState; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setKey(mAidlKeyTrigger); + return trigger; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof KeyTrigger that)) return false; - return mKeycode == that.mKeycode && mModifierState == that.mModifierState; + return Objects.equals(mAidlKeyTrigger, that.mAidlKeyTrigger); } @Override public int hashCode() { - return Objects.hash(mKeycode, mModifierState); + return mAidlKeyTrigger.hashCode(); } @Override public String toString() { return "KeyTrigger{" + - "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) + - ", mModifierState=" + mModifierState + + "mKeycode=" + KeyEvent.keyCodeToString(mAidlKeyTrigger.keycode) + + ", mModifierState=" + mAidlKeyTrigger.modifierState + '}'; } } /** Touchpad based input gesture trigger */ public static class TouchpadTrigger implements Trigger { - private final int mTouchpadGestureType; + AidlInputGestureData.TouchpadGestureTrigger mAidlTouchpadTrigger; + + private TouchpadTrigger( + @NonNull AidlInputGestureData.TouchpadGestureTrigger aidlTouchpadTrigger) { + mAidlTouchpadTrigger = aidlTouchpadTrigger; + } private TouchpadTrigger(int touchpadGestureType) { if (touchpadGestureType != TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP) { throw new IllegalArgumentException( "Invalid touchpadGestureType = " + touchpadGestureType); } - mTouchpadGestureType = touchpadGestureType; + mAidlTouchpadTrigger = new AidlInputGestureData.TouchpadGestureTrigger(); + mAidlTouchpadTrigger.gestureType = touchpadGestureType; } public int getTouchpadGestureType() { - return mTouchpadGestureType; + return mAidlTouchpadTrigger.gestureType; + } + + public AidlInputGestureData.Trigger getAidlTrigger() { + AidlInputGestureData.Trigger trigger = new AidlInputGestureData.Trigger(); + trigger.setTouchpadGesture(mAidlTouchpadTrigger); + return trigger; } @Override public String toString() { return "TouchpadTrigger{" + - "mTouchpadGestureType=" + mTouchpadGestureType + + "mTouchpadGestureType=" + mAidlTouchpadTrigger.gestureType + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TouchpadTrigger that = (TouchpadTrigger) o; - return mTouchpadGestureType == that.mTouchpadGestureType; + if (!(o instanceof TouchpadTrigger that)) return false; + return Objects.equals(mAidlTouchpadTrigger, that.mAidlTouchpadTrigger); } @Override public int hashCode() { - return Objects.hashCode(mTouchpadGestureType); + return mAidlTouchpadTrigger.hashCode(); } } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 10224c1be788..cf41e138047a 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1480,6 +1480,30 @@ public final class InputManager { mGlobal.unregisterKeyGestureEventHandler(handler); } + /** + * Find an input gesture mapped to a particular trigger. + * + * @param trigger to find the input gesture for + * @return input gesture mapped to the provided trigger, {@code null} if none found + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + @UserHandleAware + @Nullable + public InputGestureData getInputGesture(@NonNull InputGestureData.Trigger trigger) { + try { + AidlInputGestureData result = mIm.getInputGesture(mContext.getUserId(), + trigger.getAidlTrigger()); + if (result == null) { + return null; + } + return new InputGestureData(result); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** Adds a new custom input gesture * * @param inputGestureData gesture data to add as custom gesture diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index cb1e0161441f..4025242fd208 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -43,6 +43,9 @@ public final class KeyGestureEvent { private static final int LOG_EVENT_UNSPECIFIED = FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED; + // Used as a placeholder to identify if a gesture is reserved for system + public static final int KEY_GESTURE_TYPE_SYSTEM_RESERVED = -1; + // These values should not change and values should not be re-used as this data is persisted to // long term storage and must be kept backwards compatible. public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0; @@ -144,6 +147,7 @@ public final class KeyGestureEvent { public static final int ACTION_GESTURE_COMPLETE = 2; @IntDef(prefix = "KEY_GESTURE_TYPE_", value = { + KEY_GESTURE_TYPE_SYSTEM_RESERVED, KEY_GESTURE_TYPE_UNSPECIFIED, KEY_GESTURE_TYPE_HOME, KEY_GESTURE_TYPE_RECENT_APPS, @@ -232,6 +236,26 @@ public final class KeyGestureEvent { public @interface KeyGestureType { } + /** + * Returns whether the key gesture type passed as argument is allowed for visible background + * users. + * + * @hide + */ + public static boolean isVisibleBackgrounduserAllowedGesture(int keyGestureType) { + switch (keyGestureType) { + case KEY_GESTURE_TYPE_SLEEP: + case KEY_GESTURE_TYPE_WAKEUP: + case KEY_GESTURE_TYPE_LAUNCH_ASSISTANT: + case KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT: + case KEY_GESTURE_TYPE_VOLUME_MUTE: + case KEY_GESTURE_TYPE_RECENT_APPS: + case KEY_GESTURE_TYPE_APP_SWITCH: + return false; + } + return true; + } + public KeyGestureEvent(@NonNull AidlKeyGestureEvent keyGestureEvent) { this.mKeyGestureEvent = keyGestureEvent; } @@ -645,6 +669,8 @@ public final class KeyGestureEvent { private static String keyGestureTypeToString(@KeyGestureType int value) { switch (value) { + case KEY_GESTURE_TYPE_SYSTEM_RESERVED: + return "KEY_GESTURE_TYPE_SYSTEM_RESERVED"; case KEY_GESTURE_TYPE_UNSPECIFIED: return "KEY_GESTURE_TYPE_UNSPECIFIED"; case KEY_GESTURE_TYPE_HOME: diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java index 4b79bc482c7b..32aac2efb3c1 100644 --- a/core/java/android/hardware/input/VirtualStylus.java +++ b/core/java/android/hardware/input/VirtualStylus.java @@ -16,11 +16,9 @@ package android.hardware.input; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; import android.companion.virtual.IVirtualDevice; -import android.companion.virtual.flags.Flags; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; @@ -34,7 +32,6 @@ import android.util.Log; * * @hide */ -@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) @SystemApi public class VirtualStylus extends VirtualInputDevice { /** @hide */ diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.java b/core/java/android/hardware/input/VirtualStylusButtonEvent.java index 8fcf561bedcd..9fe725a627b4 100644 --- a/core/java/android/hardware/input/VirtualStylusButtonEvent.java +++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.java @@ -16,11 +16,9 @@ package android.hardware.input; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; @@ -35,7 +33,6 @@ import java.lang.annotation.RetentionPolicy; * * @hide */ -@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) @SystemApi public final class VirtualStylusButtonEvent implements Parcelable { /** @hide */ @@ -128,7 +125,6 @@ public final class VirtualStylusButtonEvent implements Parcelable { /** * Builder for {@link VirtualStylusButtonEvent}. */ - @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) public static final class Builder { @Action diff --git a/core/java/android/hardware/input/VirtualStylusConfig.java b/core/java/android/hardware/input/VirtualStylusConfig.java index 64cf1f56d8bc..3c56023fa6d3 100644 --- a/core/java/android/hardware/input/VirtualStylusConfig.java +++ b/core/java/android/hardware/input/VirtualStylusConfig.java @@ -16,11 +16,9 @@ package android.hardware.input; -import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -29,7 +27,6 @@ import android.os.Parcelable; * * @hide */ -@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) @SystemApi public final class VirtualStylusConfig extends VirtualTouchDeviceConfig implements Parcelable { @@ -68,7 +65,6 @@ public final class VirtualStylusConfig extends VirtualTouchDeviceConfig implemen /** * Builder for creating a {@link VirtualStylusConfig}. */ - @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> { /** diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.java b/core/java/android/hardware/input/VirtualStylusMotionEvent.java index 0ac6f3aa3e15..fa0ff4f7eeab 100644 --- a/core/java/android/hardware/input/VirtualStylusMotionEvent.java +++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.java @@ -16,12 +16,10 @@ package android.hardware.input; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; -import android.companion.virtual.flags.Flags; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; @@ -38,7 +36,6 @@ import java.lang.annotation.RetentionPolicy; * * @hide */ -@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) @SystemApi public final class VirtualStylusMotionEvent implements Parcelable { private static final int TILT_MIN = -90; @@ -209,7 +206,6 @@ public final class VirtualStylusMotionEvent implements Parcelable { /** * Builder for {@link VirtualStylusMotionEvent}. */ - @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) public static final class Builder { @ToolType diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 7887c15a72ff..62126963cba4 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -178,6 +178,13 @@ flag { } flag { + name: "enable_display_color_inversion_key_gestures" + namespace: "input" + description: "Adds key gestures for display color inversion for accessibility needs" + bug: "383730505" +} + +flag { name: "enable_talkback_and_magnifier_key_gestures" namespace: "input" description: "Adds key gestures for talkback and magnifier" diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index d9969d8b9596..3026609ed5cb 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -36,7 +36,6 @@ import android.util.Printer; import android.util.SparseArray; import android.util.proto.ProtoOutputStream; -import com.android.internal.annotations.GuardedBy; import com.android.internal.ravenwood.RavenwoodEnvironment; import dalvik.annotation.optimization.NeverCompile; @@ -1392,12 +1391,12 @@ public final class MessageQueue { } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { + if (peek) { + return msg; + } if (now >= msg.when) { // Got a message. mBlocked = false; - if (peek) { - return msg; - } if (prevMsg != null) { prevMsg.next = msg.next; if (prevMsg.next == null) { diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 2bc6ab5a18e9..d5640221e6bd 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -2783,4 +2783,12 @@ public final class Debug */ public static native boolean logAllocatorStats(); + /** + * Return the amount of memory (in kB) allocated by kernel drivers through CMA. + * @return a non-negative value or -1 on error. + * + * @hide + */ + public static native long getKernelCmaUsageKb(); + } diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 12080ca511b2..45a7afa014a1 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -706,11 +706,11 @@ public class GraphicsEnvironment { * @param context */ public void showAngleInUseDialogBox(Context context) { - if (!shouldShowAngleInUseDialogBox(context)) { + if (!mShouldUseAngle) { return; } - if (!mShouldUseAngle) { + if (!shouldShowAngleInUseDialogBox(context)) { return; } diff --git a/core/java/android/os/IPowerStatsService.aidl b/core/java/android/os/IPowerStatsService.aidl index a0c226205460..e0e9497cfb5c 100644 --- a/core/java/android/os/IPowerStatsService.aidl +++ b/core/java/android/os/IPowerStatsService.aidl @@ -25,6 +25,8 @@ interface IPowerStatsService { const String KEY_ENERGY = "energy"; /** @hide */ const String KEY_TIMESTAMPS = "timestamps"; + /** @hide */ + const String KEY_GRANULARITY = "granularity"; /** @hide */ const int RESULT_SUCCESS = 0; diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java index c0333e914b4d..d12d99a71251 100644 --- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java +++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java @@ -16,7 +16,6 @@ package android.os; -import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -754,12 +753,12 @@ public final class MessageQueue { } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { + if (peek) { + return msg; + } if (now >= msg.when) { // Got a message. mBlocked = false; - if (peek) { - return msg; - } if (prevMsg != null) { prevMsg.next = msg.next; if (prevMsg.next == null) { diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index 012590510714..2fe4871e08dd 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -70,6 +70,13 @@ public final class Looper { private static final String TAG = "Looper"; + private static class NoImagePreloadHolder { + // Enable/Disable verbose logging with a system prop. e.g. + // adb shell 'setprop log.looper.slow.verbose false && stop && start' + private static final boolean sVerboseLogging = + SystemProperties.getBoolean("log.looper.slow.verbose", false); + } + // sThreadLocal.get() will return null unless you've called prepare(). @UnsupportedAppUsage static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); @@ -246,17 +253,21 @@ public final class Looper { } } if (logSlowDelivery) { + boolean slow = false; + + if (!me.mSlowDeliveryDetected || NoImagePreloadHolder.sVerboseLogging) { + slow = showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, + "delivery", msg); + } if (me.mSlowDeliveryDetected) { - if ((dispatchStart - msg.when) <= 10) { + if (!slow && (dispatchStart - msg.when) <= 10) { Slog.w(TAG, "Drained"); me.mSlowDeliveryDetected = false; } - } else { - if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery", - msg)) { - // Once we write a slow delivery log, suppress until the queue drains. - me.mSlowDeliveryDetected = true; - } + } else if (slow) { + // A slow delivery is detected, suppressing further logs unless verbose logging + // is enabled. + me.mSlowDeliveryDetected = true; } } if (logSlowDispatch) { @@ -322,6 +333,23 @@ public final class Looper { @android.ravenwood.annotation.RavenwoodReplace private static int getThresholdOverride() { + // Allow overriding the threshold for all processes' main looper with a system prop. + // e.g. adb shell 'setprop log.looper.any.main.slow 1 && stop && start' + if (myLooper() == getMainLooper()) { + final int globalOverride = SystemProperties.getInt("log.looper.any.main.slow", -1); + if (globalOverride >= 0) { + return globalOverride; + } + } + + // Allow overriding the threshold for all threads within a process with a system prop. + // e.g. adb shell 'setprop log.looper.1000.any.slow 1 && stop && start' + final int processOverride = SystemProperties.getInt("log.looper." + + Process.myUid() + ".any.slow", -1); + if (processOverride >= 0) { + return processOverride; + } + return SystemProperties.getInt("log.looper." + Process.myUid() + "." + Thread.currentThread().getName() diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 0879118ff856..4aa74621bd62 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -593,11 +593,11 @@ public final class Parcel { */ public final void recycle() { if (mRecycled) { - Log.wtf(TAG, "Recycle called on unowned Parcel. (recycle twice?) Here: " + String error = "Recycle called on unowned Parcel. (recycle twice?) Here: " + Log.getStackTraceString(new Throwable()) - + " Original recycle call (if DEBUG_RECYCLE): ", mStack); - - return; + + " Original recycle call (if DEBUG_RECYCLE): "; + Log.wtf(TAG, error, mStack); + throw new IllegalStateException(error, mStack); } mRecycled = true; diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 1801df048b3e..2a5666cbe83c 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -615,6 +615,7 @@ public final class PowerManager { WAKE_REASON_WAKE_KEY, WAKE_REASON_WAKE_MOTION, WAKE_REASON_HDMI, + WAKE_REASON_LID, WAKE_REASON_DISPLAY_GROUP_ADDED, WAKE_REASON_DISPLAY_GROUP_TURNED_ON, WAKE_REASON_UNFOLD_DEVICE, diff --git a/core/java/android/os/PowerMonitorReadings.java b/core/java/android/os/PowerMonitorReadings.java index a0ab066ffb75..85ffc4661df3 100644 --- a/core/java/android/os/PowerMonitorReadings.java +++ b/core/java/android/os/PowerMonitorReadings.java @@ -18,8 +18,12 @@ package android.os; import android.annotation.ElapsedRealtimeLong; import android.annotation.FlaggedApi; +import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SystemApi; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Comparator; @@ -38,6 +42,37 @@ public final class PowerMonitorReadings { @NonNull private final long[] mTimestampsMs; + /** + * PowerMonitorReadings have the default level of granularity, which may be coarse or fine + * as determined by the implementation. + * @hide + */ + @FlaggedApi(android.permission.flags.Flags.FLAG_FINE_POWER_MONITOR_PERMISSION) + @SystemApi + public static final int GRANULARITY_UNSPECIFIED = 0; + + /** + * PowerMonitorReadings have a high level of granularity. This level of granularity is + * provided to applications that have the + * {@link android.Manifest.permission#ACCESS_FINE_POWER_MONITORS} permission. + * + * @hide + */ + @FlaggedApi(android.permission.flags.Flags.FLAG_FINE_POWER_MONITOR_PERMISSION) + @SystemApi + public static final int GRANULARITY_FINE = 1; + + /** @hide */ + @IntDef(prefix = {"GRANULARITY_"}, value = { + GRANULARITY_UNSPECIFIED, + GRANULARITY_FINE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PowerMonitorGranularity {} + + @PowerMonitorGranularity + private final int mGranularity; + private static final Comparator<PowerMonitor> POWER_MONITOR_COMPARATOR = Comparator.comparingInt(pm -> pm.index); @@ -46,10 +81,12 @@ public final class PowerMonitorReadings { * @hide */ public PowerMonitorReadings(@NonNull PowerMonitor[] powerMonitors, - @NonNull long[] energyUws, @NonNull long[] timestampsMs) { + @NonNull long[] energyUws, @NonNull long[] timestampsMs, + @PowerMonitorGranularity int granularity) { mPowerMonitors = powerMonitors; mEnergyUws = energyUws; mTimestampsMs = timestampsMs; + mGranularity = granularity; } /** @@ -79,6 +116,19 @@ public final class PowerMonitorReadings { return 0; } + /** + * Returns the granularity level of the results, which refers to the maximum age of the + * power monitor readings, {@link #GRANULARITY_FINE} indicating the highest level + * of freshness supported by the service implementation. + * @hide + */ + @FlaggedApi(android.permission.flags.Flags.FLAG_FINE_POWER_MONITOR_PERMISSION) + @SystemApi + @PowerMonitorGranularity + public int getGranularity() { + return mGranularity; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java index 9085fe09bdaa..a58fea891851 100644 --- a/core/java/android/os/ServiceManager.java +++ b/core/java/android/os/ServiceManager.java @@ -278,7 +278,7 @@ public final class ServiceManager { return service; } else { return Binder.allowBlocking( - getIServiceManager().checkService(name).getServiceWithMetadata().service); + getIServiceManager().checkService2(name).getServiceWithMetadata().service); } } catch (RemoteException e) { Log.e(TAG, "error in checkService", e); diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java index 7ea521ec5dd4..a5aa1b3efcd2 100644 --- a/core/java/android/os/ServiceManagerNative.java +++ b/core/java/android/os/ServiceManagerNative.java @@ -62,16 +62,23 @@ class ServiceManagerProxy implements IServiceManager { @UnsupportedAppUsage public IBinder getService(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return checkService(name).getServiceWithMetadata().service; + return checkService2(name).getServiceWithMetadata().service; } public Service getService2(String name) throws RemoteException { // Same as checkService (old versions of servicemanager had both methods). - return checkService(name); + return checkService2(name); } - public Service checkService(String name) throws RemoteException { - return mServiceManager.checkService(name); + // TODO(b/355394904): This function has been deprecated, please use checkService2 instead. + @UnsupportedAppUsage + public IBinder checkService(String name) throws RemoteException { + // Same as checkService (old versions of servicemanager had both methods). + return checkService2(name).getServiceWithMetadata().service; + } + + public Service checkService2(String name) throws RemoteException { + return mServiceManager.checkService2(name); } public void addService(String name, IBinder service, boolean allowIsolated, int dumpPriority) diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index e24f08b7dfe5..8b8369890d1b 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -110,6 +110,14 @@ flag { } flag { + name: "allow_thermal_hal_skin_forecast" + is_exported: true + namespace: "game" + description: "Enable thermal HAL skin temperature forecast to be used by headroom API" + bug: "383211885" +} + +flag { name: "allow_thermal_headroom_thresholds" is_exported: true namespace: "game" diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java index a1e9cf25e3e1..9d0e221bd9e7 100644 --- a/core/java/android/os/health/SystemHealthManager.java +++ b/core/java/android/os/health/SystemHealthManager.java @@ -344,11 +344,7 @@ public class SystemHealthManager { || !mHintManagerClientData.supportInfo.headroom.isCpuSupported) { throw new UnsupportedOperationException(); } - try { - return mHintManager.getCpuHeadroomMinIntervalMillis(); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } + return mHintManagerClientData.supportInfo.headroom.cpuMinIntervalMillis; } /** @@ -366,11 +362,7 @@ public class SystemHealthManager { || !mHintManagerClientData.supportInfo.headroom.isGpuSupported) { throw new UnsupportedOperationException(); } - try { - return mHintManager.getGpuHeadroomMinIntervalMillis(); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } + return mHintManagerClientData.supportInfo.headroom.gpuMinIntervalMillis; } /** @@ -588,7 +580,8 @@ public class SystemHealthManager { if (resultCode == IPowerStatsService.RESULT_SUCCESS) { PowerMonitorReadings result = new PowerMonitorReadings(powerMonitorsArray, resultData.getLongArray(IPowerStatsService.KEY_ENERGY), - resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS)); + resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS), + resultData.getInt(IPowerStatsService.KEY_GRANULARITY)); if (executor != null) { executor.execute(() -> onResult.onResult(result)); } else { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ec58eff92410..0cfec2cc7314 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1254,21 +1254,6 @@ public final class Settings { "android.settings.TEMPERATURE_UNIT_SETTINGS"; /** - * Activity Action: Show numbering system configuration settings. - * <p> - * Input: Nothing. - * <p> - * Output: After calling {@link android.app.Activity#startActivityForResult}, the callback - * {@code onActivityResult} will have resultCode {@link android.app.Activity#RESULT_OK} if - * the numbering system settings page is suitable to show on the UI. Otherwise, the result is - * set to {@link android.app.Activity#RESULT_CANCELED}. - */ - @FlaggedApi(Flags.FLAG_SYSTEM_REGIONAL_PREFERENCES_API_ENABLED) - @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) - public static final String ACTION_NUMBERING_SYSTEM_SETTINGS = - "android.settings.NUMBERING_SYSTEM_SETTINGS"; - - /** * Activity Action: Show measurement system configuration settings. * <p> * Input: Nothing. @@ -10525,6 +10510,15 @@ public final class Settings { public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep"; /** + * If screensavers are enabled, whether the screensaver should be + * automatically launched when the device is stationary and upright. + * @hide + */ + @Readable + public static final String SCREENSAVER_ACTIVATE_ON_POSTURED = + "screensaver_activate_on_postured"; + + /** * If screensavers are enabled, the default screensaver component. * @hide */ @@ -13328,6 +13322,16 @@ public final class Settings { public static final String AUTO_TIME_ZONE_EXPLICIT = "auto_time_zone_explicit"; /** + * Value to specify if the device should send notifications when {@link #AUTO_TIME_ZONE} is + * on and the device's time zone changes. + * + * <p>1=yes, 0=no. + * + * @hide + */ + public static final String TIME_ZONE_NOTIFICATIONS = "time_zone_notifications"; + + /** * URI for the car dock "in" event sound. * @hide */ diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index ebb6fb451699..4a9e945e62a9 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -42,6 +42,16 @@ flag { } flag { + name: "secure_array_zeroization" + namespace: "platform_security" + description: "Enable secure array zeroization" + bug: "320392352" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "deprecate_fsv_sig" namespace: "hardware_backed_security" description: "Feature flag for deprecating .fsv_sig" diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index dfc11dcb5427..d3a230d1335d 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -77,3 +77,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "allow_dream_when_postured" + namespace: "systemui" + description: "Allow dreaming when device is stationary and upright" + bug: "383208131" +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index d53b98c65f9a..3c53506990d1 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,7 +16,6 @@ package android.text; -import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; @@ -670,15 +669,11 @@ public abstract class Layout { // High-contrast text mode // Determine if the text is black-on-white or white-on-black, so we know what blendmode will // give the highest contrast and most realistic text color. - // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h - if (highContrastTextLuminance()) { - var lab = new double[3]; - ColorUtils.colorToLAB(color, lab); - return lab[0] < 50.0; - } else { - int channelSum = Color.red(color) + Color.green(color) + Color.blue(color); - return channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + var lab = new double[3]; + ColorUtils.colorToLAB(color, lab); + return lab[0] < 50.0; + // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken) } private boolean isJustificationRequired(int lineNum) { diff --git a/core/java/android/view/DragEvent.java b/core/java/android/view/DragEvent.java index b65e3ebc3871..77af312eac4a 100644 --- a/core/java/android/view/DragEvent.java +++ b/core/java/android/view/DragEvent.java @@ -157,6 +157,11 @@ public class DragEvent implements Parcelable { private float mOffsetY; /** + * The id of the display where the `mX` and `mY` of this event belongs to. + */ + private int mDisplayId; + + /** * The View#DRAG_FLAG_* flags used to start the current drag, only provided if the target window * has the {@link WindowManager.LayoutParams#PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP} flag * and is only sent with {@link #ACTION_DRAG_STARTED} and {@link #ACTION_DROP}. @@ -297,14 +302,15 @@ public class DragEvent implements Parcelable { private DragEvent() { } - private void init(int action, float x, float y, float offsetX, float offsetY, int flags, - ClipDescription description, ClipData data, SurfaceControl dragSurface, + private void init(int action, float x, float y, float offsetX, float offsetY, int displayId, + int flags, ClipDescription description, ClipData data, SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions, Object localState, boolean result) { mAction = action; mX = x; mY = y; mOffsetX = offsetX; mOffsetY = offsetY; + mDisplayId = displayId; mFlags = flags; mClipDescription = description; mClipData = data; @@ -315,20 +321,20 @@ public class DragEvent implements Parcelable { } static DragEvent obtain() { - return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, null, null, null, null, null, false); + return DragEvent.obtain(0, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false); } /** @hide */ public static DragEvent obtain(int action, float x, float y, float offsetX, float offsetY, - int flags, Object localState, ClipDescription description, ClipData data, + int displayId, int flags, Object localState, ClipDescription description, ClipData data, SurfaceControl dragSurface, IDragAndDropPermissions dragAndDropPermissions, boolean result) { final DragEvent ev; synchronized (gRecyclerLock) { if (gRecyclerTop == null) { ev = new DragEvent(); - ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface, - dragAndDropPermissions, localState, result); + ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, + dragSurface, dragAndDropPermissions, localState, result); return ev; } ev = gRecyclerTop; @@ -339,7 +345,7 @@ public class DragEvent implements Parcelable { ev.mRecycled = false; ev.mNext = null; - ev.init(action, x, y, offsetX, offsetY, flags, description, data, dragSurface, + ev.init(action, x, y, offsetX, offsetY, displayId, flags, description, data, dragSurface, dragAndDropPermissions, localState, result); return ev; @@ -349,8 +355,9 @@ public class DragEvent implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static DragEvent obtain(DragEvent source) { return obtain(source.mAction, source.mX, source.mY, source.mOffsetX, source.mOffsetY, - source.mFlags, source.mLocalState, source.mClipDescription, source.mClipData, - source.mDragSurface, source.mDragAndDropPermissions, source.mDragResult); + source.mDisplayId, source.mFlags, source.mLocalState, source.mClipDescription, + source.mClipData, source.mDragSurface, source.mDragAndDropPermissions, + source.mDragResult); } /** @@ -398,6 +405,11 @@ public class DragEvent implements Parcelable { return mOffsetY; } + /** @hide */ + public int getDisplayId() { + return mDisplayId; + } + /** * Returns the {@link android.content.ClipData} object sent to the system as part of the call * to diff --git a/core/java/android/view/InputEventReceiver.java b/core/java/android/view/InputEventReceiver.java index 2cc05b0bc4b0..1c36eaf99afa 100644 --- a/core/java/android/view/InputEventReceiver.java +++ b/core/java/android/view/InputEventReceiver.java @@ -177,7 +177,7 @@ public abstract class InputEventReceiver { * drag * if true, the window associated with this input channel has just lost drag */ - public void onDragEvent(boolean isExiting, float x, float y) { + public void onDragEvent(boolean isExiting, float x, float y, int displayId) { } /** diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 4fead2ad5246..6decd6d3a603 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -112,6 +112,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro private Insets mPendingInsets; private float mPendingFraction; private boolean mFinished; + private boolean mCancelling; private boolean mCancelled; private boolean mShownOnFinish; private float mCurrentAlpha = 1.0f; @@ -371,7 +372,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro mPendingInsets = mLayoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN ? mShownInsets : mHiddenInsets; mPendingAlpha = 1f; - mPendingFraction = 1f; + mCancelling = true; applyChangeInsets(null); mCancelled = true; mListener.onCancelled(mReadyDispatched ? this : null); @@ -488,15 +489,15 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro return; } - final boolean visible = mPendingFraction == 0 - // The first frame of ANIMATION_TYPE_SHOW should be invisible since it is - // animated from the hidden state. - ? mAnimationType != ANIMATION_TYPE_SHOW - : mPendingFraction < 1f || (mFinished - ? mShownOnFinish - // If the animation is cancelled, mFinished and mShownOnFinish are not set. + final boolean visible = mFinished + ? mShownOnFinish + : (mCancelling + // If the animation is being cancelled, mShownOnFinish is not valid. // Here uses mLayoutInsetsDuringAnimation to decide if it should be visible. - : mLayoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN); + ? mLayoutInsetsDuringAnimation == LAYOUT_INSETS_DURING_ANIMATION_SHOWN + // The first frame of ANIMATION_TYPE_SHOW should be invisible since it is + // animated from the hidden state. + : (mAnimationType != ANIMATION_TYPE_SHOW || mPendingFraction != 0)); // TODO: Implement behavior when inset spans over multiple types for (int i = controls.size() - 1; i >= 0; i--) { diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java index b21e85aeeb6a..da3a817f0341 100644 --- a/core/java/android/view/PointerIcon.java +++ b/core/java/android/view/PointerIcon.java @@ -514,10 +514,14 @@ public final class PointerIcon implements Parcelable { final TypedArray a = resources.obtainAttributes( parser, com.android.internal.R.styleable.PointerIcon); bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0); - hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0) - * pointerScale; - hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0) - * pointerScale; + // Cast the hotspot dimensions to int before scaling to match the scaling logic of + // the bitmap, whose intrinsic size is also an int before it is scaled. + final int unscaledHotSpotX = + (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0); + final int unscaledHotSpotY = + (int) a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0); + hotSpotX = unscaledHotSpotX * pointerScale; + hotSpotY = unscaledHotSpotY * pointerScale; a.recycle(); } catch (Exception ex) { throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex); diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index e665c08c63e4..d7cf3e827695 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -4865,7 +4865,7 @@ public final class SurfaceControl implements Parcelable { /** * @hide */ - public Transaction setDesintationFrame(SurfaceControl sc, @NonNull Rect destinationFrame) { + public Transaction setDestinationFrame(SurfaceControl sc, @NonNull Rect destinationFrame) { checkPreconditions(sc); nativeSetDestinationFrame(mNativeObject, sc.mNativeObject, destinationFrame.left, destinationFrame.top, destinationFrame.right, @@ -4876,7 +4876,7 @@ public final class SurfaceControl implements Parcelable { /** * @hide */ - public Transaction setDesintationFrame(SurfaceControl sc, int width, int height) { + public Transaction setDestinationFrame(SurfaceControl sc, int width, int height) { checkPreconditions(sc); nativeSetDestinationFrame(mNativeObject, sc.mNativeObject, 0, 0, width, height); return this; diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index b0051cefb21b..780e76122e8a 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -1125,7 +1125,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } } - surfaceUpdateTransaction.setDesintationFrame(mBlastSurfaceControl, mSurfaceWidth, + surfaceUpdateTransaction.setDestinationFrame(mBlastSurfaceControl, mSurfaceWidth, mSurfaceHeight); if (isHardwareAccelerated()) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 8ef0b0eebb8c..36671b901a6b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -10561,13 +10561,13 @@ public final class ViewRootImpl implements ViewParent, } @Override - public void onDragEvent(boolean isExiting, float x, float y) { + public void onDragEvent(boolean isExiting, float x, float y, int displayId) { // force DRAG_EXITED_EVENT if appropriate DragEvent event = DragEvent.obtain( - isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, - x, y, 0 /* offsetX */, 0 /* offsetY */, 0 /* flags */, null/* localState */, - null/* description */, null /* data */, null /* dragSurface */, - null /* dragAndDropPermissions */, false /* result */); + isExiting ? DragEvent.ACTION_DRAG_EXITED : DragEvent.ACTION_DRAG_LOCATION, x, y, + 0 /* offsetX */, 0 /* offsetY */, displayId, 0 /* flags */, + null/* localState */, null/* description */, null /* data */, + null /* dragSurface */, null /* dragAndDropPermissions */, false /* result */); dispatchDragEvent(event); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 544f42b9acfa..64277b14098d 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -157,6 +157,9 @@ public final class AccessibilityManager { /** @hide */ public static final int AUTOCLICK_CURSOR_AREA_SIZE_MAX = 100; + /** @hide */ + public static final int AUTOCLICK_CURSOR_AREA_INCREMENT_SIZE = 20; + /** * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 71a832d84f08..99fe0cbdca25 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -18,7 +18,6 @@ package android.widget; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.graphics.Paint.NEW_FONT_VARIATION_MANAGEMENT; import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT; import static android.view.ContentInfo.SOURCE_AUTOFILL; import static android.view.ContentInfo.SOURCE_CLIPBOARD; @@ -5544,13 +5543,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; } - final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() - && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); boolean effective; - if (useFontVariationStore) { + if (Flags.typefaceRedesignReadonly()) { if (mFontWeightAdjustment != 0 && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) { - mTextPaint.setFontVariationSettings(fontVariationSettings, mFontWeightAdjustment); + List<FontVariationAxis> axes = FontVariationAxis.fromFontVariationSettingsForList( + fontVariationSettings); + if (axes == null) { + return false; // invalid format of the font variation settings. + } + boolean wghtAdjusted = false; + for (int i = 0; i < axes.size(); ++i) { + FontVariationAxis axis = axes.get(i); + if (axis.getOpenTypeTagValue() == 0x77676874 /* wght */) { + axes.set(i, new FontVariationAxis("wght", + Math.clamp(axis.getStyleValue() + mFontWeightAdjustment, + FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); + wghtAdjusted = true; + } + } + if (!wghtAdjusted) { + axes.add(new FontVariationAxis("wght", + Math.clamp(400 + mFontWeightAdjustment, + FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX))); + } + mTextPaint.setFontVariationSettings( + FontVariationAxis.toFontVariationSettings(axes)); } else { mTextPaint.setFontVariationSettings(fontVariationSettings); } diff --git a/core/java/android/window/DisplayAreaOrganizer.java b/core/java/android/window/DisplayAreaOrganizer.java index 84ce247264f6..bd711fc79083 100644 --- a/core/java/android/window/DisplayAreaOrganizer.java +++ b/core/java/android/window/DisplayAreaOrganizer.java @@ -121,6 +121,14 @@ public class DisplayAreaOrganizer extends WindowOrganizer { public static final int FEATURE_WINDOWING_LAYER = FEATURE_SYSTEM_FIRST + 9; /** + * Display area for rendering app zoom out. When there are multiple layers on the screen, + * we want to render these layers based on a depth model. Here we zoom out the layer behind, + * whether it's an app or the homescreen. + * @hide + */ + public static final int FEATURE_APP_ZOOM_OUT = FEATURE_SYSTEM_FIRST + 10; + + /** * The last boundary of display area for system features */ public static final int FEATURE_SYSTEM_LAST = 10_000; diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index 20e3f6b93bd0..2911b0a6643d 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -464,7 +464,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { * Returns false if the legacy back behavior should be used. */ public boolean isOnBackInvokedCallbackEnabled() { - return isOnBackInvokedCallbackEnabled(mChecker.getContext()); + final Context hostContext = mChecker.getContext(); + if (hostContext == null) { + Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!"); + return false; + } + return isOnBackInvokedCallbackEnabled(hostContext); } /** @@ -695,7 +700,12 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { */ public boolean checkApplicationCallbackRegistration(int priority, OnBackInvokedCallback callback) { - if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(getContext()) + final Context hostContext = getContext(); + if (hostContext == null) { + Log.w(TAG, "OnBackInvokedCallback is disabled, host context is removed!"); + return false; + } + if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(hostContext) && !(callback instanceof CompatOnBackInvokedCallback)) { Log.w(TAG, "OnBackInvokedCallback is not enabled for the application." @@ -720,7 +730,7 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { return true; } - private Context getContext() { + @Nullable private Context getContext() { return mContext.get(); } } diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index de3e0d3faf43..7a1078f8718f 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -415,6 +415,17 @@ flag { } flag { + name: "keep_app_window_hide_while_locked" + namespace: "windowing_frontend" + description: "Do not let app window visible while device is locked" + is_fixed_read_only: true + bug: "378088391" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "port_window_size_animation" namespace: "windowing_frontend" description: "Port window-resize animation from legacy to shell" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index b38feeef290b..f178b0ed2043 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -167,3 +167,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "use_self_sync_transaction_for_layer" + description: "Always use this.getSyncTransaction for assignLayer" + bug: "388127825" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index 5635943cb76e..972c2ea403e0 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -49,6 +49,7 @@ public class SystemNotificationChannels { public static final String NETWORK_ALERTS = "NETWORK_ALERTS"; public static final String NETWORK_AVAILABLE = "NETWORK_AVAILABLE"; public static final String VPN = "VPN"; + public static final String TIME = "TIME"; /** * @deprecated Legacy device admin channel with low importance which is no longer used, * Use the high importance {@link #DEVICE_ADMIN} channel instead. @@ -146,6 +147,12 @@ public class SystemNotificationChannels { NotificationManager.IMPORTANCE_LOW); channelsList.add(vpn); + final NotificationChannel time = new NotificationChannel( + TIME, + context.getString(R.string.notification_channel_system_time), + NotificationManager.IMPORTANCE_DEFAULT); + channelsList.add(time); + final NotificationChannel deviceAdmin = new NotificationChannel( DEVICE_ADMIN, getDeviceAdminNotificationChannelName(context), diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index c9c4be1e2c93..dc440e36ca0d 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -19,6 +19,7 @@ package com.android.internal.os; import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH; import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START; import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE; +import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; import android.annotation.NonNull; import android.annotation.Nullable; @@ -215,6 +216,7 @@ public class BatteryStatsHistory { private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>(); private byte mLastHistoryStepLevel = 0; private boolean mMutable = true; + private int mIteratorCookie; private final BatteryStatsHistory mWritableHistory; private static class BatteryHistoryFile implements Comparable<BatteryHistoryFile> { @@ -289,6 +291,7 @@ public class BatteryStatsHistory { } void load() { + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); mDirectory.mkdirs(); if (!mDirectory.exists()) { Slog.wtf(TAG, "HistoryDir does not exist:" + mDirectory.getPath()); @@ -325,8 +328,11 @@ public class BatteryStatsHistory { } } finally { unlock(); + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); } }); + } else { + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.load", 0); } } @@ -418,6 +424,7 @@ public class BatteryStatsHistory { } void writeToParcel(Parcel out, boolean useBlobs) { + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.writeToParcel"); lock(); try { final long start = SystemClock.uptimeMillis(); @@ -443,6 +450,7 @@ public class BatteryStatsHistory { } } finally { unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } @@ -482,34 +490,39 @@ public class BatteryStatsHistory { } private void cleanup() { - if (mDirectory == null) { - return; - } - - if (!tryLock()) { - mCleanupNeeded = true; - return; - } - - mCleanupNeeded = false; + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.cleanup"); try { - // if free disk space is less than 100MB, delete oldest history file. - if (!hasFreeDiskSpace(mDirectory)) { - BatteryHistoryFile oldest = mHistoryFiles.remove(0); - oldest.atomicFile.delete(); + if (mDirectory == null) { + return; + } + + if (!tryLock()) { + mCleanupNeeded = true; + return; } - // if there is more history stored than allowed, delete oldest history files. - int size = getSize(); - while (size > mMaxHistorySize) { - BatteryHistoryFile oldest = mHistoryFiles.get(0); - int length = (int) oldest.atomicFile.getBaseFile().length(); - oldest.atomicFile.delete(); - mHistoryFiles.remove(0); - size -= length; + mCleanupNeeded = false; + try { + // if free disk space is less than 100MB, delete oldest history file. + if (!hasFreeDiskSpace(mDirectory)) { + BatteryHistoryFile oldest = mHistoryFiles.remove(0); + oldest.atomicFile.delete(); + } + + // if there is more history stored than allowed, delete oldest history files. + int size = getSize(); + while (size > mMaxHistorySize) { + BatteryHistoryFile oldest = mHistoryFiles.get(0); + int length = (int) oldest.atomicFile.getBaseFile().length(); + oldest.atomicFile.delete(); + mHistoryFiles.remove(0); + size -= length; + } + } finally { + unlock(); } } finally { - unlock(); + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } } @@ -710,13 +723,18 @@ public class BatteryStatsHistory { * in the system directory, so it is not safe while actively writing history. */ public BatteryStatsHistory copy() { - synchronized (this) { - // Make a copy of battery history to avoid concurrent modification. - Parcel historyBufferCopy = Parcel.obtain(); - historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.copy"); + try { + synchronized (this) { + // Make a copy of battery history to avoid concurrent modification. + Parcel historyBufferCopy = Parcel.obtain(); + historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); - return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null, - null, mEventLogger, this); + return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, + null, null, mEventLogger, this); + } + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } } @@ -826,7 +844,7 @@ public class BatteryStatsHistory { */ @NonNull public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) { - if (mMutable) { + if (mMutable || mIteratorCookie != 0) { return copy().iterate(startTimeMs, endTimeMs); } @@ -837,7 +855,12 @@ public class BatteryStatsHistory { mCurrentParcel = null; mCurrentParcelEnd = 0; mParcelIndex = 0; - return new BatteryStatsHistoryIterator(this, startTimeMs, endTimeMs); + BatteryStatsHistoryIterator iterator = new BatteryStatsHistoryIterator( + this, startTimeMs, endTimeMs); + mIteratorCookie = System.identityHashCode(iterator); + Trace.asyncTraceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", + mIteratorCookie); + return iterator; } /** @@ -848,6 +871,9 @@ public class BatteryStatsHistory { if (mHistoryDir != null) { mHistoryDir.unlock(); } + Trace.asyncTraceEnd(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.iterate", + mIteratorCookie); + mIteratorCookie = 0; } /** @@ -949,28 +975,33 @@ public class BatteryStatsHistory { * @return true if success, false otherwise. */ public boolean readFileToParcel(Parcel out, AtomicFile file) { - byte[] raw = null; + Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, "BatteryStatsHistory.read"); try { - final long start = SystemClock.uptimeMillis(); - raw = file.readFully(); - if (DEBUG) { - Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() - + " duration ms:" + (SystemClock.uptimeMillis() - start)); + byte[] raw = null; + try { + final long start = SystemClock.uptimeMillis(); + raw = file.readFully(); + if (DEBUG) { + Slog.d(TAG, "readFileToParcel:" + file.getBaseFile().getPath() + + " duration ms:" + (SystemClock.uptimeMillis() - start)); + } + } catch (Exception e) { + Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); + return false; } - } catch (Exception e) { - Slog.e(TAG, "Error reading file " + file.getBaseFile().getPath(), e); - return false; - } - out.unmarshall(raw, 0, raw.length); - out.setDataPosition(0); - if (!verifyVersion(out)) { - return false; + out.unmarshall(raw, 0, raw.length); + out.setDataPosition(0); + if (!verifyVersion(out)) { + return false; + } + // skip monotonic time field. + out.readLong(); + // skip monotonic size field + out.readLong(); + return true; + } finally { + Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); } - // skip monotonic time field. - out.readLong(); - // skip monotonic size field - out.readLong(); - return true; } /** diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index 7f7ea8b28546..37500766a4ac 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -36,7 +36,6 @@ import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier; import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import com.android.internal.org.bouncycastle.operator.OperatorCreationException; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -53,12 +52,6 @@ import java.security.cert.X509Certificate; public abstract class VerityUtils { private static final String TAG = "VerityUtils"; - /** - * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of - * foo.apk. - */ - public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig"; - /** SHA256 hash size. */ private static final int HASH_SIZE_BYTES = 32; @@ -67,16 +60,6 @@ public abstract class VerityUtils { || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; } - /** Returns true if the given file looks like containing an fs-verity signature. */ - public static boolean isFsveritySignatureFile(File file) { - return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION); - } - - /** Returns the fs-verity signature file path of the given file. */ - public static String getFsveritySignatureFilePath(String filePath) { - return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION; - } - /** Enables fs-verity for the file without signature. */ public static void setUpFsverity(@NonNull String filePath) throws IOException { int errno = enableFsverityNative(filePath); diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index f14e1f63cdf6..ec0954d5590a 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -239,4 +239,7 @@ interface IStatusBarService /** Unbundle a categorized notification */ void unbundleNotification(String key); + + /** Rebundle an (un)categorized notification */ + void rebundleNotification(String key); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 39ddea614ee4..74707703f5f2 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -65,6 +65,7 @@ import android.util.SparseLongArray; import android.view.InputDevice; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.google.android.collect.Lists; @@ -75,6 +76,7 @@ import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -292,6 +294,56 @@ public class LockPatternUtils { } + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static byte[] newNonMovableByteArray(int length) { + if (!android.security.Flags.secureArrayZeroization()) { + return new byte[length]; + } + return ArrayUtils.newNonMovableByteArray(length); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static char[] newNonMovableCharArray(int length) { + if (!android.security.Flags.secureArrayZeroization()) { + return new char[length]; + } + return ArrayUtils.newNonMovableCharArray(length); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static void zeroize(byte[] array) { + if (!android.security.Flags.secureArrayZeroization()) { + if (array != null) { + Arrays.fill(array, (byte) 0); + } + return; + } + ArrayUtils.zeroize(array); + } + + /** + * This exists temporarily due to trunk-stable policies. + * Please use ArrayUtils directly if you can. + */ + public static void zeroize(char[] array) { + if (!android.security.Flags.secureArrayZeroization()) { + if (array != null) { + Arrays.fill(array, (char) 0); + } + return; + } + ArrayUtils.zeroize(array); + } + @UnsupportedAppUsage public DevicePolicyManager getDevicePolicyManager() { if (mDevicePolicyManager == null) { diff --git a/core/java/com/android/internal/widget/LockscreenCredential.java b/core/java/com/android/internal/widget/LockscreenCredential.java index 54b9a225f944..92ce990c67df 100644 --- a/core/java/com/android/internal/widget/LockscreenCredential.java +++ b/core/java/com/android/internal/widget/LockscreenCredential.java @@ -246,7 +246,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { */ public void zeroize() { if (mCredential != null) { - Arrays.fill(mCredential, (byte) 0); + LockPatternUtils.zeroize(mCredential); mCredential = null; } } @@ -346,7 +346,7 @@ public class LockscreenCredential implements Parcelable, AutoCloseable { byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword); byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword); - Arrays.fill(saltedPassword, (byte) 0); + LockPatternUtils.zeroize(saltedPassword); return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5)); } catch (NoSuchAlgorithmException e) { throw new AssertionError("Missing digest algorithm: ", e); diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java index 80bc4fd89c8d..dd12f69a56fb 100644 --- a/core/java/com/android/internal/widget/NotificationExpandButton.java +++ b/core/java/com/android/internal/widget/NotificationExpandButton.java @@ -56,8 +56,6 @@ public class NotificationExpandButton extends FrameLayout { private int mDefaultTextColor; private int mHighlightPillColor; private int mHighlightTextColor; - // Track whether this ever had mExpanded = true, so that we don't highlight it anymore. - private boolean mWasExpanded = false; public NotificationExpandButton(Context context) { this(context, null, 0, 0); @@ -136,7 +134,6 @@ public class NotificationExpandButton extends FrameLayout { int contentDescriptionId; if (mExpanded) { if (notificationsRedesignTemplates()) { - mWasExpanded = true; drawableId = R.drawable.ic_notification_2025_collapse; } else { drawableId = R.drawable.ic_collapse_notification; @@ -156,8 +153,6 @@ public class NotificationExpandButton extends FrameLayout { if (!notificationsRedesignTemplates()) { // changing the expanded state can affect the number display updateNumber(); - } else { - updateColors(); } } @@ -197,43 +192,22 @@ public class NotificationExpandButton extends FrameLayout { ); } - /** - * Use highlight colors for the expander for groups (when the number is showing) that haven't - * been opened before, as long as the colors are available. - */ - private boolean shouldBeHighlighted() { - return !mWasExpanded && shouldShowNumber() - && mHighlightPillColor != 0 && mHighlightTextColor != 0; - } - private void updateColors() { - if (notificationsRedesignTemplates()) { - if (shouldBeHighlighted()) { + if (shouldShowNumber()) { + if (mHighlightPillColor != 0) { mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor)); - mIconView.setColorFilter(mHighlightTextColor); + } + mIconView.setColorFilter(mHighlightTextColor); + if (mHighlightTextColor != 0) { mNumberView.setTextColor(mHighlightTextColor); - } else { - mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); - mIconView.setColorFilter(mDefaultTextColor); - mNumberView.setTextColor(mDefaultTextColor); } } else { - if (shouldShowNumber()) { - if (mHighlightPillColor != 0) { - mPillDrawable.setTintList(ColorStateList.valueOf(mHighlightPillColor)); - } - mIconView.setColorFilter(mHighlightTextColor); - if (mHighlightTextColor != 0) { - mNumberView.setTextColor(mHighlightTextColor); - } - } else { - if (mDefaultPillColor != 0) { - mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); - } - mIconView.setColorFilter(mDefaultTextColor); - if (mDefaultTextColor != 0) { - mNumberView.setTextColor(mDefaultTextColor); - } + if (mDefaultPillColor != 0) { + mPillDrawable.setTintList(ColorStateList.valueOf(mDefaultPillColor)); + } + mIconView.setColorFilter(mDefaultTextColor); + if (mDefaultTextColor != 0) { + mNumberView.setTextColor(mDefaultTextColor); } } } diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 8cd7843fe1d9..1b770207f2cb 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -31,6 +32,7 @@ import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Log; +import android.util.Pair; import android.view.RemotableViewMethod; import android.widget.ProgressBar; import android.widget.RemoteViews; @@ -40,14 +42,15 @@ import androidx.annotation.ColorInt; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -56,18 +59,26 @@ import java.util.TreeSet; * represent Notification ProgressStyle progress, such as for ridesharing and navigation. */ @RemoteViews.RemoteView -public final class NotificationProgressBar extends ProgressBar { +public final class NotificationProgressBar extends ProgressBar implements + NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; + private static final boolean DEBUG = false; private NotificationProgressDrawable mNotificationProgressDrawable; + private final Rect mProgressDrawableBounds = new Rect(); private NotificationProgressModel mProgressModel; @Nullable - private List<Part> mProgressDrawableParts = null; + private List<Part> mParts = null; + + // List of drawable parts before segment splitting by process. + @Nullable + private List<DrawablePart> mProgressDrawableParts = null; @Nullable private Drawable mTracker = null; + private boolean mHasTrackerIcon = false; /** @see R.styleable#NotificationProgressBar_trackerHeight */ private final int mTrackerHeight; @@ -76,7 +87,13 @@ public final class NotificationProgressBar extends ProgressBar { private final Matrix mMatrix = new Matrix(); private Matrix mTrackerDrawMatrix = null; - private float mScale = 0; + private float mProgressFraction = 0; + /** + * The location of progress on the stretched and rescaled progress bar, in fraction. Used for + * calculating the tracker position. If stretching and rescaling is not needed, == + * mProgressFraction. + */ + private float mAdjustedProgressFraction = 0; /** Indicates whether mTrackerPos needs to be recalculated before the tracker is drawn. */ private boolean mTrackerPosIsDirty = false; @@ -96,20 +113,21 @@ public final class NotificationProgressBar extends ProgressBar { int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.NotificationProgressBar, defStyleAttr, defStyleRes); saveAttributeDataForStyleable(context, R.styleable.NotificationProgressBar, attrs, a, defStyleAttr, defStyleRes); try { mNotificationProgressDrawable = getNotificationProgressDrawable(); + mNotificationProgressDrawable.setBoundsChangeListener(this); } catch (IllegalStateException ex) { Log.e(TAG, "Can't get NotificationProgressDrawable", ex); } // Supports setting the tracker in xml, but ProgressStyle notifications set/override it - // via {@code setProgressTrackerIcon}. + // via {@code #setProgressTrackerIcon}. final Drawable tracker = a.getDrawable(R.styleable.NotificationProgressBar_tracker); setTracker(tracker); @@ -126,8 +144,7 @@ public final class NotificationProgressBar extends ProgressBar { */ @RemotableViewMethod public void setProgressModel(@Nullable Bundle bundle) { - Preconditions.checkArgument(bundle != null, - "Bundle shouldn't be null"); + Preconditions.checkArgument(bundle != null, "Bundle shouldn't be null"); mProgressModel = NotificationProgressModel.fromBundle(bundle); final boolean isIndeterminate = mProgressModel.isIndeterminate(); @@ -137,20 +154,25 @@ public final class NotificationProgressBar extends ProgressBar { final int indeterminateColor = mProgressModel.getIndeterminateColor(); setIndeterminateTintList(ColorStateList.valueOf(indeterminateColor)); } else { + // TODO: b/372908709 - maybe don't rerun the entire calculation every time the + // progress model is updated? For example, if the segments and parts aren't changed, + // there is no need to call `processAndConvertToViewParts` again. + final int progress = mProgressModel.getProgress(); final int progressMax = mProgressModel.getProgressMax(); - mProgressDrawableParts = processAndConvertToDrawableParts(mProgressModel.getSegments(), + + mParts = processAndConvertToViewParts(mProgressModel.getSegments(), mProgressModel.getPoints(), progress, - progressMax, - mProgressModel.isStyledByProgress()); - - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setParts(mProgressDrawableParts); - } + progressMax); setMax(progressMax); setProgress(progress); + + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0) { + updateDrawableParts(); + } } } @@ -200,9 +222,7 @@ public final class NotificationProgressBar extends ProgressBar { } else { progressTrackerDrawable = null; } - return () -> { - setTracker(progressTrackerDrawable); - }; + return () -> setTracker(progressTrackerDrawable); } private void setTracker(@Nullable Drawable tracker) { @@ -226,8 +246,14 @@ public final class NotificationProgressBar extends ProgressBar { final boolean trackerSizeChanged = trackerSizeChanged(tracker, mTracker); mTracker = tracker; - if (mNotificationProgressDrawable != null) { - mNotificationProgressDrawable.setHasTrackerIcon(mTracker != null); + final boolean hasTrackerIcon = (mTracker != null); + if (mHasTrackerIcon != hasTrackerIcon) { + mHasTrackerIcon = hasTrackerIcon; + if (mNotificationProgressDrawable != null + && mNotificationProgressDrawable.getBounds().width() != 0 + && mProgressModel.isStyledByProgress()) { + updateDrawableParts(); + } } configureTrackerBounds(); @@ -293,6 +319,8 @@ public final class NotificationProgressBar extends ProgressBar { mTrackerDrawMatrix.postTranslate(Math.round(dx), Math.round(dy)); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setProgress(int progress) { super.setProgress(progress); @@ -300,6 +328,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public void setProgress(int progress, boolean animate) { // Animation isn't supported by NotificationProgressBar. @@ -308,6 +338,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMin(int min) { super.setMin(min); @@ -315,6 +347,8 @@ public final class NotificationProgressBar extends ProgressBar { onMaybeVisualProgressChanged(); } + // This updates the visual position of the progress indicator, i.e., the tracker. It doesn't + // update the NotificationProgressDrawable, which is updated by {@code #setProgressModel}. @Override public synchronized void setMax(int max) { super.setMax(max); @@ -323,10 +357,10 @@ public final class NotificationProgressBar extends ProgressBar { } private void onMaybeVisualProgressChanged() { - float scale = getScale(); - if (mScale == scale) return; + float progressFraction = getProgressFraction(); + if (mProgressFraction == progressFraction) return; - mScale = scale; + mProgressFraction = progressFraction; mTrackerPosIsDirty = true; invalidate(); } @@ -350,8 +384,7 @@ public final class NotificationProgressBar extends ProgressBar { super.drawableStateChanged(); final Drawable tracker = mTracker; - if (tracker != null && tracker.isStateful() - && tracker.setState(getDrawableState())) { + if (tracker != null && tracker.isStateful() && tracker.setState(getDrawableState())) { invalidateDrawable(tracker); } } @@ -372,6 +405,65 @@ public final class NotificationProgressBar extends ProgressBar { updateTrackerAndBarPos(w, h); } + @Override + public void onDrawableBoundsChanged() { + final Rect progressDrawableBounds = mNotificationProgressDrawable.getBounds(); + + if (mProgressDrawableBounds.equals(progressDrawableBounds)) return; + + if (mProgressDrawableBounds.width() != progressDrawableBounds.width()) { + updateDrawableParts(); + } + + mProgressDrawableBounds.set(progressDrawableBounds); + } + + private void updateDrawableParts() { + if (DEBUG) { + Log.d(TAG, "updateDrawableParts() called. mNotificationProgressDrawable = " + + mNotificationProgressDrawable + ", mParts = " + mParts); + } + + if (mNotificationProgressDrawable == null) return; + if (mParts == null) return; + + final float width = mNotificationProgressDrawable.getBounds().width(); + if (width == 0) { + if (mProgressDrawableParts != null) { + if (DEBUG) { + Log.d(TAG, "Clearing mProgressDrawableParts"); + } + mProgressDrawableParts.clear(); + mNotificationProgressDrawable.setParts(mProgressDrawableParts); + } + return; + } + + mProgressDrawableParts = processAndConvertToDrawableParts( + mParts, + width, + mNotificationProgressDrawable.getSegSegGap(), + mNotificationProgressDrawable.getSegPointGap(), + mNotificationProgressDrawable.getPointRadius(), + mHasTrackerIcon + ); + Pair<List<DrawablePart>, Float> p = maybeStretchAndRescaleSegments( + mParts, + mProgressDrawableParts, + mNotificationProgressDrawable.getSegmentMinWidth(), + mNotificationProgressDrawable.getPointRadius(), + getProgressFraction(), + width, + mProgressModel.isStyledByProgress(), + mHasTrackerIcon ? 0F : mNotificationProgressDrawable.getSegSegGap()); + + if (DEBUG) { + Log.d(TAG, "Updating NotificationProgressDrawable parts"); + } + mNotificationProgressDrawable.setParts(p.first); + mAdjustedProgressFraction = p.second / width; + } + private void updateTrackerAndBarPos(int w, int h) { final int paddedHeight = h - mPaddingTop - mPaddingBottom; final Drawable bar = getCurrentDrawable(); @@ -402,11 +494,11 @@ public final class NotificationProgressBar extends ProgressBar { } if (tracker != null) { - setTrackerPos(w, tracker, mScale, trackerOffsetY); + setTrackerPos(w, tracker, mAdjustedProgressFraction, trackerOffsetY); } } - private float getScale() { + private float getProgressFraction() { int min = getMin(); int max = getMax(); int range = max - min; @@ -416,19 +508,19 @@ public final class NotificationProgressBar extends ProgressBar { /** * Updates the tracker drawable bounds. * - * @param w Width of the view, including padding - * @param tracker Drawable used for the tracker - * @param scale Current progress between 0 and 1 - * @param offsetY Vertical offset for centering. If set to - * {@link Integer#MIN_VALUE}, the current offset will be used. + * @param w Width of the view, including padding + * @param tracker Drawable used for the tracker + * @param progressFraction Current progress between 0 and 1 + * @param offsetY Vertical offset for centering. If set to + * {@link Integer#MIN_VALUE}, the current offset will be used. */ - private void setTrackerPos(int w, Drawable tracker, float scale, int offsetY) { + private void setTrackerPos(int w, Drawable tracker, float progressFraction, int offsetY) { int available = w - mPaddingLeft - mPaddingRight; final int trackerWidth = tracker.getIntrinsicWidth(); final int trackerHeight = tracker.getIntrinsicHeight(); available -= ((mTrackerHeight <= 0) ? trackerWidth : mTrackerWidth); - final int trackerPos = (int) (scale * available + 0.5f); + final int trackerPos = (int) (progressFraction * available + 0.5f); final int top, bottom; if (offsetY == Integer.MIN_VALUE) { @@ -448,8 +540,8 @@ public final class NotificationProgressBar extends ProgressBar { if (background != null) { final int bkgOffsetX = mPaddingLeft; final int bkgOffsetY = mPaddingTop; - background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, - right + bkgOffsetX, bottom + bkgOffsetY); + background.setHotspotBounds(left + bkgOffsetX, top + bkgOffsetY, right + bkgOffsetX, + bottom + bkgOffsetY); } // Canvas will be translated, so 0,0 is where we start drawing @@ -482,7 +574,7 @@ public final class NotificationProgressBar extends ProgressBar { if (mTracker == null) return; if (mTrackerPosIsDirty) { - setTrackerPos(getWidth(), mTracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), mTracker, mAdjustedProgressFraction, Integer.MIN_VALUE); } final int saveCount = canvas.save(); @@ -531,7 +623,7 @@ public final class NotificationProgressBar extends ProgressBar { final Drawable tracker = mTracker; if (tracker != null) { - setTrackerPos(getWidth(), tracker, mScale, Integer.MIN_VALUE); + setTrackerPos(getWidth(), tracker, mAdjustedProgressFraction, Integer.MIN_VALUE); // Since we draw translated, the drawable's bounds that it signals // for invalidation won't be the actual bounds we want invalidated, @@ -541,16 +633,14 @@ public final class NotificationProgressBar extends ProgressBar { } /** - * Processes the ProgressStyle data and convert to list of {@code - * NotificationProgressDrawable.Part}. + * Processes the ProgressStyle data and convert to a list of {@code Part}. */ @VisibleForTesting - public static List<Part> processAndConvertToDrawableParts( + public static List<Part> processAndConvertToViewParts( List<ProgressStyle.Segment> segments, List<ProgressStyle.Point> points, int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { if (segments.isEmpty()) { throw new IllegalArgumentException("List of segments shouldn't be empty"); @@ -571,6 +661,7 @@ public final class NotificationProgressBar extends ProgressBar { if (progress < 0 || progress > progressMax) { throw new IllegalArgumentException("Invalid progress : " + progress); } + for (ProgressStyle.Point point : points) { final int pos = point.getPosition(); if (pos < 0 || pos > progressMax) { @@ -583,23 +674,21 @@ public final class NotificationProgressBar extends ProgressBar { final Map<Integer, ProgressStyle.Point> positionToPointMap = generatePositionToPointMap( points); final SortedSet<Integer> sortedPos = generateSortedPositionSet(startToSegmentMap, - positionToPointMap, progress, isStyledByProgress); + positionToPointMap); - final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = - splitSegmentsByPointsAndProgress( - startToSegmentMap, sortedPos, progressMax); + final Map<Integer, ProgressStyle.Segment> startToSplitSegmentMap = splitSegmentsByPoints( + startToSegmentMap, sortedPos, progressMax); - return convertToDrawableParts(startToSplitSegmentMap, positionToPointMap, sortedPos, - progress, progressMax, - isStyledByProgress); + return convertToViewParts(startToSplitSegmentMap, positionToPointMap, sortedPos, + progressMax); } // Any segment with a point on it gets split by the point. - // If isStyledByProgress is true, also split the segment with the progress value in its range. - private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPointsAndProgress( + private static Map<Integer, ProgressStyle.Segment> splitSegmentsByPoints( Map<Integer, ProgressStyle.Segment> startToSegmentMap, SortedSet<Integer> sortedPos, - int progressMax) { + int progressMax + ) { int prevSegStart = 0; for (Integer pos : sortedPos) { if (pos == 0 || pos == progressMax) continue; @@ -610,8 +699,7 @@ public final class NotificationProgressBar extends ProgressBar { final ProgressStyle.Segment prevSeg = startToSegmentMap.get(prevSegStart); final ProgressStyle.Segment leftSeg = new ProgressStyle.Segment( - pos - prevSegStart).setColor( - prevSeg.getColor()); + pos - prevSegStart).setColor(prevSeg.getColor()); final ProgressStyle.Segment rightSeg = new ProgressStyle.Segment( prevSegStart + prevSeg.getLength() - pos).setColor(prevSeg.getColor()); @@ -624,32 +712,21 @@ public final class NotificationProgressBar extends ProgressBar { return startToSegmentMap; } - private static List<Part> convertToDrawableParts( + private static List<Part> convertToViewParts( Map<Integer, ProgressStyle.Segment> startToSegmentMap, Map<Integer, ProgressStyle.Point> positionToPointMap, SortedSet<Integer> sortedPos, - int progress, - int progressMax, - boolean isStyledByProgress + int progressMax ) { List<Part> parts = new ArrayList<>(); - boolean styleRemainingParts = false; for (Integer pos : sortedPos) { if (positionToPointMap.containsKey(pos)) { final ProgressStyle.Point point = positionToPointMap.get(pos); - final int color = maybeGetFadedColor(point.getColor(), styleRemainingParts); - parts.add(new Point(null, color, styleRemainingParts)); - } - // We want the Point at the current progress to be filled (not faded), but a Segment - // starting at this progress to be faded. - if (isStyledByProgress && !styleRemainingParts && pos == progress) { - styleRemainingParts = true; + parts.add(new Point(point.getColor())); } if (startToSegmentMap.containsKey(pos)) { final ProgressStyle.Segment seg = startToSegmentMap.get(pos); - final int color = maybeGetFadedColor(seg.getColor(), styleRemainingParts); - parts.add(new Segment( - (float) seg.getLength() / progressMax, color, styleRemainingParts)); + parts.add(new Segment((float) seg.getLength() / progressMax, seg.getColor())); } } @@ -660,11 +737,24 @@ public final class NotificationProgressBar extends ProgressBar { private static int maybeGetFadedColor(@ColorInt int color, boolean fade) { if (!fade) return color; - return NotificationProgressDrawable.getFadedColor(color); + return getFadedColor(color); + } + + /** + * Get a color with an opacity that's 40% of the input color. + */ + @ColorInt + static int getFadedColor(@ColorInt int color) { + return Color.argb( + (int) (Color.alpha(color) * 0.4f + 0.5f), + Color.red(color), + Color.green(color), + Color.blue(color)); } private static Map<Integer, ProgressStyle.Segment> generateStartToSegmentMap( - List<ProgressStyle.Segment> segments) { + List<ProgressStyle.Segment> segments + ) { final Map<Integer, ProgressStyle.Segment> startToSegmentMap = new HashMap<>(); int currentStart = 0; // Initial start position is 0 @@ -681,7 +771,8 @@ public final class NotificationProgressBar extends ProgressBar { } private static Map<Integer, ProgressStyle.Point> generatePositionToPointMap( - List<ProgressStyle.Point> points) { + List<ProgressStyle.Point> points + ) { final Map<Integer, ProgressStyle.Point> positionToPointMap = new HashMap<>(); for (ProgressStyle.Point point : points) { @@ -693,14 +784,395 @@ public final class NotificationProgressBar extends ProgressBar { private static SortedSet<Integer> generateSortedPositionSet( Map<Integer, ProgressStyle.Segment> startToSegmentMap, - Map<Integer, ProgressStyle.Point> positionToPointMap, int progress, - boolean isStyledByProgress) { + Map<Integer, ProgressStyle.Point> positionToPointMap + ) { final SortedSet<Integer> sortedPos = new TreeSet<>(startToSegmentMap.keySet()); sortedPos.addAll(positionToPointMap.keySet()); - if (isStyledByProgress) { - sortedPos.add(progress); - } return sortedPos; } + + /** + * Processes the list of {@code Part} and convert to a list of {@code DrawablePart}. + */ + @VisibleForTesting + public static List<DrawablePart> processAndConvertToDrawableParts( + List<Part> parts, + float totalWidth, + float segSegGap, + float segPointGap, + float pointRadius, + boolean hasTrackerIcon + ) { + List<DrawablePart> drawableParts = new ArrayList<>(); + + // generally, we will start drawing at (x, y) and end at (x+w, y) + float x = (float) 0; + + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + final Part prevPart = iPart == 0 ? null : parts.get(iPart - 1); + final Part nextPart = iPart + 1 == nParts ? null : parts.get(iPart + 1); + if (part instanceof Segment segment) { + final float segWidth = segment.mFraction * totalWidth; + // Advance the start position to account for a point immediately prior. + final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); + final float start = x + startOffset; + // Retract the end position to account for the padding and a point immediately + // after. + final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, + segSegGap, x + segWidth, totalWidth, hasTrackerIcon); + final float end = x + segWidth - endOffset; + + drawableParts.add(new DrawableSegment(start, end, segment.mColor, segment.mFaded)); + + segment.mStart = x; + segment.mEnd = x + segWidth; + + // Advance the current position to account for the segment's fraction of the total + // width (ignoring offset and padding) + x += segWidth; + } else if (part instanceof Point point) { + final float pointWidth = 2 * pointRadius; + float start = x - pointRadius; + float end = x + pointRadius; + // Only shift the points right at the start/end. + // For the points close to the start/end, the segment minimum width requirement + // would take care of shifting them to be within the bounds. + if (x == 0) { + start = 0; + end = pointWidth; + } else if (x == totalWidth) { + start = totalWidth - pointWidth; + end = totalWidth; + } + + drawableParts.add(new DrawablePoint(start, end, point.mColor)); + } + } + + return drawableParts; + } + + private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, + float startX) { + if (!(prevPart instanceof Point)) return 0F; + final float pointOffset = (startX == 0) ? pointRadius : 0; + return pointOffset + pointRadius + segPointGap; + } + + private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, + float segPointGap, float segSegGap, float endX, float totalWidth, + boolean hasTrackerIcon) { + if (nextPart == null) return 0F; + if (nextPart instanceof Segment nextSeg) { + if (!seg.mFaded && nextSeg.mFaded) { + // @see Segment#mFaded + return hasTrackerIcon ? 0F : segSegGap; + } + return segSegGap; + } + + final float pointOffset = (endX == totalWidth) ? pointRadius : 0; + return segPointGap + pointRadius + pointOffset; + } + + /** + * Processes the list of {@code DrawablePart} data and convert to a pair of: + * - list of processed {@code DrawablePart}. + * - location of progress on the stretched and rescaled progress bar. + */ + @VisibleForTesting + public static Pair<List<DrawablePart>, Float> maybeStretchAndRescaleSegments( + List<Part> parts, + List<DrawablePart> drawableParts, + float segmentMinWidth, + float pointRadius, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + final List<DrawableSegment> drawableSegments = drawableParts + .stream() + .filter(DrawableSegment.class::isInstance) + .map(DrawableSegment.class::cast) + .toList(); + float totalExcessWidth = 0; + float totalPositiveExcessWidth = 0; + for (DrawableSegment drawableSegment : drawableSegments) { + final float excessWidth = drawableSegment.getWidth() - segmentMinWidth; + totalExcessWidth += excessWidth; + if (excessWidth > 0) totalPositiveExcessWidth += excessWidth; + } + + // All drawable segments are above minimum width. No need to stretch and rescale. + if (totalExcessWidth == totalPositiveExcessWidth) { + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + if (totalExcessWidth < 0) { + // TODO: b/372908709 - throw an error so that the caller can catch and go to fallback + // option. (instead of return.) + Log.w(TAG, "Not enough width to satisfy the minimum width for segments."); + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + final int nParts = drawableParts.size(); + float startOffset = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawableSegment drawableSegment) { + final float origDrawableSegmentWidth = drawableSegment.getWidth(); + + float drawableSegmentWidth = segmentMinWidth; + // Allocate the totalExcessWidth to the segments above minimum, proportionally to + // their initial excessWidth. + if (origDrawableSegmentWidth > segmentMinWidth) { + drawableSegmentWidth += + totalExcessWidth * (origDrawableSegmentWidth - segmentMinWidth) + / totalPositiveExcessWidth; + } + + final float widthDiff = drawableSegmentWidth - drawableSegment.getWidth(); + + // Adjust drawable segments to new widths + drawableSegment.setStart(drawableSegment.getStart() + startOffset); + drawableSegment.setEnd( + drawableSegment.getStart() + origDrawableSegmentWidth + widthDiff); + + // Also adjust view segments to new width. (For view segments, only start is + // needed?) + // Check that segments and drawableSegments are of the same size? + final Segment segment = (Segment) parts.get(iPart); + final float origSegmentWidth = segment.getWidth(); + segment.mStart = segment.mStart + startOffset; + segment.mEnd = segment.mStart + origSegmentWidth + widthDiff; + + // Increase startOffset for the subsequent segments. + startOffset += widthDiff; + } else if (drawablePart instanceof DrawablePoint drawablePoint) { + drawablePoint.setStart(drawablePoint.getStart() + startOffset); + drawablePoint.setEnd(drawablePoint.getStart() + 2 * pointRadius); + } + } + + return maybeSplitDrawableSegmentsByProgress( + parts, + drawableParts, + progressFraction, + totalWidth, + isStyledByProgress, + progressGap); + } + + /** + * Find the location of progress on the stretched and rescaled progress bar. + * If isStyledByProgress is true, also split the drawable segment with the progress value in its + * range. Style the drawable parts after process with reduced opacity and segment height. + */ + private static Pair<List<DrawablePart>, Float> maybeSplitDrawableSegmentsByProgress( + // Needed to get the original segment start and end positions in pixels. + List<Part> parts, + List<DrawablePart> drawableParts, + float progressFraction, + float totalWidth, + boolean isStyledByProgress, + float progressGap + ) { + if (progressFraction == 1) return new Pair<>(drawableParts, totalWidth); + + int iPartFirstSegmentToStyle = -1; + int iPartSegmentToSplit = -1; + float rescaledProgressX = 0; + float startFraction = 0; + final int nParts = parts.size(); + for (int iPart = 0; iPart < nParts; iPart++) { + final Part part = parts.get(iPart); + if (!(part instanceof Segment)) continue; + final Segment segment = (Segment) part; + if (startFraction == progressFraction) { + iPartFirstSegmentToStyle = iPart; + rescaledProgressX = segment.mStart; + break; + } else if (startFraction < progressFraction + && progressFraction < startFraction + segment.mFraction) { + iPartSegmentToSplit = iPart; + rescaledProgressX = segment.mStart + + (progressFraction - startFraction) / segment.mFraction + * segment.getWidth(); + break; + } + startFraction += segment.mFraction; + } + + if (!isStyledByProgress) return new Pair<>(drawableParts, rescaledProgressX); + + List<DrawablePart> splitDrawableParts = new ArrayList<>(); + boolean styleRemainingParts = false; + for (int iPart = 0; iPart < nParts; iPart++) { + final DrawablePart drawablePart = drawableParts.get(iPart); + if (drawablePart instanceof DrawablePoint drawablePoint) { + final int color = maybeGetFadedColor(drawablePoint.getColor(), styleRemainingParts); + splitDrawableParts.add( + new DrawablePoint(drawablePoint.getStart(), drawablePoint.getEnd(), color)); + } + if (iPart == iPartFirstSegmentToStyle) styleRemainingParts = true; + if (drawablePart instanceof DrawableSegment drawableSegment) { + if (iPart == iPartSegmentToSplit) { + if (rescaledProgressX <= drawableSegment.getStart()) { + styleRemainingParts = true; + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, true)); + } else if (drawableSegment.getStart() < rescaledProgressX + && rescaledProgressX < drawableSegment.getEnd()) { + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + rescaledProgressX - progressGap, drawableSegment.getColor())); + final int color = maybeGetFadedColor(drawableSegment.getColor(), true); + splitDrawableParts.add( + new DrawableSegment(rescaledProgressX, drawableSegment.getEnd(), + color, true)); + styleRemainingParts = true; + } else { + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), drawableSegment.getColor())); + styleRemainingParts = true; + } + } else { + final int color = maybeGetFadedColor(drawableSegment.getColor(), + styleRemainingParts); + splitDrawableParts.add(new DrawableSegment(drawableSegment.getStart(), + drawableSegment.getEnd(), color, styleRemainingParts)); + } + } + } + + return new Pair<>(splitDrawableParts, rescaledProgressX); + } + + /** + * A part of the progress bar, which is either a {@link Segment} with non-zero length, or a + * {@link Point} with zero length. + */ + // TODO: b/372908709 - maybe this should be made private? Only test the final + // NotificationDrawable.Parts. + public interface Part { + } + + /** + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + */ + public static final class Segment implements Part { + private final float mFraction; + @ColorInt + private final int mColor; + /** + * Whether the segment is faded or not. + * <p> + * <pre> + * When mFaded is set to true, a combination of the following is done to the segment: + * 1. The drawing color is mColor with opacity updated to 40%. + * 2. The gap between faded and non-faded segments is: + * - the segment-segment gap, when there is no tracker icon + * - 0, when there is tracker icon + * </pre> + * </p> + */ + private final boolean mFaded; + + /** Start position (in pixels) */ + private float mStart; + /** End position (in pixels */ + private float mEnd; + + public Segment(float fraction, @ColorInt int color) { + this(fraction, color, false); + } + + public Segment(float fraction, @ColorInt int color, boolean faded) { + mFraction = fraction; + mColor = color; + mFaded = faded; + } + + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; + } + + @Override + public String toString() { + return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" + + this.mFaded + "), mStart = " + this.mStart + ", mEnd = " + this.mEnd; + } + + // Needed for unit tests + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Segment that = (Segment) other; + if (Float.compare(this.mFraction, that.mFraction) != 0) return false; + if (this.mColor != that.mColor) return false; + return this.mFaded == that.mFaded; + } + + @Override + public int hashCode() { + return Objects.hash(mFraction, mColor, mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class Point implements Part { + @ColorInt + private final int mColor; + + public Point(@ColorInt int color) { + mColor = color; + } + + @Override + public String toString() { + return "Point(color=" + this.mColor + ")"; + } + + // Needed for unit tests. + @Override + public boolean equals(@androidx.annotation.Nullable Object other) { + if (this == other) return true; + + if (other == null || getClass() != other.getClass()) return false; + + Point that = (Point) other; + + return this.mColor == that.mColor; + } + + @Override + public int hashCode() { + return Objects.hash(mColor); + } + } } diff --git a/core/java/com/android/internal/widget/NotificationProgressDrawable.java b/core/java/com/android/internal/widget/NotificationProgressDrawable.java index 8629a1c95202..4ece81c24edc 100644 --- a/core/java/com/android/internal/widget/NotificationProgressDrawable.java +++ b/core/java/com/android/internal/widget/NotificationProgressDrawable.java @@ -21,7 +21,6 @@ import android.content.res.Resources; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -49,22 +48,24 @@ import java.util.Objects; /** * This is used by NotificationProgressBar for displaying a custom background. It composes of - * segments, which have non-zero length, and points, which have zero length. + * segments, which have non-zero length varying drawing width, and points, which have zero length + * and fixed size for drawing. * - * @see Segment - * @see Point + * @see DrawableSegment + * @see DrawablePoint */ public final class NotificationProgressDrawable extends Drawable { private static final String TAG = "NotifProgressDrawable"; + @Nullable + private BoundsChangeListener mBoundsChangeListener = null; + private State mState; private boolean mMutated; - private final ArrayList<Part> mParts = new ArrayList<>(); - private boolean mHasTrackerIcon; + private final ArrayList<DrawablePart> mParts = new ArrayList<>(); private final RectF mSegRectF = new RectF(); - private final Rect mPointRect = new Rect(); private final RectF mPointRectF = new RectF(); private final Paint mFillPaint = new Paint(); @@ -80,33 +81,37 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * <p>Set the segment default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the stroke - * @see #mutate() + * Returns the gap between two segments. */ - public void setSegmentDefaultColor(@ColorInt int color) { - mState.setSegmentColor(color); + public float getSegSegGap() { + return mState.mSegSegGap; } /** - * <p>Set the point rect default color for the drawable.</p> - * <p>Note: changing this property will affect all instances of a drawable loaded from a - * resource. It is recommended to invoke {@link #mutate()} before changing this property.</p> - * - * @param color The color of the point rect - * @see #mutate() + * Returns the gap between a segment and a point. + */ + public float getSegPointGap() { + return mState.mSegPointGap; + } + + /** + * Returns the gap between a segment and a point. */ - public void setPointRectDefaultColor(@ColorInt int color) { - mState.setPointRectColor(color); + public float getSegmentMinWidth() { + return mState.mSegmentMinWidth; + } + + /** + * Returns the radius for the points. + */ + public float getPointRadius() { + return mState.mPointRadius; } /** * Set the segments and points that constitute the drawable. */ - public void setParts(List<Part> parts) { + public void setParts(List<DrawablePart> parts) { mParts.clear(); mParts.addAll(parts); @@ -116,51 +121,22 @@ public final class NotificationProgressDrawable extends Drawable { /** * Set the segments and points that constitute the drawable. */ - public void setParts(@NonNull Part... parts) { + public void setParts(@NonNull DrawablePart... parts) { setParts(Arrays.asList(parts)); } - /** - * Set whether a tracker is drawn on top of this NotificationProgressDrawable. - */ - public void setHasTrackerIcon(boolean hasTrackerIcon) { - if (mHasTrackerIcon != hasTrackerIcon) { - mHasTrackerIcon = hasTrackerIcon; - invalidateSelf(); - } - } - @Override public void draw(@NonNull Canvas canvas) { - final float pointRadius = - mState.mPointRadius; // how big the point icon will be, halved - - // generally, we will start drawing at (x, y) and end at (x+w, y) - float x = (float) getBounds().left; + final float pointRadius = mState.mPointRadius; + final float left = (float) getBounds().left; final float centerY = (float) getBounds().centerY(); - final float totalWidth = (float) getBounds().width(); - float segPointGap = mState.mSegPointGap; final int numParts = mParts.size(); for (int iPart = 0; iPart < numParts; iPart++) { - final Part part = mParts.get(iPart); - final Part prevPart = iPart == 0 ? null : mParts.get(iPart - 1); - final Part nextPart = iPart + 1 == numParts ? null : mParts.get(iPart + 1); - if (part instanceof Segment segment) { - final float segWidth = segment.mFraction * totalWidth; - // Advance the start position to account for a point immediately prior. - final float startOffset = getSegStartOffset(prevPart, pointRadius, segPointGap, x); - final float start = x + startOffset; - // Retract the end position to account for the padding and a point immediately - // after. - final float endOffset = getSegEndOffset(segment, nextPart, pointRadius, segPointGap, - mState.mSegSegGap, x + segWidth, totalWidth, mHasTrackerIcon); - final float end = x + segWidth - endOffset; - - // Advance the current position to account for the segment's fraction of the total - // width (ignoring offset and padding) - x += segWidth; - + final DrawablePart part = mParts.get(iPart); + final float start = left + part.mStart; + final float end = left + part.mEnd; + if (part instanceof DrawableSegment segment) { // No space left to draw the segment if (start > end) continue; @@ -168,67 +144,23 @@ public final class NotificationProgressDrawable extends Drawable { : mState.mSegmentHeight / 2F; final float cornerRadius = mState.mSegmentCornerRadius; - mFillPaint.setColor(segment.mColor != Color.TRANSPARENT ? segment.mColor - : (segment.mFaded ? mState.mFadedSegmentColor : mState.mSegmentColor)); + mFillPaint.setColor(segment.mColor); mSegRectF.set(start, centerY - radiusY, end, centerY + radiusY); canvas.drawRoundRect(mSegRectF, cornerRadius, cornerRadius, mFillPaint); - } else if (part instanceof Point point) { - final float pointWidth = 2 * pointRadius; - float start = x - pointRadius; - if (start < 0) start = 0; - float end = start + pointWidth; - if (end > totalWidth) { - end = totalWidth; - if (totalWidth > pointWidth) start = totalWidth - pointWidth; - } - mPointRect.set((int) start, (int) (centerY - pointRadius), (int) end, - (int) (centerY + pointRadius)); - - if (point.mIcon != null) { - point.mIcon.setBounds(mPointRect); - point.mIcon.draw(canvas); - } else { - // TODO: b/367804171 - actually use a vector asset for the default point - // rather than drawing it as a box? - mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); - final float inset = mState.mPointRectInset; - final float cornerRadius = mState.mPointRectCornerRadius; - mPointRectF.inset(inset, inset); - - mFillPaint.setColor(point.mColor != Color.TRANSPARENT ? point.mColor - : (point.mFaded ? mState.mFadedPointRectColor - : mState.mPointRectColor)); - - canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); - } - } - } - } + } else if (part instanceof DrawablePoint point) { + // TODO: b/367804171 - actually use a vector asset for the default point + // rather than drawing it as a box? + mPointRectF.set(start, centerY - pointRadius, end, centerY + pointRadius); + final float inset = mState.mPointRectInset; + final float cornerRadius = mState.mPointRectCornerRadius; + mPointRectF.inset(inset, inset); - private static float getSegStartOffset(Part prevPart, float pointRadius, float segPointGap, - float startX) { - if (!(prevPart instanceof Point)) return 0F; - final float pointOffset = (startX < pointRadius) ? (pointRadius - startX) : 0; - return pointOffset + pointRadius + segPointGap; - } + mFillPaint.setColor(point.mColor); - private static float getSegEndOffset(Segment seg, Part nextPart, float pointRadius, - float segPointGap, - float segSegGap, float endX, float totalWidth, boolean hasTrackerIcon) { - if (nextPart == null) return 0F; - if (nextPart instanceof Segment nextSeg) { - if (!seg.mFaded && nextSeg.mFaded) { - // @see Segment#mFaded - return hasTrackerIcon ? 0F : segSegGap; + canvas.drawRoundRect(mPointRectF, cornerRadius, cornerRadius, mFillPaint); } - return segSegGap; } - - final float pointWidth = 2 * pointRadius; - final float pointOffset = (endX + pointRadius > totalWidth && totalWidth > pointWidth) - ? (endX + pointRadius - totalWidth) : 0; - return segPointGap + pointRadius + pointOffset; } @Override @@ -260,6 +192,19 @@ public final class NotificationProgressDrawable extends Drawable { return PixelFormat.UNKNOWN; } + public void setBoundsChangeListener(BoundsChangeListener listener) { + mBoundsChangeListener = listener; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + if (mBoundsChangeListener != null) { + mBoundsChangeListener.onDrawableBoundsChanged(); + } + } + @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme) @@ -384,6 +329,8 @@ public final class NotificationProgressDrawable extends Drawable { // Extract the theme attributes, if any. state.mThemeAttrsSegments = a.extractThemeAttrs(); + state.mSegmentMinWidth = a.getDimension( + R.styleable.NotificationProgressDrawableSegments_minWidth, state.mSegmentMinWidth); state.mSegmentHeight = a.getDimension( R.styleable.NotificationProgressDrawableSegments_height, state.mSegmentHeight); state.mFadedSegmentHeight = a.getDimension( @@ -392,9 +339,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mSegmentCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawableSegments_cornerRadius, state.mSegmentCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawableSegments_color, - state.mSegmentColor); - setSegmentDefaultColor(color); } private void updatePointsFromTypedArray(TypedArray a) { @@ -413,9 +357,6 @@ public final class NotificationProgressDrawable extends Drawable { state.mPointRectCornerRadius = a.getDimension( R.styleable.NotificationProgressDrawablePoints_cornerRadius, state.mPointRectCornerRadius); - final int color = a.getColor(R.styleable.NotificationProgressDrawablePoints_color, - state.mPointRectColor); - setPointRectDefaultColor(color); } static int resolveDensity(@Nullable Resources r, int parentDensity) { @@ -464,63 +405,57 @@ public final class NotificationProgressDrawable extends Drawable { } /** - * A part of the progress bar, which is either a S{@link Segment} with non-zero length, or a - * {@link Point} with zero length. + * Listener to receive updates about drawable bounds changing */ - public interface Part { + public interface BoundsChangeListener { + /** Called when bounds have changed */ + void onDrawableBoundsChanged(); } /** - * A segment is a part of the progress bar with non-zero length. For example, it can - * represent a portion in a navigation journey with certain traffic condition. - * + * A part of the progress drawable, which is either a {@link DrawableSegment} with non-zero + * length and varying drawing width, or a {@link DrawablePoint} with zero length and fixed size + * for drawing. */ - public static final class Segment implements Part { - private final float mFraction; - @ColorInt private final int mColor; - /** Whether the segment is faded or not. - * <p> - * <pre> - * When mFaded is set to true, a combination of the following is done to the segment: - * 1. The drawing color is mColor with opacity updated to 40%. - * 2. The gap between faded and non-faded segments is: - * - the segment-segment gap, when there is no tracker icon - * - 0, when there is tracker icon - * </pre> - * </p> - */ - private final boolean mFaded; - - public Segment(float fraction) { - this(fraction, Color.TRANSPARENT); + public abstract static class DrawablePart { + // TODO: b/372908709 - maybe rename start/end to left/right, to be consistent with the + // bounds rect. + /** Start position for drawing (in pixels) */ + protected float mStart; + /** End position for drawing (in pixels) */ + protected float mEnd; + /** Drawing color. */ + @ColorInt protected final int mColor; + + protected DrawablePart(float start, float end, @ColorInt int color) { + mStart = start; + mEnd = end; + mColor = color; } - public Segment(float fraction, @ColorInt int color) { - this(fraction, color, false); + public float getStart() { + return this.mStart; } - public Segment(float fraction, @ColorInt int color, boolean faded) { - mFraction = fraction; - mColor = color; - mFaded = faded; + public void setStart(float start) { + mStart = start; } - public float getFraction() { - return this.mFraction; + public float getEnd() { + return this.mEnd; } - public int getColor() { - return this.mColor; + public void setEnd(float end) { + mEnd = end; } - public boolean getFaded() { - return this.mFaded; + /** Returns the calculated drawing width of the part */ + public float getWidth() { + return mEnd - mStart; } - @Override - public String toString() { - return "Segment(fraction=" + this.mFraction + ", color=" + this.mColor + ", faded=" - + this.mFaded + ')'; + public int getColor() { + return this.mColor; } // Needed for unit tests @@ -530,80 +465,79 @@ public final class NotificationProgressDrawable extends Drawable { if (other == null || getClass() != other.getClass()) return false; - Segment that = (Segment) other; - if (Float.compare(this.mFraction, that.mFraction) != 0) return false; - if (this.mColor != that.mColor) return false; - return this.mFaded == that.mFaded; + DrawablePart that = (DrawablePart) other; + if (Float.compare(this.mStart, that.mStart) != 0) return false; + if (Float.compare(this.mEnd, that.mEnd) != 0) return false; + return this.mColor == that.mColor; } @Override public int hashCode() { - return Objects.hash(mFraction, mColor, mFaded); + return Objects.hash(mStart, mEnd, mColor); } } /** - * A point is a part of the progress bar with zero length. Points are designated points within a - * progressbar to visualize distinct stages or milestones. For example, a stop in a multi-stop - * ride-share journey. + * A segment is a part of the progress bar with non-zero length. For example, it can + * represent a portion in a navigation journey with certain traffic condition. + * <p> + * The start and end positions for drawing a segment are assumed to have been adjusted for + * the Points and gaps neighboring the segment. + * </p> */ - public static final class Point implements Part { - @Nullable - private final Drawable mIcon; - @ColorInt private final int mColor; + public static final class DrawableSegment extends DrawablePart { + /** + * Whether the segment is faded or not. + * <p> + * Faded segments and non-faded segments are drawn with different heights. + * </p> + */ private final boolean mFaded; - public Point(@Nullable Drawable icon) { - this(icon, Color.TRANSPARENT, false); - } - - public Point(@Nullable Drawable icon, @ColorInt int color) { - this(icon, color, false); - + public DrawableSegment(float start, float end, int color) { + this(start, end, color, false); } - public Point(@Nullable Drawable icon, @ColorInt int color, boolean faded) { - mIcon = icon; - mColor = color; + public DrawableSegment(float start, float end, int color, boolean faded) { + super(start, end, color); mFaded = faded; } - @Nullable - public Drawable getIcon() { - return this.mIcon; - } - - public int getColor() { - return this.mColor; - } - - public boolean getFaded() { - return this.mFaded; - } - @Override public String toString() { - return "Point(icon=" + this.mIcon + ", color=" + this.mColor + ", faded=" + this.mFaded - + ")"; + return "Segment(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ", faded=" + this.mFaded + ')'; } // Needed for unit tests. @Override public boolean equals(@Nullable Object other) { - if (this == other) return true; - - if (other == null || getClass() != other.getClass()) return false; + if (!super.equals(other)) return false; - Point that = (Point) other; - - if (!Objects.equals(this.mIcon, that.mIcon)) return false; - if (this.mColor != that.mColor) return false; + DrawableSegment that = (DrawableSegment) other; return this.mFaded == that.mFaded; } @Override public int hashCode() { - return Objects.hash(mIcon, mColor, mFaded); + return Objects.hash(super.hashCode(), mFaded); + } + } + + /** + * A point is a part of the progress bar with zero length. Points are designated points within a + * progress bar to visualize distinct stages or milestones. For example, a stop in a multi-stop + * ride-share journey. + */ + public static final class DrawablePoint extends DrawablePart { + public DrawablePoint(float start, float end, int color) { + super(start, end, color); + } + + @Override + public String toString() { + return "Point(start=" + this.mStart + ", end=" + this.mEnd + ", color=" + this.mColor + + ")"; } } @@ -628,16 +562,14 @@ public final class NotificationProgressDrawable extends Drawable { int mChangingConfigurations; float mSegSegGap = 0.0f; float mSegPointGap = 0.0f; + float mSegmentMinWidth = 0.0f; float mSegmentHeight; float mFadedSegmentHeight; float mSegmentCornerRadius; - int mSegmentColor; - int mFadedSegmentColor; + // how big the point icon will be, halved float mPointRadius; float mPointRectInset; float mPointRectCornerRadius; - int mPointRectColor; - int mFadedPointRectColor; int[] mThemeAttrs; int[] mThemeAttrsSegments; @@ -652,16 +584,13 @@ public final class NotificationProgressDrawable extends Drawable { mChangingConfigurations = orig.mChangingConfigurations; mSegSegGap = orig.mSegSegGap; mSegPointGap = orig.mSegPointGap; + mSegmentMinWidth = orig.mSegmentMinWidth; mSegmentHeight = orig.mSegmentHeight; mFadedSegmentHeight = orig.mFadedSegmentHeight; mSegmentCornerRadius = orig.mSegmentCornerRadius; - mSegmentColor = orig.mSegmentColor; - mFadedSegmentColor = orig.mFadedSegmentColor; mPointRadius = orig.mPointRadius; mPointRectInset = orig.mPointRectInset; mPointRectCornerRadius = orig.mPointRectCornerRadius; - mPointRectColor = orig.mPointRectColor; - mFadedPointRectColor = orig.mFadedPointRectColor; mThemeAttrs = orig.mThemeAttrs; mThemeAttrsSegments = orig.mThemeAttrsSegments; @@ -674,6 +603,18 @@ public final class NotificationProgressDrawable extends Drawable { } private void applyDensityScaling(int sourceDensity, int targetDensity) { + if (mSegSegGap > 0) { + mSegSegGap = scaleFromDensity( + mSegSegGap, sourceDensity, targetDensity); + } + if (mSegPointGap > 0) { + mSegPointGap = scaleFromDensity( + mSegPointGap, sourceDensity, targetDensity); + } + if (mSegmentMinWidth > 0) { + mSegmentMinWidth = scaleFromDensity( + mSegmentMinWidth, sourceDensity, targetDensity); + } if (mSegmentHeight > 0) { mSegmentHeight = scaleFromDensity( mSegmentHeight, sourceDensity, targetDensity); @@ -740,28 +681,6 @@ public final class NotificationProgressDrawable extends Drawable { applyDensityScaling(sourceDensity, targetDensity); } } - - public void setSegmentColor(int color) { - mSegmentColor = color; - mFadedSegmentColor = getFadedColor(color); - } - - public void setPointRectColor(int color) { - mPointRectColor = color; - mFadedPointRectColor = getFadedColor(color); - } - } - - /** - * Get a color with an opacity that's 25% of the input color. - */ - @ColorInt - static int getFadedColor(@ColorInt int color) { - return Color.argb( - (int) (Color.alpha(color) * 0.4f + 0.5f), - Color.red(color), - Color.green(color), - Color.blue(color)); } @Override diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index e6364a96bd9f..1e7bfe32ba79 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -111,8 +111,9 @@ static void DeleteGuardedApkAssets(Guarded<AssetManager2::ApkAssetsPtr>& apk_ass class LoaderAssetsProvider : public AssetsProvider { public: static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) { - return std::unique_ptr<AssetsProvider>{ - assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr}; + return (!assets_provider) ? EmptyAssetsProvider::Create() + : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider( + env, assets_provider)); } bool ForEachFile(const std::string& /* root_path */, @@ -128,8 +129,8 @@ class LoaderAssetsProvider : public AssetsProvider { return debug_name_; } - UpToDate IsUpToDate() const override { - return UpToDate::Always; + bool IsUpToDate() const override { + return true; } ~LoaderAssetsProvider() override { @@ -211,7 +212,7 @@ class LoaderAssetsProvider : public AssetsProvider { auto string_result = static_cast<jstring>(env->CallObjectMethod( assets_provider_, gAssetsProviderOffsets.toString)); ScopedUtfChars str(env, string_result); - debug_name_ = std::string(str.c_str()); + debug_name_ = std::string(str.c_str(), str.size()); } // The global reference to the AssetsProvider @@ -242,10 +243,10 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags); break; case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), - MultiAssetsProvider::Create(std::move(loader_assets)), - property_flags); - break; + apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), + std::move(loader_assets), + property_flags); + break; case FORMAT_DIRECTORY: { auto assets = MultiAssetsProvider::Create(std::move(loader_assets), DirectoryAssetsProvider::Create(path.c_str())); @@ -315,11 +316,10 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), - nullptr /* path */), - MultiAssetsProvider::Create(std::move(loader_assets)), - property_flags); - break; + apk_assets = ApkAssets::LoadTable( + AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */), + std::move(loader_assets), property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -386,15 +386,12 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ break; } case FORMAT_ARSC: - apk_assets = - ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), - nullptr /* path */, - static_cast<off64_t>(offset), - static_cast<off64_t>( - length)), - MultiAssetsProvider::Create(std::move(loader_assets)), - property_flags); - break; + apk_assets = ApkAssets::LoadTable( + AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */, + static_cast<off64_t>(offset), + static_cast<off64_t>(length)), + std::move(loader_assets), property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -411,16 +408,13 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ } static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) { - auto apk_assets = ApkAssets::Load(MultiAssetsProvider::Create( - LoaderAssetsProvider::Create(env, assets_provider)), - flags); - if (apk_assets == nullptr) { - const std::string error_msg = - base::StringPrintf("Failed to load empty assets with provider %p", - (void*)assets_provider); - jniThrowException(env, "java/io/IOException", error_msg.c_str()); - return 0; - } + auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags); + if (apk_assets == nullptr) { + const std::string error_msg = + base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } return CreateGuardedApkAssets(std::move(apk_assets)); } @@ -449,10 +443,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool()); } -static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { +static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr)); auto apk_assets = scoped_apk_assets->get(); - return (jint)apk_assets->IsUpToDate(); + return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE; } static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) { @@ -564,7 +558,7 @@ static const JNINativeMethod gApkAssetsMethods[] = { {"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName}, {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, // @CriticalNative - {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate}, + {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;", (void*)NativeGetOverlayableInfo}, diff --git a/core/jni/android_hardware_UsbDeviceConnection.cpp b/core/jni/android_hardware_UsbDeviceConnection.cpp index b1221ee38db3..68ef3d424d12 100644 --- a/core/jni/android_hardware_UsbDeviceConnection.cpp +++ b/core/jni/android_hardware_UsbDeviceConnection.cpp @@ -165,19 +165,25 @@ android_hardware_UsbDeviceConnection_control_request(JNIEnv *env, jobject thiz, return -1; } - jbyte* bufferBytes = NULL; - if (buffer) { - bufferBytes = (jbyte*)env->GetPrimitiveArrayCritical(buffer, NULL); + bool is_dir_in = (requestType & USB_ENDPOINT_DIR_MASK) == USB_DIR_IN; + std::unique_ptr<jbyte[]> bufferBytes(new (std::nothrow) jbyte[length]); + if (!bufferBytes) { + jniThrowException(env, "java/lang/OutOfMemoryError", NULL); + return -1; + } + + if (!is_dir_in && buffer) { + env->GetByteArrayRegion(buffer, start, length, bufferBytes.get()); } - jint result = usb_device_control_transfer(device, requestType, request, - value, index, bufferBytes + start, length, timeout); + jint bytes_transferred = usb_device_control_transfer(device, requestType, request, + value, index, bufferBytes.get(), length, timeout); - if (bufferBytes) { - env->ReleasePrimitiveArrayCritical(buffer, bufferBytes, 0); + if (bytes_transferred > 0 && is_dir_in) { + env->SetByteArrayRegion(buffer, start, bytes_transferred, bufferBytes.get()); } - return result; + return bytes_transferred; } static jint diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index 3c2dccd451d4..9ef17e82c38e 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -729,6 +729,17 @@ static jlong android_os_Debug_getGpuPrivateMemoryKb(JNIEnv* env, jobject clazz) return gpuPrivateMem / 1024; } +static jlong android_os_Debug_getKernelCmaUsageKb(JNIEnv* env, jobject clazz) { + jlong totalKernelCmaUsageKb = -1; + uint64_t size; + + if (meminfo::ReadKernelCmaUsageKb(&size)) { + totalKernelCmaUsageKb = size; + } + + return totalKernelCmaUsageKb; +} + static jlong android_os_Debug_getDmabufMappedSizeKb(JNIEnv* env, jobject clazz) { jlong dmabufPss = 0; std::vector<dmabufinfo::DmaBuffer> dmabufs; @@ -836,6 +847,7 @@ static const JNINativeMethod gMethods[] = { {"getGpuTotalUsageKb", "()J", (void*)android_os_Debug_getGpuTotalUsageKb}, {"isVmapStack", "()Z", (void*)android_os_Debug_isVmapStack}, {"logAllocatorStats", "()Z", (void*)android_os_Debug_logAllocatorStats}, + {"getKernelCmaUsageKb", "()J", (void*)android_os_Debug_getKernelCmaUsageKb}, }; int register_android_os_Debug(JNIEnv *env) diff --git a/core/jni/android_view_InputEventReceiver.cpp b/core/jni/android_view_InputEventReceiver.cpp index 3a1e8835c8db..6272fb1947c1 100644 --- a/core/jni/android_view_InputEventReceiver.cpp +++ b/core/jni/android_view_InputEventReceiver.cpp @@ -441,7 +441,8 @@ status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, } env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.onDragEvent, jboolean(dragEvent->isExiting()), dragEvent->getX(), - dragEvent->getY()); + dragEvent->getY(), + static_cast<jint>(dragEvent->getDisplayId().val())); finishInputEvent(seq, /*handled=*/true); continue; } @@ -643,7 +644,7 @@ int register_android_view_InputEventReceiver(JNIEnv* env) { GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onPointerCaptureEvent", "(Z)V"); gInputEventReceiverClassInfo.onDragEvent = - GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFF)V"); + GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onDragEvent", "(ZFFI)V"); gInputEventReceiverClassInfo.onTouchModeChanged = GetMethodIDOrDie(env, gInputEventReceiverClassInfo.clazz, "onTouchModeChanged", "(Z)V"); gInputEventReceiverClassInfo.onBatchedInputEventPending = diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 4f7ba9388a1d..5d0b340ac839 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -580,6 +580,7 @@ message SecureSettingsProto { optional SettingProto activate_on_dock = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto activate_on_sleep = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto default_component = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto activate_on_postured = 6 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Screensaver screensaver = 47; diff --git a/core/res/res/drawable/notification_progress.xml b/core/res/res/drawable/notification_progress.xml index 5d272fb00e34..ff5450ee106f 100644 --- a/core/res/res/drawable/notification_progress.xml +++ b/core/res/res/drawable/notification_progress.xml @@ -24,6 +24,7 @@ android:segPointGap="@dimen/notification_progress_segPoint_gap"> <segments android:color="?attr/colorProgressBackgroundNormal" + android:minWidth="@dimen/notification_progress_segments_min_width" android:height="@dimen/notification_progress_segments_height" android:fadedHeight="@dimen/notification_progress_segments_faded_height" android:cornerRadius="@dimen/notification_progress_segments_corner_radius"/> diff --git a/core/res/res/layout/notification_2025_conversation_header.xml b/core/res/res/layout/notification_2025_conversation_header.xml index db79e79c96df..1bde17358825 100644 --- a/core/res/res/layout/notification_2025_conversation_header.xml +++ b/core/res/res/layout/notification_2025_conversation_header.xml @@ -136,10 +136,10 @@ <ImageView android:id="@+id/phishing_alert" - android:layout_width="@dimen/notification_phishing_alert_size" - android:layout_height="@dimen/notification_phishing_alert_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:scaleType="fitCenter" android:src="@drawable/ic_dialog_alert_material" android:visibility="gone" @@ -148,10 +148,10 @@ <ImageView android:id="@+id/profile_badge" - android:layout_width="@dimen/notification_badge_size" - android:layout_height="@dimen/notification_badge_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:scaleType="fitCenter" android:visibility="gone" android:contentDescription="@string/notification_work_profile_content_description" @@ -159,10 +159,10 @@ <ImageView android:id="@+id/alerted_icon" - android:layout_width="@dimen/notification_alerted_size" - android:layout_height="@dimen/notification_alerted_size" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:baseline="10dp" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" android:contentDescription="@string/notification_alerted_content_description" android:scaleType="fitCenter" android:src="@drawable/ic_notifications_alerted" diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index f108ce5bd1b9..d29b7af9e24e 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -87,7 +87,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -104,7 +104,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index bd17a3a0a74e..5beab508aecf 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -89,7 +89,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -106,7 +106,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index edbebb17f825..d7c3263904d4 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -115,7 +115,7 @@ > <!-- - NOTE: The notification_top_line_views layout contains the app_name_text. + NOTE: The notification_2025_top_line_views layout contains the app_name_text. In order to include the title view at the beginning, the Notification.Builder has logic to hide that view whenever this title view is to be visible. --> @@ -132,7 +132,7 @@ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" /> - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index 0c07053d428a..72b3798e0780 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -68,7 +68,7 @@ android:theme="@style/Theme.DeviceDefault.Notification" > - <include layout="@layout/notification_top_line_views" /> + <include layout="@layout/notification_2025_top_line_views" /> </NotificationTopLineView> diff --git a/core/res/res/layout/notification_2025_template_heads_up_base.xml b/core/res/res/layout/notification_2025_template_heads_up_base.xml index e4ff835a3524..084ec7daa683 100644 --- a/core/res/res/layout/notification_2025_template_heads_up_base.xml +++ b/core/res/res/layout/notification_2025_template_heads_up_base.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?><!-- - ~ Copyright (C) 2014 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. diff --git a/core/res/res/layout/notification_2025_top_line_views.xml b/core/res/res/layout/notification_2025_top_line_views.xml new file mode 100644 index 000000000000..74873463391e --- /dev/null +++ b/core/res/res/layout/notification_2025_top_line_views.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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 + --> +<!-- + This layout file should be included inside a NotificationTopLineView, sometimes after a + <TextView android:id="@+id/title"/> +--> +<merge + xmlns:android="http://schemas.android.com/apk/res/android"> + + <TextView + android:id="@+id/app_name_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:singleLine="true" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:visibility="?attr/notificationHeaderAppNameVisibility" + /> + + <TextView + android:id="@+id/header_text_secondary_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:visibility="gone" + /> + + <TextView + android:id="@+id/header_text_secondary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:visibility="gone" + android:singleLine="true" + /> + + <TextView + android:id="@+id/header_text_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:visibility="gone" + /> + + <TextView + android:id="@+id/header_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:visibility="gone" + android:singleLine="true" + /> + + <TextView + android:id="@+id/time_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:singleLine="true" + android:visibility="gone" + /> + + <DateTimeView + android:id="@+id/time" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:showRelative="true" + android:singleLine="true" + android:visibility="gone" + /> + + <ViewStub + android:id="@+id/chronometer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:layout_marginEnd="@dimen/notification_header_separating_margin" + android:layout="@layout/notification_template_part_chronometer" + android:visibility="gone" + /> + + <ImageButton + android:id="@+id/feedback" + android:layout_width="@dimen/notification_feedback_size" + android:layout_height="@dimen/notification_feedback_size" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:baseline="13dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_feedback_indicator" + android:background="?android:selectableItemBackgroundBorderless" + android:visibility="gone" + android:contentDescription="@string/notification_feedback_indicator" + /> + + <ImageView + android:id="@+id/phishing_alert" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:src="@drawable/ic_dialog_alert_material" + android:visibility="gone" + android:contentDescription="@string/notification_phishing_alert_content_description" + /> + + <ImageView + android:id="@+id/profile_badge" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:scaleType="fitCenter" + android:visibility="gone" + android:contentDescription="@string/notification_work_profile_content_description" + /> + + <ImageView + android:id="@+id/alerted_icon" + android:layout_width="@dimen/notification_2025_badge_size" + android:layout_height="@dimen/notification_2025_badge_size" + android:layout_marginStart="@dimen/notification_2025_badge_margin" + android:baseline="@dimen/notification_2025_badge_baseline" + android:contentDescription="@string/notification_alerted_content_description" + android:scaleType="fitCenter" + android:src="@drawable/ic_notifications_alerted" + android:visibility="gone" + /> +</merge> + diff --git a/core/res/res/values-round-watch/dimens.xml b/core/res/res/values-round-watch/dimens.xml index f288b41fb556..59ee554798bc 100644 --- a/core/res/res/values-round-watch/dimens.xml +++ b/core/res/res/values-round-watch/dimens.xml @@ -26,6 +26,6 @@ <item name="input_extract_action_button_height" type="dimen">32dp</item> <item name="input_extract_action_icon_padding" type="dimen">5dp</item> - <item name="global_actions_vertical_padding_percentage" type="fraction">20.8%</item> + <item name="global_actions_vertical_padding_percentage" type="fraction">21.8%</item> <item name="global_actions_horizontal_padding_percentage" type="fraction">5.2%</item> </resources> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index e6295ea06177..4ff3f8825cc4 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -101,6 +101,13 @@ P.S this is a change only intended for wear devices. --> <bool name="config_enableViewGroupScalingFading">true</bool> - <!-- Allow the gesture to double tap the power button to trigger a target action. --> - <bool name="config_doubleTapPowerGestureEnabled">false</bool> + <!-- Controls the double tap power button gesture to trigger a target action. + 0: Gesture is disabled + 1: Launch camera mode, allowing the user to disable/enable the double tap power gesture + from launching the camera application. + 2: Multi target mode, allowing the user to select one of the targets defined in + config_doubleTapPowerGestureMultiTargetDefaultAction and to disable/enable the double + tap power gesture from triggering the selected target action. + --> + <integer name="config_doubleTapPowerGestureMode">0</integer> </resources> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 728c856f5855..8bf61bbcf55e 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7572,25 +7572,31 @@ <!-- NotificationProgressDrawable class --> <!-- ================================== --> - <!-- Drawable used to render a segmented bar, with segments and points. --> + <!-- Drawable used to render a notification progress bar, with segments and points. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawable"> - <!-- Default color for the parts. --> + <!-- The gap between two segments. --> <attr name="segSegGap" format="dimension" /> + <!-- The gap between a segment and a point. --> <attr name="segPointGap" format="dimension" /> </declare-styleable> <!-- Used to config the segments of a NotificationProgressDrawable. --> <!-- @hide internal use only --> <declare-styleable name="NotificationProgressDrawableSegments"> - <!-- Height of the solid segments --> + <!-- TODO: b/372908709 - maybe move this to NotificationProgressBar, because that's the only + place this is used actually. Same for NotificationProgressDrawable.segSegGap/segPointGap + above. --> + <!-- Minimum required drawing width. The drawing width refers to the width after + the original segments have been adjusted for the neighboring Points and gaps. This is + enforced by stretching the segments that are too short. --> + <attr name="minWidth" /> + <!-- Height of the solid segments. --> <attr name="height" /> - <!-- Height of the faded segments --> - <attr name="fadedHeight" format="dimension"/> + <!-- Height of the faded segments. --> + <attr name="fadedHeight" format="dimension" /> <!-- Corner radius of the segment rect. --> <attr name="cornerRadius" format="dimension" /> - <!-- Default color of the segment. --> - <attr name="color" /> </declare-styleable> <!-- Used to config the points of a NotificationProgressDrawable. --> @@ -7602,8 +7608,6 @@ <attr name="inset" /> <!-- Corner radius of the point rect. --> <attr name="cornerRadius"/> - <!-- Default color of the point rect. --> - <attr name="color" /> </declare-styleable> <!-- ========================== --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index c50c5e9d3341..e14cffd72b0c 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2083,6 +2083,18 @@ See com.android.server.timezonedetector.TimeZoneDetectorStrategy for more information. --> <bool name="config_supportTelephonyTimeZoneFallback" translatable="false">true</bool> + <!-- Whether the time notifications feature is enabled. Settings this to false means the feature + cannot be used. Setting this to true means the feature can be enabled on the device. --> + <bool name="config_enableTimeZoneNotificationsSupported" translatable="false">true</bool> + + <!-- Whether the time zone notifications tracking feature is enabled. Settings this to false + means the feature cannot be used. --> + <bool name="config_enableTimeZoneNotificationsTrackingSupported" translatable="false">true</bool> + + <!-- Whether the time zone manual change tracking feature is enabled. Settings this to false + means the feature cannot be used. --> + <bool name="config_enableTimeZoneManualChangeTrackingSupported" translatable="false">true</bool> + <!-- Whether to enable network location overlay which allows network location provider to be replaced by an app at run-time. When disabled, only the config_networkLocationProviderPackageName package will be searched for network location @@ -2754,6 +2766,9 @@ <bool name="config_dreamsActivatedOnDockByDefault">true</bool> <!-- If supported and enabled, are dreams activated when asleep and charging? (by default) --> <bool name="config_dreamsActivatedOnSleepByDefault">false</bool> + <!-- If supported and enabled, are dreams enabled while device is stationary and upright? + (by default) --> + <bool name="config_dreamsActivatedOnPosturedByDefault">false</bool> <!-- ComponentName of the default dream (Settings.Secure.DEFAULT_SCREENSAVER_COMPONENT) --> <string name="config_dreamsDefaultComponent" translatable="false">com.android.deskclock/com.android.deskclock.Screensaver</string> <!-- ComponentNames of the dreams that we should hide --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 2adb79118ed9..d6b8704a978b 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -608,12 +608,23 @@ <!-- Size of the feedback indicator for notifications --> <dimen name="notification_feedback_size">20dp</dimen> + <!-- Size of the (work) profile badge for notifications --> + <dimen name="notification_badge_size">12dp</dimen> + + <!-- Size of the (work) profile badge for notifications (2025 redesign version). + Scales with font size. Chosen to look good alongside notification_subtext_size text. --> + <dimen name="notification_2025_badge_size">14sp</dimen> + + <!-- Baseline for aligning icons in the top line (like the work profile icon or alerting icon) + to the text properly. This is equal to notification_2025_badge_size - 2sp. --> + <dimen name="notification_2025_badge_baseline">12sp</dimen> + + <!-- Spacing for the top line icons (e.g. the work profile badge). --> + <dimen name="notification_2025_badge_margin">4dp</dimen> + <!-- Size of the phishing alert for notifications --> <dimen name="notification_phishing_alert_size">@dimen/notification_badge_size</dimen> - <!-- Size of the profile badge for notifications --> - <dimen name="notification_badge_size">12dp</dimen> - <!-- Size of the alerted icon for notifications --> <dimen name="notification_alerted_size">@dimen/notification_badge_size</dimen> @@ -888,6 +899,8 @@ <dimen name="notification_progress_segSeg_gap">4dp</dimen> <!-- The gap between a segment and a point in the notification progress bar --> <dimen name="notification_progress_segPoint_gap">4dp</dimen> + <!-- The minimum required drawing width of the notification progress bar segments --> + <dimen name="notification_progress_segments_min_width">16dp</dimen> <!-- The height of the notification progress bar segments --> <dimen name="notification_progress_segments_height">6dp</dimen> <!-- The height of the notification progress bar faded segments --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index ad9e7252c6a8..fa4c21de682e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -853,6 +853,9 @@ <!-- Text shown when viewing channel settings for notifications related to vpn status --> <string name="notification_channel_vpn">VPN status</string> + <!-- Text shown when viewing channel settings for notifications related to system time --> + <string name="notification_channel_system_time">Time and time zones</string> + <!-- Notification channel name. This channel sends high-priority alerts from the user's IT admin for key updates about the user's work device or work profile. --> <string name="notification_channel_device_admin">Alerts from your IT admin</string> @@ -3879,6 +3882,12 @@ <string name="carrier_app_notification_title">New SIM inserted</string> <string name="carrier_app_notification_text">Tap to set it up</string> + <!-- Time zone notification strings --> + <!-- Title for time zone change notifications --> + <string name="time_zone_change_notification_title">Your time zone changed</string> + <!-- Body for time zone change notifications --> + <string name="time_zone_change_notification_body">You\'re now in <xliff:g id="time_zone_display_name">%1$s</xliff:g> (<xliff:g id="time_zone_offset">%2$s</xliff:g>)</string> + <!-- Date/Time picker dialogs strings --> <!-- The title of the time picker dialog. [CHAR LIMIT=NONE] --> @@ -6637,6 +6646,8 @@ ul.</string> <string name="satellite_notification_title">Auto connected to satellite</string> <!-- Notification summary when satellite service is auto connected. [CHAR LIMIT=NONE] --> <string name="satellite_notification_summary">You can send and receive messages without a mobile or Wi-Fi network</string> + <!-- Notification summary when satellite service connected with data service supported. [CHAR LIMIT=NONE] --> + <string name="satellite_notification_summary_with_data">You can send and receive messages and use limited data by satellite</string> <!-- Notification title when satellite service can be manually enabled. --> <string name="satellite_notification_manual_title">Use satellite messaging?</string> <!-- Notification summary when satellite service can be manually enabled. [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index d20b95f59b0c..68008e57094d 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -575,6 +575,7 @@ <java-symbol type="dimen" name="notification_top_pad_large_text" /> <java-symbol type="dimen" name="notification_top_pad_large_text_narrow" /> <java-symbol type="dimen" name="notification_badge_size" /> + <java-symbol type="dimen" name="notification_2025_badge_size" /> <java-symbol type="dimen" name="immersive_mode_cling_width" /> <java-symbol type="dimen" name="accessibility_magnification_indicator_width" /> <java-symbol type="dimen" name="circular_display_mask_thickness" /> @@ -2329,6 +2330,7 @@ <java-symbol type="bool" name="config_dreamsEnabledOnBattery" /> <java-symbol type="bool" name="config_dreamsActivatedOnDockByDefault" /> <java-symbol type="bool" name="config_dreamsActivatedOnSleepByDefault" /> + <java-symbol type="bool" name="config_dreamsActivatedOnPosturedByDefault" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelMinimumWhenNotPowered" /> <java-symbol type="integer" name="config_dreamsBatteryLevelDrainCutoff" /> @@ -2377,6 +2379,9 @@ <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" /> <java-symbol type="bool" name="config_enableTelephonyTimeZoneDetection" /> <java-symbol type="bool" name="config_supportTelephonyTimeZoneFallback" /> + <java-symbol type="bool" name="config_enableTimeZoneNotificationsSupported" /> + <java-symbol type="bool" name="config_enableTimeZoneNotificationsTrackingSupported" /> + <java-symbol type="bool" name="config_enableTimeZoneManualChangeTrackingSupported" /> <java-symbol type="bool" name="config_autoResetAirplaneMode" /> <java-symbol type="string" name="config_notificationAccessConfirmationActivity" /> <java-symbol type="bool" name="config_preventImeStartupUnlessTextEditor" /> @@ -3946,6 +3951,7 @@ <java-symbol type="dimen" name="notification_progress_tracker_height" /> <java-symbol type="dimen" name="notification_progress_segSeg_gap" /> <java-symbol type="dimen" name="notification_progress_segPoint_gap" /> + <java-symbol type="dimen" name="notification_progress_segments_min_width" /> <java-symbol type="dimen" name="notification_progress_segments_height" /> <java-symbol type="dimen" name="notification_progress_segments_faded_height" /> <java-symbol type="dimen" name="notification_progress_segments_corner_radius" /> @@ -4025,6 +4031,7 @@ <java-symbol type="string" name="notification_channel_network_available" /> <java-symbol type="array" name="config_defaultCloudSearchServices" /> <java-symbol type="string" name="notification_channel_vpn" /> + <java-symbol type="string" name="notification_channel_system_time" /> <java-symbol type="string" name="notification_channel_device_admin" /> <java-symbol type="string" name="notification_channel_alerts" /> <java-symbol type="string" name="notification_channel_retail_mode" /> @@ -4035,6 +4042,8 @@ <java-symbol type="string" name="notification_channel_accessibility_hearing_device" /> <java-symbol type="string" name="notification_channel_accessibility_security_policy" /> <java-symbol type="string" name="notification_channel_display" /> + <java-symbol type="string" name="time_zone_change_notification_title" /> + <java-symbol type="string" name="time_zone_change_notification_body" /> <java-symbol type="string" name="config_defaultAutofillService" /> <java-symbol type="string" name="config_defaultFieldClassificationService" /> <java-symbol type="string" name="config_defaultOnDeviceSpeechRecognitionService" /> @@ -5643,6 +5652,7 @@ <!-- System notification for satellite service --> <java-symbol type="string" name="satellite_notification_title" /> <java-symbol type="string" name="satellite_notification_summary" /> + <java-symbol type="string" name="satellite_notification_summary_with_data" /> <java-symbol type="string" name="satellite_notification_manual_title" /> <java-symbol type="string" name="satellite_notification_manual_summary" /> <java-symbol type="string" name="satellite_notification_open_message" /> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 06cd44e6544d..9b3a6cba5f23 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -111,7 +111,7 @@ <shortcode country="cz" premium="90\\d{5}|90\\d{3}" free="116\\d{3}" /> <!-- Germany: 4-5 digits plus 1232xxx (premium codes from http://www.vodafone.de/infofaxe/537.pdf and http://premiumdienste.eplus.de/pdf/kodex.pdf), plus EU. To keep the premium regex from being too large, it only includes payment processors that have been used by SMS malware, with the regular pattern matching the other premium short codes. --> - <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438" /> + <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}|81214|81215|47529|70296|83782|3011|73240|72438|70997" /> <!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers --> <shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}|4665" /> diff --git a/core/tests/coretests/src/android/app/NotificationManagerTest.java b/core/tests/coretests/src/android/app/NotificationManagerTest.java index 6538ce85457c..3d6e1225bd92 100644 --- a/core/tests/coretests/src/android/app/NotificationManagerTest.java +++ b/core/tests/coretests/src/android/app/NotificationManagerTest.java @@ -16,6 +16,8 @@ package android.app; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; @@ -25,8 +27,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.ParceledListSlice; +import android.os.UserHandle; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; @@ -35,6 +41,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,6 +49,7 @@ import org.junit.runner.RunWith; import java.time.Instant; import java.time.InstantSource; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest @@ -50,14 +58,24 @@ public class NotificationManagerTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private Context mContext; private NotificationManagerWithMockService mNotificationManager; private final FakeClock mClock = new FakeClock(); + private PackageTestableContext mContext; + @Before public void setUp() { - mContext = ApplicationProvider.getApplicationContext(); + mContext = new PackageTestableContext(ApplicationProvider.getApplicationContext()); mNotificationManager = new NotificationManagerWithMockService(mContext, mClock); + + // Caches must be in test mode in order to be used in tests. + PropertyInvalidatedCache.setTestMode(true); + mNotificationManager.setChannelCacheToTestMode(); + } + + @After + public void tearDown() { + PropertyInvalidatedCache.setTestMode(false); } @Test @@ -243,12 +261,161 @@ public class NotificationManagerTest { anyInt(), any(), anyInt()); } + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_cachedUntilInvalidated() throws Exception { + // Invalidate the cache first because the cache won't do anything until then + NotificationManager.invalidateNotificationChannelCache(); + + // It doesn't matter what the returned contents are, as long as we return a channel. + // This setup must set up getNotificationChannels(), as that's the method called. + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for the same channel 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + NotificationChannel unused = mNotificationManager.getNotificationChannel("id"); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_sameApp_oneCall() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + NotificationChannel c1 = new NotificationChannel("id1", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel c2 = new NotificationChannel("id2", "name2", + NotificationManager.IMPORTANCE_NONE); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(c1, c2))); + + assertThat(mNotificationManager.getNotificationChannel("id1")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("id2")).isEqualTo(c2); + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannels_cachedUntilInvalidated() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), + anyInt())).thenReturn(new ParceledListSlice<>(List.of(exampleChannel()))); + + // ask for channels 100 times without invalidating the cache + for (int i = 0; i < 100; i++) { + List<NotificationChannel> unused = mNotificationManager.getNotificationChannels(); + } + + // invalidate the cache; then ask again + NotificationManager.invalidateNotificationChannelCache(); + List<NotificationChannel> res = mNotificationManager.getNotificationChannels(); + + verify(mNotificationManager.mBackendService, times(2)) + .getNotificationChannels(any(), any(), anyInt()); + assertThat(res).containsExactlyElementsIn(List.of(exampleChannel())); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_channelAndConversationLookup() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + + // Full list of channels: c1; conv1 = child of c1; c2 is unrelated + NotificationChannel c1 = new NotificationChannel("id", "name", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel conv1 = new NotificationChannel("", "name_conversation", + NotificationManager.IMPORTANCE_DEFAULT); + conv1.setConversationId("id", "id_conversation"); + NotificationChannel c2 = new NotificationChannel("other", "name2", + NotificationManager.IMPORTANCE_DEFAULT); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), any(), anyInt())) + .thenReturn(new ParceledListSlice<>(List.of(c1, conv1, c2))); + + // Lookup for channel c1 and c2: returned as expected + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(c1); + assertThat(mNotificationManager.getNotificationChannel("other")).isEqualTo(c2); + + // Lookup for conv1 should return conv1 + assertThat(mNotificationManager.getNotificationChannel("id", "id_conversation")).isEqualTo( + conv1); + + // Lookup for a different conversation channel that doesn't exist, whose parent channel id + // is "id", should return c1 + assertThat(mNotificationManager.getNotificationChannel("id", "nonexistent")).isEqualTo(c1); + + // Lookup of a nonexistent channel is null + assertThat(mNotificationManager.getNotificationChannel("id3")).isNull(); + + // All of that should have been one call to getNotificationChannels() + verify(mNotificationManager.mBackendService, times(1)) + .getNotificationChannels(any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void getNotificationChannel_differentPackages() throws Exception { + NotificationManager.invalidateNotificationChannelCache(); + final String pkg1 = "one"; + final String pkg2 = "two"; + final int userId = 0; + final int userId1 = 1; + + // multiple channels with the same ID, but belonging to different packages/users + NotificationChannel channel1 = new NotificationChannel("id", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel2 = channel1.copy(); + channel2.setName("name2"); + NotificationChannel channel3 = channel1.copy(); + channel3.setName("name3"); + + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel1))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg2), + eq(userId))).thenReturn(new ParceledListSlice<>(List.of(channel2))); + when(mNotificationManager.mBackendService.getNotificationChannels(any(), eq(pkg1), + eq(userId1))).thenReturn(new ParceledListSlice<>(List.of(channel3))); + + // set our context to pretend to be from package 1 and userId 0 + mContext.setParameters(pkg1, pkg1, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel1); + + // now package 2 + mContext.setParameters(pkg2, pkg2, userId); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel2); + + // now pkg1 for a different user + mContext.setParameters(pkg1, pkg1, userId1); + assertThat(mNotificationManager.getNotificationChannel("id")).isEqualTo(channel3); + + // Those should have been three different calls + verify(mNotificationManager.mBackendService, times(3)) + .getNotificationChannels(any(), any(), anyInt()); + } + private Notification exampleNotification() { return new Notification.Builder(mContext, "channel") .setSmallIcon(android.R.drawable.star_big_on) .build(); } + private NotificationChannel exampleChannel() { + return new NotificationChannel("id", "channel_name", + NotificationManager.IMPORTANCE_DEFAULT); + } + private static class NotificationManagerWithMockService extends NotificationManager { private final INotificationManager mBackendService; @@ -264,6 +431,48 @@ public class NotificationManagerTest { } } + // Helper context wrapper class where we can control just the return values of getPackageName, + // getOpPackageName, and getUserId (used in getNotificationChannels). + private static class PackageTestableContext extends ContextWrapper { + private String mPackage; + private String mOpPackage; + private Integer mUserId; + + PackageTestableContext(Context base) { + super(base); + } + + void setParameters(String packageName, String opPackageName, int userId) { + mPackage = packageName; + mOpPackage = opPackageName; + mUserId = userId; + } + + @Override + public String getPackageName() { + if (mPackage != null) return mPackage; + return super.getPackageName(); + } + + @Override + public String getOpPackageName() { + if (mOpPackage != null) return mOpPackage; + return super.getOpPackageName(); + } + + @Override + public int getUserId() { + if (mUserId != null) return mUserId; + return super.getUserId(); + } + + @Override + public UserHandle getUser() { + if (mUserId != null) return UserHandle.of(mUserId); + return super.getUser(); + } + } + private static class FakeClock implements InstantSource { private long mNowMillis = 441644400000L; diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index ca6ad6fae46e..7be6950fb613 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -2504,6 +2504,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressSegments() { + final List<Notification.ProgressStyle.Segment> segments = List.of( + new Notification.ProgressStyle.Segment(100).setColor(Color.WHITE), + new Notification.ProgressStyle.Segment(50).setColor(Color.RED), + new Notification.ProgressStyle.Segment(50).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressSegments(segments); + + assertThat(progressStyle1.getProgressSegments()).isEqualTo(segments); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_addProgressPoint_dropsNegativePoints() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2532,6 +2547,21 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoints() { + final List<Notification.ProgressStyle.Point> points = List.of( + new Notification.ProgressStyle.Point(0).setColor(Color.WHITE), + new Notification.ProgressStyle.Point(50).setColor(Color.RED), + new Notification.ProgressStyle.Point(100).setColor(Color.BLUE) + ); + + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressPoints(points); + + assertThat(progressStyle1.getProgressPoints()).isEqualTo(points); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { // GIVEN final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); @@ -2673,11 +2703,58 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressIndeterminate() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setProgressIndeterminate(true); + assertThat(progressStyle1.isProgressIndeterminate()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_styledByProgress_defaultValueTrue() { final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); assertThat(progressStyle1.isStyledByProgress()).isTrue(); } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setStyledByProgress() { + final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); + progressStyle1.setStyledByProgress(false); + assertThat(progressStyle1.isStyledByProgress()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_point() { + final int id = 1; + final int position = 10; + final int color = Color.RED; + + final Notification.ProgressStyle.Point point = + new Notification.ProgressStyle.Point(position).setId(id).setColor(color); + + assertEquals(id, point.getId()); + assertEquals(position, point.getPosition()); + assertEquals(color, point.getColor()); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_segment() { + final int id = 1; + final int length = 100; + final int color = Color.RED; + + final Notification.ProgressStyle.Segment segment = + new Notification.ProgressStyle.Segment(length).setId(id).setColor(color); + + assertEquals(id, segment.getId()); + assertEquals(length, segment.getLength()); + assertEquals(color, segment.getColor()); + } + private void assertValid(Notification.Colors c) { // Assert that all colors are populated assertThat(c.getBackgroundColor()).isNotEqualTo(Notification.COLOR_INVALID); diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS index c45080fb5e26..5fd4ffc7329a 100644 --- a/core/tests/coretests/src/android/os/OWNERS +++ b/core/tests/coretests/src/android/os/OWNERS @@ -10,6 +10,9 @@ per-file PowerManager*.java = file:/services/core/java/com/android/server/power/ # PerformanceHintManager per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS +# SystemHealthManager +per-file SystemHealthManagerUnitTest.java = file:/ADPF_OWNERS + # Caching per-file IpcDataCache* = file:/PERFORMANCE_OWNERS diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index da9d687ee2b0..3e6520106ab0 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -361,7 +361,11 @@ public class ParcelTest { p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie"); p.recycle(); - assertThat(p.getClassCookie(ParcelTest.class)).isNull(); + + // cannot access Parcel after it's recycled! + // this test is equivalent to checking hasClassCookie false + // after obtaining above + // assertThat(p.getClassCookie(ParcelTest.class)).isNull(); } @Test diff --git a/core/tests/coretests/src/android/os/SystemHealthManagerUnitTest.java b/core/tests/coretests/src/android/os/SystemHealthManagerUnitTest.java new file mode 100644 index 000000000000..e093e1af2eb5 --- /dev/null +++ b/core/tests/coretests/src/android/os/SystemHealthManagerUnitTest.java @@ -0,0 +1,153 @@ +/* + * 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.os; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.power.CpuHeadroomResult; +import android.hardware.power.GpuHeadroomResult; +import android.hardware.power.SupportInfo; +import android.os.health.SystemHealthManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.app.IBatteryStats; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class SystemHealthManagerUnitTest { + @Mock + private IBatteryStats mBatteryStats; + @Mock + private IPowerStatsService mPowerStats; + @Mock + private IHintManager mHintManager; + private SystemHealthManager mSystemHealthManager; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + IHintManager.HintManagerClientData clientData = new IHintManager.HintManagerClientData(); + clientData.supportInfo = new SupportInfo(); + clientData.maxCpuHeadroomThreads = 10; + clientData.supportInfo.headroom = new SupportInfo.HeadroomSupportInfo(); + clientData.supportInfo.headroom.isCpuSupported = true; + clientData.supportInfo.headroom.isGpuSupported = true; + clientData.supportInfo.headroom.cpuMinCalculationWindowMillis = 45; + clientData.supportInfo.headroom.cpuMaxCalculationWindowMillis = 9999; + clientData.supportInfo.headroom.gpuMinCalculationWindowMillis = 46; + clientData.supportInfo.headroom.gpuMaxCalculationWindowMillis = 9998; + clientData.supportInfo.headroom.cpuMinIntervalMillis = 999; + clientData.supportInfo.headroom.gpuMinIntervalMillis = 998; + when(mHintManager.getClientData()).thenReturn(clientData); + mSystemHealthManager = new SystemHealthManager(mBatteryStats, mPowerStats, mHintManager); + } + + @Test + public void testHeadroomParamsValueRange() { + assertEquals(999, mSystemHealthManager.getCpuHeadroomMinIntervalMillis()); + assertEquals(998, mSystemHealthManager.getGpuHeadroomMinIntervalMillis()); + assertEquals(45, (int) mSystemHealthManager.getCpuHeadroomCalculationWindowRange().first); + assertEquals(9999, + (int) mSystemHealthManager.getCpuHeadroomCalculationWindowRange().second); + assertEquals(46, (int) mSystemHealthManager.getGpuHeadroomCalculationWindowRange().first); + assertEquals(9998, + (int) mSystemHealthManager.getGpuHeadroomCalculationWindowRange().second); + assertEquals(10, (int) mSystemHealthManager.getMaxCpuHeadroomTidsSize()); + } + + @Test + public void testGetCpuHeadroom() throws RemoteException, InterruptedException { + final CpuHeadroomParams params1 = null; + final CpuHeadroomParamsInternal internalParams1 = new CpuHeadroomParamsInternal(); + + final CpuHeadroomParams params2 = new CpuHeadroomParams.Builder() + .setCalculationWindowMillis(100) + .build(); + final CpuHeadroomParamsInternal internalParams2 = new CpuHeadroomParamsInternal(); + internalParams2.calculationWindowMillis = 100; + + final CpuHeadroomParams params3 = new CpuHeadroomParams.Builder() + .setCalculationType(CpuHeadroomParams.CPU_HEADROOM_CALCULATION_TYPE_AVERAGE) + .build(); + final CpuHeadroomParamsInternal internalParams3 = new CpuHeadroomParamsInternal(); + internalParams3.calculationType = + (byte) CpuHeadroomParams.CPU_HEADROOM_CALCULATION_TYPE_AVERAGE; + + final CpuHeadroomParams params4 = new CpuHeadroomParams.Builder() + .setTids(1000, 1001) + .build(); + final CpuHeadroomParamsInternal internalParams4 = new CpuHeadroomParamsInternal(); + internalParams4.tids = new int[]{1000, 1001}; + + when(mHintManager.getCpuHeadroom(internalParams1)).thenReturn( + CpuHeadroomResult.globalHeadroom(99f)); + when(mHintManager.getCpuHeadroom(internalParams2)).thenReturn( + CpuHeadroomResult.globalHeadroom(98f)); + when(mHintManager.getCpuHeadroom(internalParams3)).thenReturn( + CpuHeadroomResult.globalHeadroom(97f)); + when(mHintManager.getCpuHeadroom(internalParams4)).thenReturn(null); + + assertEquals(99f, mSystemHealthManager.getCpuHeadroom(params1), 0.001f); + assertEquals(98f, mSystemHealthManager.getCpuHeadroom(params2), 0.001f); + assertEquals(97f, mSystemHealthManager.getCpuHeadroom(params3), 0.001f); + assertTrue(Float.isNaN(mSystemHealthManager.getCpuHeadroom(params4))); + verify(mHintManager, times(1)).getCpuHeadroom(internalParams1); + verify(mHintManager, times(1)).getCpuHeadroom(internalParams2); + verify(mHintManager, times(1)).getCpuHeadroom(internalParams3); + verify(mHintManager, times(1)).getCpuHeadroom(internalParams4); + } + + @Test + public void testGetGpuHeadroom() throws RemoteException, InterruptedException { + final GpuHeadroomParams params1 = null; + final GpuHeadroomParamsInternal internalParams1 = new GpuHeadroomParamsInternal(); + final GpuHeadroomParams params2 = new GpuHeadroomParams.Builder() + .setCalculationWindowMillis(100) + .build(); + final GpuHeadroomParamsInternal internalParams2 = new GpuHeadroomParamsInternal(); + internalParams2.calculationWindowMillis = 100; + final GpuHeadroomParams params3 = new GpuHeadroomParams.Builder() + .setCalculationType(GpuHeadroomParams.GPU_HEADROOM_CALCULATION_TYPE_AVERAGE) + .build(); + final GpuHeadroomParamsInternal internalParams3 = new GpuHeadroomParamsInternal(); + internalParams3.calculationType = + (byte) GpuHeadroomParams.GPU_HEADROOM_CALCULATION_TYPE_AVERAGE; + + when(mHintManager.getGpuHeadroom(internalParams1)).thenReturn( + GpuHeadroomResult.globalHeadroom(99f)); + when(mHintManager.getGpuHeadroom(internalParams2)).thenReturn( + GpuHeadroomResult.globalHeadroom(98f)); + when(mHintManager.getGpuHeadroom(internalParams3)).thenReturn(null); + + assertEquals(99f, mSystemHealthManager.getGpuHeadroom(params1), 0.001f); + assertEquals(98f, mSystemHealthManager.getGpuHeadroom(params2), 0.001f); + assertTrue(Float.isNaN(mSystemHealthManager.getGpuHeadroom(params3))); + verify(mHintManager, times(1)).getGpuHeadroom(internalParams1); + verify(mHintManager, times(1)).getGpuHeadroom(internalParams2); + verify(mHintManager, times(1)).getGpuHeadroom(internalParams3); + } +} diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java index d26bb35e5481..9818e19cea02 100644 --- a/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/NotificationProgressBarTest.java @@ -20,12 +20,16 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Notification.ProgressStyle; import android.graphics.Color; +import android.util.Pair; import androidx.test.ext.junit.runners.AndroidJUnit4; -import com.android.internal.widget.NotificationProgressDrawable.Part; -import com.android.internal.widget.NotificationProgressDrawable.Point; -import com.android.internal.widget.NotificationProgressDrawable.Segment; +import com.android.internal.widget.NotificationProgressBar.Part; +import com.android.internal.widget.NotificationProgressBar.Point; +import com.android.internal.widget.NotificationProgressBar.Segment; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePart; +import com.android.internal.widget.NotificationProgressDrawable.DrawablePoint; +import com.android.internal.widget.NotificationProgressDrawable.DrawableSegment; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,183 +41,287 @@ import java.util.List; public class NotificationProgressBarTest { @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsIsEmpty() { + public void processAndConvertToParts_segmentsIsEmpty() { List<ProgressStyle.Segment> segments = new ArrayList<>(); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentsLengthNotMatchingProgressMax() { + public void processAndConvertToParts_segmentsLengthNotMatchingProgressMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsNegative() { + public void processAndConvertToParts_segmentLengthIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(-50)); segments.add(new ProgressStyle.Segment(150)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_segmentLengthIsZero() { + public void processAndConvertToParts_segmentLengthIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(0)); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressIsNegative() { + public void processAndConvertToParts_progressIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = -50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_progressIsZero() { + public void processAndConvertToParts_progressIsZero() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 0; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedRed = 0x66FF0000; + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, fadedRed, true))); - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, fadedRed, true))); - - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(0); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_progressAtMax() { + public void processAndConvertToParts_progressAtMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.RED)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 100; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; - List<Part> expected = new ArrayList<>(List.of(new Segment(1f, Color.RED))); + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); - assertThat(parts).isEqualTo(expected); + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 300, Color.RED))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + assertThat(p.second).isEqualTo(300); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_progressAboveMax() { + public void processAndConvertToParts_progressAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 150; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionIsNegative() { + public void processAndConvertToParts_pointPositionIsNegative() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(-50).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test(expected = IllegalArgumentException.class) - public void processAndConvertToDrawableParts_pointPositionAboveMax() { + public void processAndConvertToParts_pointPositionAboveMax() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100)); List<ProgressStyle.Point> points = new ArrayList<>(); points.add(new ProgressStyle.Point(150).setColor(Color.RED)); int progress = 50; int progressMax = 100; - boolean isStyledByProgress = true; - NotificationProgressBar.processAndConvertToDrawableParts(segments, points, progress, - progressMax, - isStyledByProgress); + NotificationProgressBar.processAndConvertToViewParts(segments, points, progress, + progressMax); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithoutPoints() { + public void processAndConvertToParts_multipleSegmentsWithoutPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); List<ProgressStyle.Point> points = new ArrayList<>(); int progress = 60; int progressMax = 100; - boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 180, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.50f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Segment(0.40f, fadedGreen, true))); + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void processAndConvertToParts_multipleSegmentsWithoutPoints_noTracker() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + int progress = 60; + int progressMax = 100; + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.50f, Color.RED), new Segment(0.50f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = false; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + expectedDrawableParts = new ArrayList<>(List.of(new DrawableSegment(0, 146, Color.RED), + new DrawableSegment(150, 176, Color.GREEN), + new DrawableSegment(180, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_singleSegmentWithPoints() { + public void processAndConvertToParts_singleSegmentWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); List<ProgressStyle.Point> points = new ArrayList<>(); @@ -223,31 +331,72 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.BLUE), + new Point(Color.RED), + new Segment(0.10f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.35f, Color.BLUE), + new Point(Color.BLUE), + new Segment(0.15f, Color.BLUE), + new Point(Color.YELLOW), + new Segment(0.25f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.BLUE), + new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.BLUE), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 170, Color.BLUE), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.BLUE), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + // Colors with 40% opacity int fadedBlue = 0x660000FF; int fadedYellow = 0x66FFFF00; - - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.BLUE), - new Point(null, Color.RED), - new Segment(0.10f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.35f, Color.BLUE), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedBlue, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedBlue, true))); - - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); - - assertThat(parts).isEqualTo(expected); + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.219177F, Color.BLUE), + new DrawablePoint(38.219177F, 50.219177F, Color.RED), + new DrawableSegment(54.219177F, 70.21918F, Color.BLUE), + new DrawablePoint(74.21918F, 86.21918F, Color.BLUE), + new DrawableSegment(90.21918F, 172.38356F, Color.BLUE), + new DrawablePoint(176.38356F, 188.38356F, Color.BLUE), + new DrawableSegment(192.38356F, 217.0137F, fadedBlue, true), + new DrawablePoint(221.0137F, 233.0137F, fadedYellow), + new DrawableSegment(237.0137F, 300F, fadedBlue, true))); + + assertThat(p.second).isEqualTo(182.38356F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints() { + public void processAndConvertToParts_multipleSegmentsWithPoints() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -258,32 +407,225 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), + new Point(Color.RED), + new Segment(0.10f, Color.RED), + new Point(Color.BLUE), + new Segment(0.25f, Color.RED), + new Segment(0.10f, Color.GREEN), + new Point(Color.BLUE), + new Segment(0.15f, Color.GREEN), + new Point(Color.YELLOW), + new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = true; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); // Colors with 40% opacity int fadedGreen = 0x6600FF00; int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.095238F, Color.RED), + new DrawablePoint(38.095238F, 50.095238F, Color.RED), + new DrawableSegment(54.095238F, 70.09524F, Color.RED), + new DrawablePoint(74.09524F, 86.09524F, Color.BLUE), + new DrawableSegment(90.09524F, 148.9524F, Color.RED), + new DrawableSegment(152.95238F, 172.7619F, Color.GREEN), + new DrawablePoint(176.7619F, 188.7619F, Color.BLUE), + new DrawableSegment(192.7619F, 217.33333F, fadedGreen, true), + new DrawablePoint(221.33333F, 233.33333F, fadedYellow), + new DrawableSegment(237.33333F, 299.99997F, fadedGreen, true))); + + assertThat(p.second).isEqualTo(182.7619F); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(null, Color.RED), - new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.10f, Color.GREEN), - new Point(null, Color.BLUE), - new Segment(0.15f, fadedGreen, true), - new Point(null, fadedYellow, true), - new Segment(0.25f, fadedGreen, true))); - - assertThat(parts).isEqualTo(expected); + @Test + public void processAndConvertToParts_multipleSegmentsWithPointsAtStartAndEnd() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.RED)); + points.add(new ProgressStyle.Point(25).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(60).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(100).setColor(Color.YELLOW)); + int progress = 60; + int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.RED), + new Segment(0.25f, Color.RED), + new Point(Color.BLUE), + new Segment(0.25f, Color.RED), + new Segment(0.10f, Color.GREEN), + new Point(Color.BLUE), + new Segment(0.4f, Color.GREEN), + new Point(Color.YELLOW))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.RED), + new DrawableSegment(16, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 284, Color.GREEN), + new DrawablePoint(288, 300, Color.YELLOW))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.RED), + new DrawableSegment(16, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 284, fadedGreen, true), + new DrawablePoint(288, 300, fadedYellow))); + + assertThat(p.second).isEqualTo(180); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The points are so close to start/end that they would go out of bounds without the minimum + // segment width requirement. + @Test + public void processAndConvertToParts_multipleSegmentsWithPointsNearStartAndEnd() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); + segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(1).setColor(Color.RED)); + points.add(new ProgressStyle.Point(25).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(60).setColor(Color.BLUE)); + points.add(new ProgressStyle.Point(99).setColor(Color.YELLOW)); + int progress = 60; + int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.01f, Color.RED), + new Point(Color.RED), + new Segment(0.24f, Color.RED), + new Point(Color.BLUE), + new Segment(0.25f, Color.RED), + new Segment(0.10f, Color.GREEN), + new Point(Color.BLUE), + new Segment(0.39f, Color.GREEN), + new Point(Color.YELLOW), + new Segment(0.01f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, -7, Color.RED), + new DrawablePoint(-3, 9, Color.RED), + new DrawableSegment(13, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 170, Color.GREEN), + new DrawablePoint(174, 186, Color.BLUE), + new DrawableSegment(190, 287, Color.GREEN), + new DrawablePoint(291, 303, Color.YELLOW), + new DrawableSegment(307, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + // Colors with 40% opacity + int fadedGreen = 0x6600FF00; + int fadedYellow = 0x66FFFF00; + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 16, Color.RED), + new DrawablePoint(20, 32, Color.RED), + new DrawableSegment(36, 78.02409F, Color.RED), + new DrawablePoint(82.02409F, 94.02409F, Color.BLUE), + new DrawableSegment(98.02409F, 146.55421F, Color.RED), + new DrawableSegment(150.55421F, 169.44579F, Color.GREEN), + new DrawablePoint(173.44579F, 185.44579F, Color.BLUE), + new DrawableSegment(189.44579F, 264, fadedGreen, true), + new DrawablePoint(268, 280, fadedYellow), + new DrawableSegment(284, 300, fadedGreen, true))); + + assertThat(p.second).isEqualTo(179.44579F); + assertThat(p.first).isEqualTo(expectedDrawableParts); } @Test - public void processAndConvertToDrawableParts_multipleSegmentsWithPoints_notStyledByProgress() { + public void processAndConvertToParts_multipleSegmentsWithPoints_notStyledByProgress() { List<ProgressStyle.Segment> segments = new ArrayList<>(); segments.add(new ProgressStyle.Segment(50).setColor(Color.RED)); segments.add(new ProgressStyle.Segment(50).setColor(Color.GREEN)); @@ -293,21 +635,223 @@ public class NotificationProgressBarTest { points.add(new ProgressStyle.Point(75).setColor(Color.YELLOW)); int progress = 60; int progressMax = 100; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Segment(0.15f, Color.RED), new Point(Color.RED), + new Segment(0.10f, Color.RED), new Point(Color.BLUE), + new Segment(0.25f, Color.RED), new Segment(0.25f, Color.GREEN), + new Point(Color.YELLOW), new Segment(0.25f, Color.GREEN))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 300; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 35, Color.RED), new DrawablePoint(39, 51, Color.RED), + new DrawableSegment(55, 65, Color.RED), + new DrawablePoint(69, 81, Color.BLUE), + new DrawableSegment(85, 146, Color.RED), + new DrawableSegment(150, 215, Color.GREEN), + new DrawablePoint(219, 231, Color.YELLOW), + new DrawableSegment(235, 300, Color.GREEN))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; boolean isStyledByProgress = false; - List<Part> parts = NotificationProgressBar.processAndConvertToDrawableParts( - segments, points, progress, progressMax, isStyledByProgress); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 300, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>( + List.of(new DrawableSegment(0, 34.296295F, Color.RED), + new DrawablePoint(38.296295F, 50.296295F, Color.RED), + new DrawableSegment(54.296295F, 70.296295F, Color.RED), + new DrawablePoint(74.296295F, 86.296295F, Color.BLUE), + new DrawableSegment(90.296295F, 149.62962F, Color.RED), + new DrawableSegment(153.62962F, 216.8148F, Color.GREEN), + new DrawablePoint(220.81482F, 232.81482F, Color.YELLOW), + new DrawableSegment(236.81482F, 300, Color.GREEN))); + + assertThat(p.second).isEqualTo(182.9037F); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `zeroWidthDrawableSegment` test below is the longer + // segmentMinWidth (= 16dp). + @Test + public void maybeStretchAndRescaleSegments_negativeWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 16; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 32, Color.BLUE), + new DrawableSegment(36, 69.41936F, Color.BLUE), + new DrawableSegment(73.41936F, 124.25807F, Color.BLUE), + new DrawableSegment(128.25807F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + // The only difference from the `negativeWidthDrawableSegment` test above is the shorter + // segmentMinWidth (= 10dp). + @Test + public void maybeStretchAndRescaleSegments_zeroWidthDrawableSegment() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.1f, Color.BLUE), + new Segment(0.2f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 16, Color.BLUE), + new DrawableSegment(20, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; + + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); + + expectedDrawableParts = new ArrayList<>(List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 26, Color.BLUE), + new DrawableSegment(30, 64.169014F, Color.BLUE), + new DrawableSegment(68.169014F, 120.92958F, Color.BLUE), + new DrawableSegment(124.92958F, 200, Color.BLUE))); + + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); + } + + @Test + public void maybeStretchAndRescaleSegments_noStretchingNecessary() { + List<ProgressStyle.Segment> segments = new ArrayList<>(); + segments.add(new ProgressStyle.Segment(200).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(100).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(300).setColor(Color.BLUE)); + segments.add(new ProgressStyle.Segment(400).setColor(Color.BLUE)); + List<ProgressStyle.Point> points = new ArrayList<>(); + points.add(new ProgressStyle.Point(0).setColor(Color.BLUE)); + int progress = 1000; + int progressMax = 1000; + + List<Part> parts = NotificationProgressBar.processAndConvertToViewParts(segments, points, + progress, progressMax); + + List<Part> expectedParts = new ArrayList<>( + List.of(new Point(Color.BLUE), new Segment(0.2f, Color.BLUE), + new Segment(0.1f, Color.BLUE), new Segment(0.3f, Color.BLUE), + new Segment(0.4f, Color.BLUE))); + + assertThat(parts).isEqualTo(expectedParts); + + float drawableWidth = 200; + float segSegGap = 4; + float segPointGap = 4; + float pointRadius = 6; + boolean hasTrackerIcon = true; + + List<DrawablePart> drawableParts = NotificationProgressBar.processAndConvertToDrawableParts( + parts, drawableWidth, segSegGap, segPointGap, pointRadius, hasTrackerIcon); + + List<DrawablePart> expectedDrawableParts = new ArrayList<>( + List.of(new DrawablePoint(0, 12, Color.BLUE), + new DrawableSegment(16, 36, Color.BLUE), + new DrawableSegment(40, 56, Color.BLUE), + new DrawableSegment(60, 116, Color.BLUE), + new DrawableSegment(120, 200, Color.BLUE))); + + assertThat(drawableParts).isEqualTo(expectedDrawableParts); + + float segmentMinWidth = 10; + boolean isStyledByProgress = true; - List<Part> expected = new ArrayList<>(List.of( - new Segment(0.15f, Color.RED), - new Point(null, Color.RED), - new Segment(0.10f, Color.RED), - new Point(null, Color.BLUE), - new Segment(0.25f, Color.RED), - new Segment(0.25f, Color.GREEN), - new Point(null, Color.YELLOW), - new Segment(0.25f, Color.GREEN))); + Pair<List<DrawablePart>, Float> p = NotificationProgressBar.maybeStretchAndRescaleSegments( + parts, drawableParts, segmentMinWidth, pointRadius, (float) progress / progressMax, + 200, isStyledByProgress, hasTrackerIcon ? 0 : segSegGap); - assertThat(parts).isEqualTo(expected); + assertThat(p.second).isEqualTo(200); + assertThat(p.first).isEqualTo(expectedDrawableParts); } } diff --git a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java index e368d2815855..cb8b5ce245b6 100644 --- a/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java +++ b/core/tests/timetests/src/android/app/time/TimeZoneCapabilitiesTest.java @@ -48,12 +48,14 @@ public class TimeZoneCapabilitiesTest { .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); TimeZoneCapabilities.Builder builder2 = new TimeZoneCapabilities.Builder(TEST_USER_HANDLE) .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); { TimeZoneCapabilities one = builder1.build(); TimeZoneCapabilities two = builder2.build(); @@ -115,6 +117,13 @@ public class TimeZoneCapabilitiesTest { TimeZoneCapabilities two = builder2.build(); assertEquals(one, two); } + + builder1.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED); + { + TimeZoneCapabilities one = builder1.build(); + TimeZoneCapabilities two = builder2.build(); + assertNotEquals(one, two); + } } @Test @@ -123,7 +132,8 @@ public class TimeZoneCapabilitiesTest { .setConfigureAutoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) - .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED); + .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED); assertRoundTripParcelable(builder.build()); builder.setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED); @@ -137,6 +147,9 @@ public class TimeZoneCapabilitiesTest { builder.setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED); assertRoundTripParcelable(builder.build()); + + builder.setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED); + assertRoundTripParcelable(builder.build()); } @Test @@ -151,6 +164,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder() @@ -175,6 +189,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); TimeZoneConfiguration configChange = new TimeZoneConfiguration.Builder() @@ -191,6 +206,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); { @@ -204,6 +220,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -221,6 +238,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(false) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -238,6 +256,7 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_POSSESSED) .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); @@ -255,6 +274,25 @@ public class TimeZoneCapabilitiesTest { .setUseLocationEnabled(true) .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) .setSetManualTimeZoneCapability(CAPABILITY_POSSESSED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_NOT_ALLOWED) + .build(); + + assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); + } + + { + TimeZoneCapabilities updatedCapabilities = + new TimeZoneCapabilities.Builder(capabilities) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) + .build(); + + TimeZoneCapabilities expectedCapabilities = + new TimeZoneCapabilities.Builder(TEST_USER_HANDLE) + .setConfigureAutoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) + .setUseLocationEnabled(true) + .setConfigureGeoDetectionEnabledCapability(CAPABILITY_NOT_ALLOWED) + .setSetManualTimeZoneCapability(CAPABILITY_NOT_ALLOWED) + .setConfigureNotificationsEnabledCapability(CAPABILITY_POSSESSED) .build(); assertThat(updatedCapabilities).isEqualTo(expectedCapabilities); diff --git a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java index 4ad3e41383aa..345e91268253 100644 --- a/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java +++ b/core/tests/timetests/src/android/app/time/TimeZoneConfigurationTest.java @@ -43,9 +43,11 @@ public class TimeZoneConfigurationTest { TimeZoneConfiguration completeConfig = new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(true) .setGeoDetectionEnabled(true) + .setNotificationsEnabled(true) .build(); assertTrue(completeConfig.isComplete()); assertTrue(completeConfig.hasIsGeoDetectionEnabled()); + assertTrue(completeConfig.hasIsNotificationsEnabled()); } @Test diff --git a/data/etc/preinstalled-packages-platform.xml b/data/etc/preinstalled-packages-platform.xml index 782327713fdc..3403bbfa2384 100644 --- a/data/etc/preinstalled-packages-platform.xml +++ b/data/etc/preinstalled-packages-platform.xml @@ -134,4 +134,9 @@ to pre-existing users, but cannot uninstall pre-existing system packages from pr <install-in-user-type package="com.android.avatarpicker"> <install-in user-type="FULL" /> </install-in-user-type> + + <!-- Users Widget (Users widget)--> + <install-in-user-type package="com.android.multiuser"> + <install-in user-type="FULL" /> + </install-in-user-type> </config> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index f136e065a405..a30570a4cce5 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -264,6 +264,7 @@ applications that come with the platform <!-- Needed for test only --> <permission name="android.permission.BATTERY_PREDICTION"/> <permission name="android.permission.BATTERY_STATS"/> + <permission name="android.permission.ACCESS_FINE_POWER_MONITORS" /> <!-- BLUETOOTH_PRIVILEGED is needed for test only --> <permission name="android.permission.BLUETOOTH_PRIVILEGED"/> <permission name="android.permission.BIND_APPWIDGET"/> diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 50c95a9fa882..b332cf0d751f 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -16,9 +16,10 @@ package android.graphics; +import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; -import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; +import static com.android.text.flags.Flags.FLAG_TYPEFACE_REDESIGN_READONLY; import static com.android.text.flags.Flags.FLAG_VERTICAL_TEXT_LAYOUT; import android.annotation.ColorInt; @@ -34,7 +35,6 @@ import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; -import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; import android.graphics.text.TextRunShaper; import android.os.Build; @@ -58,6 +58,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Objects; @@ -97,6 +98,7 @@ public class Paint { private LocaleList mLocales; private String mFontFeatureSettings; private String mFontVariationSettings; + private String mFontVariationOverride; private float mShadowLayerRadius; private float mShadowLayerDx; @@ -2100,14 +2102,6 @@ public class Paint { } /** - * A change ID for new font variation settings management. - * @hide - */ - @ChangeId - @EnabledSince(targetSdkVersion = 36) - public static final long NEW_FONT_VARIATION_MANAGEMENT = 361260253L; - - /** * Sets TrueType or OpenType font variation settings. The settings string is constructed from * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that @@ -2136,16 +2130,12 @@ public class Paint { * </li> * </ul> * - * <p>Note: If the application that targets API 35 or before, this function mutates the - * underlying typeface instance. - * * @param fontVariationSettings font variation settings. You can pass null or empty string as * no variation settings. * - * @return If the application that targets API 36 or later and is running on devices API 36 or - * later, this function always returns true. Otherwise, this function returns true if - * the given settings is effective to at least one font file underlying this typeface. - * This function also returns true for empty settings string. Otherwise returns false. + * @return true if the given settings is effective to at least one font file underlying this + * typeface. This function also returns true for empty settings string. Otherwise + * returns false * * @throws IllegalArgumentException If given string is not a valid font variation settings * format @@ -2153,40 +2143,13 @@ public class Paint { * @see #getFontVariationSettings() * @see FontVariationAxis */ + // Add following API description once the setFontVariationOverride becomes public. + // This method generates new variation instance of the {@link Typeface} instance and set it to + // this object. Therefore, subsequent {@link #setTypeface(Typeface)} call will clear the font + // variation settings. Also, creating variation instance of the Typeface requires non trivial + // amount of time and memories, therefore consider using + // {@link #setFontVariationOverride(String, int)} for better performance. public boolean setFontVariationSettings(String fontVariationSettings) { - return setFontVariationSettings(fontVariationSettings, 0 /* wght adjust */); - } - - /** - * Set font variation settings with weight adjustment - * @hide - */ - public boolean setFontVariationSettings(String fontVariationSettings, int wghtAdjust) { - final boolean useFontVariationStore = Flags.typefaceRedesignReadonly() - && CompatChanges.isChangeEnabled(NEW_FONT_VARIATION_MANAGEMENT); - if (useFontVariationStore) { - FontVariationAxis[] axes = - FontVariationAxis.fromFontVariationSettings(fontVariationSettings); - if (axes == null) { - nSetFontVariationOverride(mNativePaint, 0); - mFontVariationSettings = null; - return true; - } - - long builderPtr = nCreateFontVariationBuilder(axes.length); - for (int i = 0; i < axes.length; ++i) { - int tag = axes[i].getOpenTypeTagValue(); - float value = axes[i].getStyleValue(); - if (tag == 0x77676874 /* wght */) { - value = Math.clamp(value + wghtAdjust, - FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX); - } - nAddFontVariationToBuilder(builderPtr, tag, value); - } - nSetFontVariationOverride(mNativePaint, builderPtr); - mFontVariationSettings = fontVariationSettings; - return true; - } final String settings = TextUtils.nullIfEmpty(fontVariationSettings); if (settings == mFontVariationSettings || (settings != null && settings.equals(mFontVariationSettings))) { @@ -2220,6 +2183,68 @@ public class Paint { } /** + * Sets TrueType or OpenType font variation settings for overriding. + * + * The settings string is constructed from multiple pairs of axis tag and style values. The axis + * tag must contain four ASCII characters and must be wrapped with single quotes (U+0027) or + * double quotes (U+0022). Axis strings that are longer or shorter than four characters, or + * contain characters outside of U+0020..U+007E are invalid. + * + * If invalid font variation settings is provided, this method does nothing and returning false + * with printing error message to the logcat. + * + * Different from {@link #setFontVariationSettings(String)}, this overrides the font variation + * settings which is already assigned to the font instance. For example, if the underlying font + * is configured as {@code 'wght' 500, 'ital' 1}, and if the override is specified as + * {@code 'wght' 700, `wdth` 150}, then the effective font variation setting is + * {@code `wght' 700, 'ital' 1, 'wdth' 150}. The `wght` value is updated by override, 'ital' + * value is preserved because no overrides, and `wdth` value is added by override. + * + * @param fontVariationOverride font variation settings. You can pass null or empty string as + * no variation settings. + * + * @return true if the provided font variation settings is valid. Otherwise returns false. + * + * @see #getFontVariationSettings() + * @see #setFontVariationSettings(String) + * @see #getFontVariationOverride() + * @see FontVariationAxis + */ + @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY) + public boolean setFontVariationOverride(@Nullable String fontVariationOverride) { + if (Objects.equals(fontVariationOverride, mFontVariationOverride)) { + return true; + } + + List<FontVariationAxis> axes; + try { + axes = FontVariationAxis.fromFontVariationSettingsForList(fontVariationOverride); + } catch (IllegalArgumentException e) { + Log.i(TAG, "failed to parse font variation settings.", e); + return false; + } + long builderPtr = nCreateFontVariationBuilder(axes.size()); + for (int i = 0; i < axes.size(); ++i) { + FontVariationAxis axis = axes.get(i); + nAddFontVariationToBuilder( + builderPtr, axis.getOpenTypeTagValue(), axis.getStyleValue()); + } + nSetFontVariationOverride(mNativePaint, builderPtr); + mFontVariationOverride = fontVariationOverride; + return true; + } + + /** + * Gets the current font variation override value. + * + * @return a current font variation override value. + */ + @FlaggedApi(FLAG_TYPEFACE_REDESIGN_READONLY) + public @Nullable String getFontVariationOverride() { + return mFontVariationOverride; + } + + /** * Get the current value of start hyphen edit. * * The default value is 0 which is equivalent to {@link #START_HYPHEN_EDIT_NO_EDIT}. diff --git a/graphics/java/android/graphics/fonts/FontVariationAxis.java b/graphics/java/android/graphics/fonts/FontVariationAxis.java index d1fe2cdbcd77..30a248bb3e0e 100644 --- a/graphics/java/android/graphics/fonts/FontVariationAxis.java +++ b/graphics/java/android/graphics/fonts/FontVariationAxis.java @@ -23,6 +23,7 @@ import android.os.Build; import android.text.TextUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -139,9 +140,19 @@ public final class FontVariationAxis { */ public static @Nullable FontVariationAxis[] fromFontVariationSettings( @Nullable String settings) { - if (settings == null || settings.isEmpty()) { + List<FontVariationAxis> result = fromFontVariationSettingsForList(settings); + if (result.isEmpty()) { return null; } + return result.toArray(new FontVariationAxis[0]); + } + + /** @hide */ + public static @NonNull List<FontVariationAxis> fromFontVariationSettingsForList( + @Nullable String settings) { + if (settings == null || settings.isEmpty()) { + return Collections.emptyList(); + } final ArrayList<FontVariationAxis> axisList = new ArrayList<>(); final int length = settings.length(); for (int i = 0; i < length; i++) { @@ -172,9 +183,9 @@ public final class FontVariationAxis { i = endOfValueString; } if (axisList.isEmpty()) { - return null; + return Collections.emptyList(); } - return axisList.toArray(new FontVariationAxis[0]); + return axisList; } /** diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java index 740ccb53a691..13f1a72469c2 100644 --- a/keystore/java/android/security/keystore/KeyStoreManager.java +++ b/keystore/java/android/security/keystore/KeyStoreManager.java @@ -312,9 +312,11 @@ public final class KeyStoreManager { * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with - * the tag in attestations. + * the tag in attestations. To ensure the returned encoded structure is the one attested to, + * clients should verify its SHA-256 hash matches the one in the attestation. Note that the + * returned structure can vary between boots. */ - // TODO(b/369375199): Replace with Tag.MODULE_HASH when flagging is removed. + // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen. public static final int MODULE_HASH = TagType.BYTES | 724; /** diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index b2ac640a468d..4f1cd9780f8b 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -26,6 +26,7 @@ <uses-permission android:name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> <uses-permission android:name="android.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION" /> <uses-permission android:name="android.permission.MANAGE_KEY_GESTURES" /> + <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> <application> <activity diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 755f472ee22e..2fed1380b635 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -233,6 +233,16 @@ public class DesktopModeStatus { } /** + * Returns whether the multiple desktops feature is enabled for this device (both backend and + * frontend implementations). + */ + public static boolean enableMultipleDesktops(@NonNull Context context) { + return Flags.enableMultipleDesktopsBackend() + && Flags.enableMultipleDesktopsFrontend() + && canEnterDesktopMode(context); + } + + /** * @return {@code true} if this device is requesting to show the app handle despite non * necessarily enabling desktop mode */ diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt index d15fbed409b8..23498de72481 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeTransitionSource.kt @@ -32,9 +32,7 @@ enum class DesktopModeTransitionSource : Parcelable { /** Transitions with source unknown. */ UNKNOWN; - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeString(name) @@ -44,9 +42,8 @@ enum class DesktopModeTransitionSource : Parcelable { @JvmField val CREATOR = object : Parcelable.Creator<DesktopModeTransitionSource> { - override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource { - return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN - } + override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource = + parcel.readString()?.let { valueOf(it) } ?: UNKNOWN override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java new file mode 100644 index 000000000000..5018fdb615da --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/SizeChangeAnimation.java @@ -0,0 +1,288 @@ +/* + * 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.wm.shell.animation; + +import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.ClipRectAnimation; +import android.view.animation.ScaleAnimation; +import android.view.animation.Transformation; +import android.view.animation.TranslateAnimation; + +import java.util.function.Consumer; + +/** + * Animation implementation for size-changing window container animations. Ported from + * {@link com.android.server.wm.WindowChangeAnimationSpec}. + * <p> + * This animation behaves slightly differently depending on whether the window is growing + * or shrinking: + * <ul> + * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old) + * snapshot. + * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker + * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into + * place. + * </ul> + */ +public class SizeChangeAnimation { + private final Rect mTmpRect = new Rect(); + final Transformation mTmpTransform = new Transformation(); + final Matrix mTmpMatrix = new Matrix(); + final float[] mTmpFloats = new float[9]; + final float[] mTmpVecs = new float[4]; + + private final Animation mAnimation; + private final Animation mSnapshotAnim; + + private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f); + + /** + * The maximum of stretching applied to any surface during interpolation (since the animation + * is a combination of stretching/cropping/fading). + */ + private static final float SCALE_FACTOR = 0.7f; + + /** + * Since this animation is made of several sub-animations, we want to pre-arrange the + * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step. + * + * To do this, we have a single value-animator which animates progress from 0-1 with an + * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual + * timeline to get the interpolated transforms. + * + * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick + * an integral "duration" for our virtual timeline. That's what this constant specifies. It + * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space. + */ + private static final int ANIMATION_RESOLUTION = 1000; + + public SizeChangeAnimation(Rect startBounds, Rect endBounds) { + mAnimation = buildContainerAnimation(startBounds, endBounds); + mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds); + } + + /** + * Initialize a size-change animation for a container leash. + */ + public void initialize(SurfaceControl leash, SurfaceControl snapshot, + SurfaceControl.Transaction startT) { + startT.reparent(snapshot, leash); + startT.setPosition(snapshot, 0, 0); + startT.show(snapshot); + startT.show(leash); + apply(startT, leash, snapshot, 0.f); + } + + /** + * Initialize a size-change animation for a view containing the leash surface(s). + * + * Note that this **will** apply {@param startToApply}! + */ + public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot, + SurfaceControl.Transaction startToApply) { + startToApply.reparent(snapshot, leash); + startToApply.setPosition(snapshot, 0, 0); + startToApply.show(snapshot); + startToApply.show(leash); + apply(view, startToApply, leash, snapshot, 0.f); + } + + private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater, + SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish, + SurfaceControl.Transaction transaction, @Nullable View view) { + return setupValueAnimator(mAnimator, updater, (anim) -> { + transaction.reparent(snapshot, null); + if (view != null) { + view.setClipBounds(null); + view.setAnimationMatrix(null); + transaction.setCrop(leash, null); + } + transaction.apply(); + transaction.close(); + onFinish.accept(anim); + }); + } + + /** + * Build an animator which works on a pair of surface controls (where the snapshot is assumed + * to be a child of the main leash). + * + * @param onFinish Called when animation finishes. This is called on the anim thread! + */ + public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot, + Consumer<Animator> onFinish) { + final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + Choreographer choreographer = Choreographer.getInstance(); + return buildAnimatorInner(animator -> { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f); + apply(transaction, leash, snapshot, progress); + transaction.setFrameTimelineVsync(choreographer.getVsyncId()); + transaction.apply(); + }, leash, snapshot, onFinish, transaction, null /* view */); + } + + /** + * Build an animator which works on a view that contains a pair of surface controls (where + * the snapshot is assumed to be a child of the main leash). + * + * @param onFinish Called when animation finishes. This is called on the anim thread! + */ + public ValueAnimator buildViewAnimator(View view, SurfaceControl leash, + SurfaceControl snapshot, Consumer<Animator> onFinish) { + final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + return buildAnimatorInner(animator -> { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f); + apply(view, transaction, leash, snapshot, progress); + }, leash, snapshot, onFinish, transaction, view); + } + + /** Animation for the whole container (snapshot is inside this container). */ + private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) { + final long duration = ANIMATION_RESOLUTION; + boolean growing = endBounds.width() - startBounds.width() + + endBounds.height() - startBounds.height() >= 0; + long scalePeriod = (long) (duration * SCALE_FACTOR); + float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width() + + (1.f - SCALE_FACTOR); + float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height() + + (1.f - SCALE_FACTOR); + final AnimationSet animSet = new AnimationSet(true); + + final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1); + scaleAnim.setDuration(scalePeriod); + if (!growing) { + scaleAnim.setStartOffset(duration - scalePeriod); + } + animSet.addAnimation(scaleAnim); + final Animation translateAnim = new TranslateAnimation(startBounds.left, + endBounds.left, startBounds.top, endBounds.top); + translateAnim.setDuration(duration); + animSet.addAnimation(translateAnim); + Rect startClip = new Rect(startBounds); + Rect endClip = new Rect(endBounds); + startClip.offsetTo(0, 0); + endClip.offsetTo(0, 0); + final Animation clipAnim = new ClipRectAnimation(startClip, endClip); + clipAnim.setDuration(duration); + animSet.addAnimation(clipAnim); + animSet.initialize(startBounds.width(), startBounds.height(), + endBounds.width(), endBounds.height()); + return animSet; + } + + /** The snapshot surface is assumed to be a child of the container surface. */ + private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) { + final long duration = ANIMATION_RESOLUTION; + boolean growing = endBounds.width() - startBounds.width() + + endBounds.height() - startBounds.height() >= 0; + long scalePeriod = (long) (duration * SCALE_FACTOR); + float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width() + + (1.f - SCALE_FACTOR)); + float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height() + + (1.f - SCALE_FACTOR)); + + AnimationSet snapAnimSet = new AnimationSet(true); + // Animation for the "old-state" snapshot that is atop the task. + final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f); + snapAlphaAnim.setDuration(scalePeriod); + if (!growing) { + snapAlphaAnim.setStartOffset(duration - scalePeriod); + } + snapAnimSet.addAnimation(snapAlphaAnim); + final Animation snapScaleAnim = + new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY); + snapScaleAnim.setDuration(duration); + snapAnimSet.addAnimation(snapScaleAnim); + snapAnimSet.initialize(startBounds.width(), startBounds.height(), + endBounds.width(), endBounds.height()); + return snapAnimSet; + } + + private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) { + // The following applies an inverse scale to the clip-rect so that it crops "after" the + // scale instead of before. + mTmpVecs[1] = mTmpVecs[2] = 0; + mTmpVecs[0] = mTmpVecs[3] = 1; + fromTransform.getMatrix().mapVectors(mTmpVecs); + + mTmpVecs[0] = 1.f / mTmpVecs[0]; + mTmpVecs[3] = 1.f / mTmpVecs[3]; + final Rect clipRect = fromTransform.getClipRect(); + outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f); + outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f); + outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f); + outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f); + } + + private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot, + float progress) { + long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress); + // update thumbnail surface + mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform); + t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats); + t.setAlpha(snapshot, mTmpTransform.getAlpha()); + + // update container surface + mAnimation.getTransformation(currentPlayTime, mTmpTransform); + final Matrix matrix = mTmpTransform.getMatrix(); + t.setMatrix(leash, matrix, mTmpFloats); + + calcCurrentClipBounds(mTmpRect, mTmpTransform); + t.setCrop(leash, mTmpRect); + } + + private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash, + SurfaceControl snapshot, float progress) { + long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress); + // update thumbnail surface + mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform); + tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats); + tmpT.setAlpha(snapshot, mTmpTransform.getAlpha()); + + // update container surface + mAnimation.getTransformation(currentPlayTime, mTmpTransform); + final Matrix matrix = mTmpTransform.getMatrix(); + mTmpMatrix.set(matrix); + // animationMatrix is applied after getTranslation, so "move" the translate to the end. + mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY()); + mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY()); + view.setAnimationMatrix(mTmpMatrix); + + calcCurrentClipBounds(mTmpRect, mTmpTransform); + tmpT.setCrop(leash, mTmpRect); + view.setClipBounds(mTmpRect); + + // this takes stuff out of mTmpT so mTmpT can be re-used immediately + view.getViewRootImpl().applyTransactionOnDraw(tmpT); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java new file mode 100644 index 000000000000..9451374befe0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOut.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import com.android.wm.shell.shared.annotations.ExternalThread; + +/** + * Interface to engage with the app zoom out feature. + */ +@ExternalThread +public interface AppZoomOut { + + /** + * Called when the zoom out progress is updated, which is used to scale down the current app + * surface from fullscreen to the max pushback level we want to apply. {@param progress} ranges + * between [0,1], 0 when fullscreen, 1 when it's at the max pushback level. + */ + void setProgress(float progress); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java new file mode 100644 index 000000000000..82ef00e46e8c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.res.Configuration; +import android.util.Slog; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerTransaction; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayChangeController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.annotations.ExternalThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.sysui.ShellInit; + +/** Class that manages the app zoom out UI and states. */ +public class AppZoomOutController implements RemoteCallable<AppZoomOutController>, + ShellTaskOrganizer.FocusListener, DisplayChangeController.OnDisplayChangingListener { + + private static final String TAG = "AppZoomOutController"; + + private final Context mContext; + private final ShellTaskOrganizer mTaskOrganizer; + private final DisplayController mDisplayController; + private final AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer; + private final ShellExecutor mMainExecutor; + private final AppZoomOutImpl mImpl = new AppZoomOutImpl(); + + private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = + new DisplayController.OnDisplaysChangedListener() { + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + updateDisplayLayout(displayId); + } + + @Override + public void onDisplayAdded(int displayId) { + if (displayId != DEFAULT_DISPLAY) { + return; + } + updateDisplayLayout(displayId); + } + }; + + + public static AppZoomOutController create(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, + DisplayLayout displayLayout, @ShellMainThread ShellExecutor mainExecutor) { + AppZoomOutDisplayAreaOrganizer displayAreaOrganizer = new AppZoomOutDisplayAreaOrganizer( + context, displayLayout, mainExecutor); + return new AppZoomOutController(context, shellInit, shellTaskOrganizer, displayController, + displayAreaOrganizer, mainExecutor); + } + + @VisibleForTesting + AppZoomOutController(Context context, ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, DisplayController displayController, + AppZoomOutDisplayAreaOrganizer displayAreaOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + mContext = context; + mTaskOrganizer = shellTaskOrganizer; + mDisplayController = displayController; + mDisplayAreaOrganizer = displayAreaOrganizer; + mMainExecutor = mainExecutor; + + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mTaskOrganizer.addFocusListener(this); + + mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); + mDisplayController.addDisplayChangingController(this); + updateDisplayLayout(mContext.getDisplayId()); + + mDisplayAreaOrganizer.registerOrganizer(); + } + + public AppZoomOut asAppZoomOut() { + return mImpl; + } + + public void setProgress(float progress) { + mDisplayAreaOrganizer.setProgress(progress); + } + + void updateDisplayLayout(int displayId) { + final DisplayLayout newDisplayLayout = mDisplayController.getDisplayLayout(displayId); + if (newDisplayLayout == null) { + Slog.w(TAG, "Failed to get new DisplayLayout."); + return; + } + mDisplayAreaOrganizer.setDisplayLayout(newDisplayLayout); + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (taskInfo == null) { + return; + } + if (taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_HOME) { + mDisplayAreaOrganizer.setIsHomeTaskFocused(taskInfo.isFocused); + } + } + + @Override + public void onDisplayChange(int displayId, int fromRotation, int toRotation, + @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { + // TODO: verify if there is synchronization issues. + if (toRotation != ROTATION_UNDEFINED) { + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + } + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + @ExternalThread + private class AppZoomOutImpl implements AppZoomOut { + @Override + public void setProgress(float progress) { + mMainExecutor.execute(() -> AppZoomOutController.this.setProgress(progress)); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java new file mode 100644 index 000000000000..1c37461b2d2b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutDisplayAreaOrganizer.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.ArrayMap; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; +import android.window.WindowContainerToken; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayLayout; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; + +/** Display area organizer that manages the app zoom out UI and states. */ +public class AppZoomOutDisplayAreaOrganizer extends DisplayAreaOrganizer { + + private static final float PUSHBACK_SCALE_FOR_LAUNCHER = 0.05f; + private static final float PUSHBACK_SCALE_FOR_APP = 0.025f; + private static final float INVALID_PROGRESS = -1; + + private final DisplayLayout mDisplayLayout = new DisplayLayout(); + private final Context mContext; + private final float mCornerRadius; + private final Map<WindowContainerToken, SurfaceControl> mDisplayAreaTokenMap = + new ArrayMap<>(); + + private float mProgress = INVALID_PROGRESS; + // Denote whether the home task is focused, null when it's not yet initialized. + @Nullable private Boolean mIsHomeTaskFocused; + + public AppZoomOutDisplayAreaOrganizer(Context context, + DisplayLayout displayLayout, Executor mainExecutor) { + super(mainExecutor); + mContext = context; + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + setDisplayLayout(displayLayout); + } + + @Override + public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo, SurfaceControl leash) { + leash.setUnreleasedWarningCallSite( + "AppZoomOutDisplayAreaOrganizer.onDisplayAreaAppeared"); + mDisplayAreaTokenMap.put(displayAreaInfo.token, leash); + } + + @Override + public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + final SurfaceControl leash = mDisplayAreaTokenMap.get(displayAreaInfo.token); + if (leash != null) { + leash.release(); + } + mDisplayAreaTokenMap.remove(displayAreaInfo.token); + } + + public void registerOrganizer() { + final List<DisplayAreaAppearedInfo> displayAreaInfos = registerOrganizer( + AppZoomOutDisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT); + for (int i = 0; i < displayAreaInfos.size(); i++) { + final DisplayAreaAppearedInfo info = displayAreaInfos.get(i); + onDisplayAreaAppeared(info.getDisplayAreaInfo(), info.getLeash()); + } + } + + @Override + public void unregisterOrganizer() { + super.unregisterOrganizer(); + reset(); + } + + void setProgress(float progress) { + if (mProgress == progress) { + return; + } + + mProgress = progress; + apply(); + } + + void setIsHomeTaskFocused(boolean isHomeTaskFocused) { + if (mIsHomeTaskFocused != null && mIsHomeTaskFocused == isHomeTaskFocused) { + return; + } + + mIsHomeTaskFocused = isHomeTaskFocused; + apply(); + } + + private void apply() { + if (mIsHomeTaskFocused == null || mProgress == INVALID_PROGRESS) { + return; + } + + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + float scale = mProgress * (mIsHomeTaskFocused + ? PUSHBACK_SCALE_FOR_LAUNCHER : PUSHBACK_SCALE_FOR_APP); + mDisplayAreaTokenMap.forEach((token, leash) -> updateSurface(tx, leash, scale)); + tx.apply(); + } + + void setDisplayLayout(DisplayLayout displayLayout) { + mDisplayLayout.set(displayLayout); + } + + private void reset() { + setProgress(0); + mProgress = INVALID_PROGRESS; + mIsHomeTaskFocused = null; + } + + private void updateSurface(SurfaceControl.Transaction tx, SurfaceControl leash, float scale) { + if (scale == 0) { + // Reset when scale is set back to 0. + tx + .setCrop(leash, null) + .setScale(leash, 1, 1) + .setPosition(leash, 0, 0) + .setCornerRadius(leash, 0); + return; + } + + tx + // Rounded corner can only be applied if a crop is set. + .setCrop(leash, 0, 0, mDisplayLayout.width(), mDisplayLayout.height()) + .setScale(leash, 1 - scale, 1 - scale) + .setPosition(leash, scale * mDisplayLayout.width() * 0.5f, + scale * mDisplayLayout.height() * 0.5f) + .setCornerRadius(leash, mCornerRadius * (1 - scale)); + } + + void onRotateDisplay(Context context, int toRotation) { + if (mDisplayLayout.rotation() == toRotation) { + return; + } + mDisplayLayout.rotateTo(context.getResources(), toRotation); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java index 4569cf31dab1..b9fccc1c4147 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java @@ -106,11 +106,12 @@ public class BackAnimationRunner { private Runnable mFinishedCallback; private RemoteAnimationTarget[] mApps; - private IRemoteAnimationFinishedCallback mRemoteCallback; + private RemoteAnimationFinishedStub mRemoteCallback; private static class RemoteAnimationFinishedStub extends IRemoteAnimationFinishedCallback.Stub { //the binder callback should not hold strong reference to it to avoid memory leak. - private WeakReference<BackAnimationRunner> mRunnerRef; + private final WeakReference<BackAnimationRunner> mRunnerRef; + private boolean mAbandoned; private RemoteAnimationFinishedStub(BackAnimationRunner runner) { mRunnerRef = new WeakReference<>(runner); @@ -118,23 +119,29 @@ public class BackAnimationRunner { @Override public void onAnimationFinished() { - BackAnimationRunner runner = mRunnerRef.get(); + synchronized (this) { + if (mAbandoned) { + return; + } + } + final BackAnimationRunner runner = mRunnerRef.get(); if (runner == null) { return; } - if (runner.shouldMonitorCUJ(runner.mApps)) { - InteractionJankMonitor.getInstance().end(runner.mCujType); - } + runner.onAnimationFinish(this); + } - runner.mFinishedCallback.run(); - for (int i = runner.mApps.length - 1; i >= 0; --i) { - SurfaceControl sc = runner.mApps[i].leash; - if (sc != null && sc.isValid()) { - sc.release(); - } + void abandon() { + synchronized (this) { + mAbandoned = true; + final BackAnimationRunner runner = mRunnerRef.get(); + if (runner == null) { + return; + } + if (runner.shouldMonitorCUJ(runner.mApps)) { + InteractionJankMonitor.getInstance().end(runner.mCujType); + } } - runner.mApps = null; - runner.mFinishedCallback = null; } } @@ -144,13 +151,16 @@ public class BackAnimationRunner { */ void startAnimation(RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback) { - InteractionJankMonitor interactionJankMonitor = InteractionJankMonitor.getInstance(); + if (mRemoteCallback != null) { + mRemoteCallback.abandon(); + mRemoteCallback = null; + } + mRemoteCallback = new RemoteAnimationFinishedStub(this); mFinishedCallback = finishedCallback; mApps = apps; - if (mRemoteCallback == null) mRemoteCallback = new RemoteAnimationFinishedStub(this); mWaitingAnimation = false; if (shouldMonitorCUJ(apps)) { - interactionJankMonitor.begin( + InteractionJankMonitor.getInstance().begin( apps[0].leash, mContext, mHandler, mCujType); } try { @@ -161,6 +171,28 @@ public class BackAnimationRunner { } } + void onAnimationFinish(RemoteAnimationFinishedStub finished) { + mHandler.post(() -> { + if (mRemoteCallback != null && finished != mRemoteCallback) { + return; + } + if (shouldMonitorCUJ(mApps)) { + InteractionJankMonitor.getInstance().end(mCujType); + } + + mFinishedCallback.run(); + for (int i = mApps.length - 1; i >= 0; --i) { + final SurfaceControl sc = mApps[i].leash; + if (sc != null && sc.isValid()) { + sc.release(); + } + } + mApps = null; + mFinishedCallback = null; + mRemoteCallback = null; + }); + } + @VisibleForTesting boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) { return apps.length > 0 && mCujType != NO_CUJ; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt new file mode 100644 index 000000000000..67592e60e954 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ComponentUtils.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common + +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Intent +import com.android.wm.shell.ShellTaskOrganizer + +/** Utils to obtain [ComponentName]s. */ +object ComponentUtils { + /** Retrieves the package name from an [Intent]. */ + @JvmStatic + fun getPackageName(intent: Intent?): String? = intent?.component?.packageName + + /** Retrieves the package name from a [PendingIntent]. */ + @JvmStatic + fun getPackageName(pendingIntent: PendingIntent?): String? = + getPackageName(pendingIntent?.intent) + + /** Retrieves the package name from a [taskId]. */ + @JvmStatic + fun getPackageName(taskId: Int, taskOrganizer: ShellTaskOrganizer): String? { + val taskInfo = taskOrganizer.getRunningTaskInfo(taskId) + return getPackageName(taskInfo?.baseIntent) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index f532be6b8277..72be066fc7a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; import android.os.RemoteException; import android.util.ArraySet; import android.util.Size; @@ -34,6 +35,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; +import com.android.window.flags.Flags; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ShellInit; @@ -54,6 +56,7 @@ public class DisplayController { private final ShellExecutor mMainExecutor; private final Context mContext; private final IWindowManager mWmService; + private final DisplayManager mDisplayManager; private final DisplayChangeController mChangeController; private final IDisplayWindowListener mDisplayContainerListener; @@ -61,10 +64,11 @@ public class DisplayController { private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, - ShellExecutor mainExecutor) { + ShellExecutor mainExecutor, DisplayManager displayManager) { mMainExecutor = mainExecutor; mContext = context; mWmService = wmService; + mDisplayManager = displayManager; // TODO: Inject this instead mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); @@ -74,7 +78,7 @@ public class DisplayController { } /** - * Initializes the window listener. + * Initializes the window listener and the topology listener. */ public void onInit() { try { @@ -82,6 +86,12 @@ public class DisplayController { for (int i = 0; i < displayIds.length; i++) { onDisplayAdded(displayIds[i]); } + + if (Flags.enableConnectedDisplaysWindowDrag()) { + mDisplayManager.registerTopologyListener(mMainExecutor, + this::onDisplayTopologyChanged); + onDisplayTopologyChanged(mDisplayManager.getDisplayTopology()); + } } catch (RemoteException e) { throw new RuntimeException("Unable to register display controller"); } @@ -91,8 +101,7 @@ public class DisplayController { * Gets a display by id from DisplayManager. */ public Display getDisplay(int displayId) { - final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - return displayManager.getDisplay(displayId); + return mDisplayManager.getDisplay(displayId); } /** @@ -221,6 +230,14 @@ public class DisplayController { } } + private void onDisplayTopologyChanged(DisplayTopology topology) { + // TODO(b/381472611): Call DisplayTopology#getCoordinates and update values in + // DisplayLayout when DM code is ready. + for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { + mDisplayChangedListeners.get(i).onTopologyChanged(); + } + } + private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { synchronized (mDisplays) { final DisplayRecord dr = mDisplays.get(displayId); @@ -408,5 +425,10 @@ public class DisplayController { */ default void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, Set<Rect> unrestricted) {} + + /** + * Called when the display topology has changed. + */ + default void onTopologyChanged() {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index eb1e72790a25..c9890a5b4963 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -447,8 +447,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - private int imeTop(float surfaceOffset) { - return mImeFrame.top + (int) surfaceOffset; + private int imeTop(float surfaceOffset, float surfacePositionY) { + // surfaceOffset is already offset by the surface's top inset, so we need to subtract + // the top inset so that the return value is in screen coordinates. + return mImeFrame.top + (int) (surfaceOffset - surfacePositionY); } private boolean calcIsFloating(InsetsSource imeSource) { @@ -581,7 +583,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged final float alpha = (mAnimateAlpha || isFloating) ? (value - hiddenY) / (shownY - hiddenY) : 1f; t.setAlpha(animatingLeash, alpha); - dispatchPositionChanged(mDisplayId, imeTop(value), t); + dispatchPositionChanged(mDisplayId, imeTop(value, defaultY), t); t.apply(); mTransactionPool.release(t); }); @@ -600,11 +602,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged t.setPosition(animatingLeash, x, value); if (DEBUG) { Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" - + imeTop(hiddenY) + "->" + imeTop(shownY) + + imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY) + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); } - int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY), - imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); + int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY), + imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW, + isFloating, t); mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; final float alpha = (mAnimateAlpha || isFloating) ? (value - hiddenY) / (shownY - hiddenY) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index b6a1686bd087..4973a6f16409 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -31,7 +31,9 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.os.SystemProperties; import android.provider.Settings; import android.util.DisplayMetrics; @@ -71,9 +73,12 @@ public class DisplayLayout { public static final int NAV_BAR_RIGHT = 1 << 1; public static final int NAV_BAR_BOTTOM = 1 << 2; + private static final String TAG = "DisplayLayout"; + private int mUiMode; private int mWidth; private int mHeight; + private RectF mGlobalBoundsDp; private DisplayCutout mCutout; private int mRotation; private int mDensityDpi; @@ -109,6 +114,7 @@ public class DisplayLayout { return mUiMode == other.mUiMode && mWidth == other.mWidth && mHeight == other.mHeight + && Objects.equals(mGlobalBoundsDp, other.mGlobalBoundsDp) && Objects.equals(mCutout, other.mCutout) && mRotation == other.mRotation && mDensityDpi == other.mDensityDpi @@ -127,8 +133,8 @@ public class DisplayLayout { @Override public int hashCode() { - return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, - mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, + return Objects.hash(mUiMode, mWidth, mHeight, mGlobalBoundsDp, mCutout, mRotation, + mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, mNavBarFrameHeight, mTaskbarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState); } @@ -170,6 +176,7 @@ public class DisplayLayout { mUiMode = dl.mUiMode; mWidth = dl.mWidth; mHeight = dl.mHeight; + mGlobalBoundsDp = dl.mGlobalBoundsDp; mCutout = dl.mCutout; mRotation = dl.mRotation; mDensityDpi = dl.mDensityDpi; @@ -193,6 +200,7 @@ public class DisplayLayout { mRotation = info.rotation; mCutout = info.displayCutout; mDensityDpi = info.logicalDensityDpi; + mGlobalBoundsDp = new RectF(0, 0, pxToDp(mWidth), pxToDp(mHeight)); mHasNavigationBar = hasNavigationBar; mHasStatusBar = hasStatusBar; mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean( @@ -255,6 +263,11 @@ public class DisplayLayout { recalcInsets(res); } + /** Update the global bounds of this layout, in DP. */ + public void setGlobalBoundsDp(RectF bounds) { + mGlobalBoundsDp = bounds; + } + /** Get this layout's non-decor insets. */ public Rect nonDecorInsets() { return mNonDecorInsets; @@ -265,16 +278,21 @@ public class DisplayLayout { return mStableInsets; } - /** Get this layout's width. */ + /** Get this layout's width in pixels. */ public int width() { return mWidth; } - /** Get this layout's height. */ + /** Get this layout's height in pixels. */ public int height() { return mHeight; } + /** Get this layout's global bounds in the multi-display coordinate system in DP. */ + public RectF globalBoundsDp() { + return mGlobalBoundsDp; + } + /** Get this layout's display rotation. */ public int rotation() { return mRotation; @@ -486,4 +504,48 @@ public class DisplayLayout { ? R.dimen.navigation_bar_frame_height_landscape : R.dimen.navigation_bar_frame_height); } + + /** + * Converts a pixel value to a density-independent pixel (dp) value. + * + * @param px The pixel value to convert. + * @return The equivalent value in DP units. + */ + public float pxToDp(Number px) { + return px.floatValue() * DisplayMetrics.DENSITY_DEFAULT / mDensityDpi; + } + + /** + * Converts a density-independent pixel (dp) value to a pixel value. + * + * @param dp The DP value to convert. + * @return The equivalent value in pixel units. + */ + public float dpToPx(Number dp) { + return dp.floatValue() * mDensityDpi / DisplayMetrics.DENSITY_DEFAULT; + } + + /** + * Converts local pixel coordinates on this layout to global DP coordinates. + * + * @param xPx The x-coordinate in pixels, relative to the layout's origin. + * @param yPx The y-coordinate in pixels, relative to the layout's origin. + * @return A PointF object representing the coordinates in global DP units. + */ + public PointF localPxToGlobalDp(Number xPx, Number yPx) { + return new PointF(mGlobalBoundsDp.left + pxToDp(xPx), + mGlobalBoundsDp.top + pxToDp(yPx)); + } + + /** + * Converts global DP coordinates to local pixel coordinates on this layout. + * + * @param xDp The x-coordinate in global DP units. + * @param yDp The y-coordinate in global DP units. + * @return A PointF object representing the coordinates in local pixel units on this layout. + */ + public PointF globalDpToLocalPx(Number xDp, Number yDp) { + return new PointF(dpToPx(xDp.floatValue() - mGlobalBoundsDp.left), + dpToPx(yDp.floatValue() - mGlobalBoundsDp.top)); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index 9113c0a53178..83e5e31bd125 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -27,14 +27,10 @@ import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import android.app.ActivityManager; -import android.app.PendingIntent; -import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; -import androidx.annotation.Nullable; - import com.android.internal.util.ArrayUtils; import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; @@ -65,31 +61,6 @@ public class SplitScreenUtils { && ArrayUtils.contains(CONTROLLED_WINDOWING_MODES, taskInfo.getWindowingMode()); } - /** Retrieve package name from an intent */ - @Nullable - public static String getPackageName(Intent intent) { - if (intent == null || intent.getComponent() == null) { - return null; - } - return intent.getComponent().getPackageName(); - } - - /** Retrieve package name from a PendingIntent */ - @Nullable - public static String getPackageName(PendingIntent pendingIntent) { - if (pendingIntent == null || pendingIntent.getIntent() == null) { - return null; - } - return getPackageName(pendingIntent.getIntent()); - } - - /** Retrieve package name from a taskId */ - @Nullable - public static String getPackageName(int taskId, ShellTaskOrganizer taskOrganizer) { - final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId); - return taskInfo != null ? getPackageName(taskInfo.baseIntent) : null; - } - /** Retrieve user id from a taskId */ public static int getUserId(int taskId, ShellTaskOrganizer taskOrganizer) { final ActivityManager.RunningTaskInfo taskInfo = taskOrganizer.getRunningTaskInfo(taskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index b796b411dd1a..1323fe0fa9ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; @@ -60,7 +59,6 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.KeyguardChangeListener; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; @@ -73,6 +71,7 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -198,9 +197,6 @@ public class CompatUIController implements OnDisplaysChangedListener, private final CompatUIStatusManager mCompatUIStatusManager; @NonNull - private final FocusTransitionObserver mFocusTransitionObserver; - - @NonNull private final Optional<DesktopUserRepositories> mDesktopUserRepositories; public CompatUIController(@NonNull Context context, @@ -217,8 +213,7 @@ public class CompatUIController implements OnDisplaysChangedListener, @NonNull CompatUIShellCommandHandler compatUIShellCommandHandler, @NonNull AccessibilityManager accessibilityManager, @NonNull CompatUIStatusManager compatUIStatusManager, - @NonNull Optional<DesktopUserRepositories> desktopUserRepositories, - @NonNull FocusTransitionObserver focusTransitionObserver) { + @NonNull Optional<DesktopUserRepositories> desktopUserRepositories) { mContext = context; mShellController = shellController; mDisplayController = displayController; @@ -235,7 +230,6 @@ public class CompatUIController implements OnDisplaysChangedListener, DISAPPEAR_DELAY_MS, flags); mCompatUIStatusManager = compatUIStatusManager; mDesktopUserRepositories = desktopUserRepositories; - mFocusTransitionObserver = focusTransitionObserver; shellInit.addInitCallback(this::onInit, this); } @@ -412,8 +406,7 @@ public class CompatUIController implements OnDisplaysChangedListener, // start tracking the buttons visibility for this task. if (mTopActivityTaskId != taskInfo.taskId && !taskInfo.isTopActivityTransparent - && taskInfo.isVisible - && mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)) { + && taskInfo.isVisible && taskInfo.isFocused) { mTopActivityTaskId = taskInfo.taskId; setHasShownUserAspectRatioSettingsButton(false); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java index c493aadd57b0..151dc438702d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java @@ -20,6 +20,7 @@ import android.os.HandlerThread; import androidx.annotation.Nullable; +import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -112,4 +113,7 @@ public interface WMComponent { */ @WMSingleton Optional<DesktopMode> getDesktopMode(); + + @WMSingleton + Optional<AppZoomOut> getAppZoomOut(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 2600bcc18f72..cbbe8a2b5613 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -25,6 +25,7 @@ import android.annotation.NonNull; import android.app.ActivityTaskManager; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.SystemProperties; import android.provider.Settings; @@ -91,6 +92,7 @@ import com.android.wm.shell.compatui.impl.DefaultComponentIdGenerator; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; import com.android.wm.shell.freeform.FreeformComponents; @@ -111,6 +113,8 @@ import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; +import com.android.wm.shell.appzoomout.AppZoomOut; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingSurface; @@ -172,8 +176,9 @@ public abstract class WMShellBaseModule { static DisplayController provideDisplayController(Context context, IWindowManager wmService, ShellInit shellInit, - @ShellMainThread ShellExecutor mainExecutor) { - return new DisplayController(context, wmService, shellInit, mainExecutor); + @ShellMainThread ShellExecutor mainExecutor, + DisplayManager displayManager) { + return new DisplayController(context, wmService, shellInit, mainExecutor, displayManager); } @WMSingleton @@ -272,8 +277,7 @@ public abstract class WMShellBaseModule { @NonNull CompatUIState compatUIState, @NonNull CompatUIComponentIdGenerator componentIdGenerator, @NonNull CompatUIComponentFactory compatUIComponentFactory, - CompatUIStatusManager compatUIStatusManager, - @NonNull FocusTransitionObserver focusTransitionObserver) { + CompatUIStatusManager compatUIStatusManager) { if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) { return Optional.empty(); } @@ -298,8 +302,7 @@ public abstract class WMShellBaseModule { compatUIShellCommandHandler.get(), accessibilityManager.get(), compatUIStatusManager, - desktopUserRepositories, - focusTransitionObserver)); + desktopUserRepositories)); } @WMSingleton @@ -1033,6 +1036,38 @@ public abstract class WMShellBaseModule { }); } + @WMSingleton + @Provides + static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() { + return new DesktopWallpaperActivityTokenProvider(); + } + + @WMSingleton + @Provides + static Optional<DesktopWallpaperActivityTokenProvider> + provideOptionalDesktopWallpaperActivityTokenProvider( + Context context, + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider) { + if (DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.of(desktopWallpaperActivityTokenProvider); + } + return Optional.empty(); + } + + // + // App zoom out (optional feature) + // + + @WMSingleton + @Provides + static Optional<AppZoomOut> provideAppZoomOut( + Optional<AppZoomOutController> appZoomOutController) { + return appZoomOutController.map((controller) -> controller.asAppZoomOut()); + } + + @BindsOptionalOf + abstract AppZoomOutController optionalAppZoomOutController(); + // // Task Stack // @@ -1077,6 +1112,7 @@ public abstract class WMShellBaseModule { Optional<RecentTasksController> recentTasksOptional, Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional, Optional<OneHandedController> oneHandedControllerOptional, + Optional<AppZoomOutController> appZoomOutControllerOptional, Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional, Optional<ActivityEmbeddingController> activityEmbeddingOptional, Optional<MixedTransitionHandler> mixedTransitionHandler, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 1916215dea74..ac510f89b905 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -50,6 +50,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; import com.android.wm.shell.apptoweb.AssistContentRequester; +import com.android.wm.shell.appzoomout.AppZoomOutController; import com.android.wm.shell.back.BackAnimationController; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -1312,10 +1313,21 @@ public abstract class WMShellModule { return new DesktopModeUiEventLogger(uiEventLogger, packageManager); } + // + // App zoom out + // + @WMSingleton @Provides - static DesktopWallpaperActivityTokenProvider provideDesktopWallpaperActivityTokenProvider() { - return new DesktopWallpaperActivityTokenProvider(); + static AppZoomOutController provideAppZoomOutController( + Context context, + ShellInit shellInit, + ShellTaskOrganizer shellTaskOrganizer, + DisplayController displayController, + DisplayLayout displayLayout, + @ShellMainThread ShellExecutor mainExecutor) { + return AppZoomOutController.create(context, shellInit, shellTaskOrganizer, + displayController, displayLayout, mainExecutor); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 9e2b9b20be16..c8d0dab39837 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -41,6 +41,7 @@ import com.android.wm.shell.common.pip.SizeSpecSource; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipMotionHelper; @@ -82,11 +83,14 @@ public abstract class Pip2Module { @NonNull PipTransitionState pipStackListenerController, @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipUiStateChangeController pipUiStateChangeController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) { + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, - pipUiStateChangeController, desktopUserRepositoriesOptional); + pipUiStateChangeController, desktopUserRepositoriesOptional, + desktopWallpaperActivityTokenProviderOptional); } @WMSingleton @@ -138,9 +142,12 @@ public abstract class Pip2Module { @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState, - desktopUserRepositoriesOptional, rootTaskDisplayAreaOrganizer); + desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional, + rootTaskDisplayAreaOrganizer); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt index ca02c72c174e..f6fd9679922a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeDragAndDropTransitionHandler.kt @@ -93,9 +93,8 @@ class DesktopModeDragAndDropTransitionHandler(private val transitions: Transitio return matchingChanges.first() } - private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { - return change.taskInfo != null && change.taskInfo?.taskId != -1 - } + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean = + change.taskInfo != null && change.taskInfo?.taskId != -1 override fun handleRequest( transition: IBinder, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index e975b586c1ee..c09504ee3725 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -434,18 +434,14 @@ class DesktopModeLoggerTransitionObserver( visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo) } - private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo { - return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") - } + private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo = + this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change") - private fun TaskInfo.isFreeformWindow(): Boolean { - return this.windowingMode == WINDOWING_MODE_FREEFORM - } + private fun TaskInfo.isFreeformWindow(): Boolean = this.windowingMode == WINDOWING_MODE_FREEFORM - private fun TransitionInfo.isExitToRecentsTransition(): Boolean { - return this.type == WindowManager.TRANSIT_TO_FRONT && + private fun TransitionInfo.isExitToRecentsTransition(): Boolean = + this.type == WindowManager.TRANSIT_TO_FRONT && this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS - } companion object { @VisibleForTesting const val VISIBLE_TASKS_COUNTER_NAME = "desktop_mode_visible_tasks" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index dba8c9367654..cdfa14bbc4e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -24,8 +24,8 @@ import java.io.PrintWriter class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) : ShellCommandHandler.ShellCommandActionHandler { - override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean { - return when (args[0]) { + override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean = + when (args[0]) { "moveToDesktop" -> { if (!runMoveToDesktop(args, pw)) { pw.println("Task not found. Please enter a valid taskId.") @@ -47,7 +47,6 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl false } } - } private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean { if (args.size < 2) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index d3066645f32e..c975533abf24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -27,6 +27,7 @@ import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread @@ -56,6 +57,10 @@ class DesktopRepository( * @property topTransparentFullscreenTaskId the task id of any current top transparent * fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is * closed or sent to back. (top is at index 0). + * @property pipTaskId the task id of PiP task entered while in Desktop Mode. + * @property pipShouldKeepDesktopActive whether an active PiP window should keep the Desktop + * Mode session active. Only false when we are explicitly exiting Desktop Mode (via user + * action) while there is an active PiP window. */ private data class DesktopTaskData( val activeTasks: ArraySet<Int> = ArraySet(), @@ -66,6 +71,8 @@ class DesktopRepository( val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), var fullImmersiveTaskId: Int? = null, var topTransparentFullscreenTaskId: Int? = null, + var pipTaskId: Int? = null, + var pipShouldKeepDesktopActive: Boolean = true, ) { fun deepCopy(): DesktopTaskData = DesktopTaskData( @@ -76,6 +83,8 @@ class DesktopRepository( freeformTasksInZOrder = ArrayList(freeformTasksInZOrder), fullImmersiveTaskId = fullImmersiveTaskId, topTransparentFullscreenTaskId = topTransparentFullscreenTaskId, + pipTaskId = pipTaskId, + pipShouldKeepDesktopActive = pipShouldKeepDesktopActive, ) fun clear() { @@ -86,6 +95,8 @@ class DesktopRepository( freeformTasksInZOrder.clear() fullImmersiveTaskId = null topTransparentFullscreenTaskId = null + pipTaskId = null + pipShouldKeepDesktopActive = true } } @@ -104,6 +115,9 @@ class DesktopRepository( /* Tracks last bounds of task before toggled to immersive state. */ private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>() + /* Callback for when a pending PiP transition has been aborted. */ + private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null + private var desktopGestureExclusionListener: Consumer<Region>? = null private var desktopGestureExclusionExecutor: Executor? = null @@ -302,6 +316,54 @@ class DesktopRepository( } } + /** Set whether the given task is the Desktop-entered PiP task in this display. */ + fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) { + val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId) + if (enterPip) { + desktopData.pipTaskId = taskId + desktopData.pipShouldKeepDesktopActive = true + } else { + desktopData.pipTaskId = + if (desktopData.pipTaskId == taskId) null + else { + logW( + "setTaskInPip: taskId=$taskId did not match saved taskId=${desktopData.pipTaskId}" + ) + desktopData.pipTaskId + } + } + notifyVisibleTaskListeners(displayId, getVisibleTaskCount(displayId)) + } + + /** Returns whether there is a PiP that was entered/minimized from Desktop in this display. */ + fun isMinimizedPipPresentInDisplay(displayId: Int): Boolean = + desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId != null + + /** Returns whether the given task is the Desktop-entered PiP task in this display. */ + fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean = + desktopTaskDataByDisplayId.getOrCreate(displayId).pipTaskId == taskId + + /** Returns whether Desktop session should be active in this display due to active PiP. */ + fun shouldDesktopBeActiveForPip(displayId: Int): Boolean = + Flags.enableDesktopWindowingPip() && + isMinimizedPipPresentInDisplay(displayId) && + desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive + + /** Saves whether a PiP window should keep Desktop session active in this display. */ + fun setPipShouldKeepDesktopActive(displayId: Int, keepActive: Boolean) { + desktopTaskDataByDisplayId.getOrCreate(displayId).pipShouldKeepDesktopActive = keepActive + } + + /** Saves callback to handle a pending PiP transition being aborted. */ + fun setOnPipAbortedCallback(callbackIfPipAborted: ((Int, Int) -> Unit)?) { + onPipAbortedCallback = callbackIfPipAborted + } + + /** Invokes callback to handle a pending PiP transition with the given task id being aborted. */ + fun onPipAborted(displayId: Int, pipTaskId: Int) { + onPipAbortedCallback?.invoke(displayId, pipTaskId) + } + /** Set whether the given task is the full-immersive task in this display. */ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) { val desktopData = desktopTaskDataByDisplayId.getOrCreate(displayId) @@ -338,8 +400,12 @@ class DesktopRepository( } private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) { + val visibleAndPipTasksCount = + if (shouldDesktopBeActiveForPip(displayId)) visibleTasksCount + 1 else visibleTasksCount visibleTasksListeners.forEach { (listener, executor) -> - executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) } + executor.execute { + listener.onTasksVisibilityChanged(displayId, visibleAndPipTasksCount) + } } } @@ -404,7 +470,7 @@ class DesktopRepository( * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id * will be looked up from the task id. */ - fun removeFreeformTask(displayId: Int, taskId: Int) { + fun removeTask(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d", taskId) if (displayId == INVALID_DISPLAY) { // Removes the original display id of the task. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index 947a8dddb239..c958a0975f11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -29,7 +29,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser val desktopRepository: DesktopRepository = desktopUserRepositories.getProfile(taskInfo.userId) if (!isFreeformTask(taskInfo) && desktopRepository.isActiveTask(taskInfo.taskId)) { - desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId) return } if (isFreeformTask(taskInfo)) { @@ -50,7 +50,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser desktopRepository.updateTask(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible) } else { // Case 2: Freeform task is changed outside Desktop Mode. - desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId) } } @@ -60,7 +60,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser // Any changes to [DesktopRepository] from this method should be made carefully to minimize risk // of race conditions and possible duplications with [onTaskChanging]. override fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) { - // TODO: b/367268953 - Propapagate usages from FreeformTaskListener to this method. + // TODO: b/367268953 - Propagate usages from FreeformTaskListener to this method. } override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) { @@ -68,7 +68,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return if (!isFreeformTask(taskInfo)) { - desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId) } // TODO: b/367268953 - Connect this with DesktopRepository for handling // task moving to front for tasks in windowing mode. @@ -90,7 +90,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser // A task that's vanishing should be removed: // - If it's closed by the X button which means it's marked as a closing task. desktopRepository.removeClosingTask(taskInfo.taskId) - desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId) } else { desktopRepository.updateTask(taskInfo.displayId, taskInfo.taskId, isVisible = false) desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt index 848d80ff4f0b..f29301d92292 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt @@ -41,49 +41,35 @@ sealed class DesktopTaskPosition { return Point(x, y.toInt()) } - override fun next(): DesktopTaskPosition { - return BottomRight - } + override fun next(): DesktopTaskPosition = BottomRight } data object BottomRight : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.right - window.width(), frame.bottom - window.height()) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.right - window.width(), frame.bottom - window.height()) - override fun next(): DesktopTaskPosition { - return TopLeft - } + override fun next(): DesktopTaskPosition = TopLeft } data object TopLeft : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.left, frame.top) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.left, frame.top) - override fun next(): DesktopTaskPosition { - return BottomLeft - } + override fun next(): DesktopTaskPosition = BottomLeft } data object BottomLeft : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.left, frame.bottom - window.height()) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.left, frame.bottom - window.height()) - override fun next(): DesktopTaskPosition { - return TopRight - } + override fun next(): DesktopTaskPosition = TopRight } data object TopRight : DesktopTaskPosition() { - override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point { - return Point(frame.right - window.width(), frame.top) - } + override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point = + Point(frame.right - window.width(), frame.top) - override fun next(): DesktopTaskPosition { - return Center - } + override fun next(): DesktopTaskPosition = Center } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 050dfb6f562c..73d15270c811 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -225,7 +225,6 @@ class DesktopTasksController( // Launch cookie used to identify a drag and drop transition to fullscreen after it has begun. // Used to prevent handleRequest from moving the new fullscreen task to freeform. private var dragAndDropFullscreenCookie: Binder? = null - private var pendingPipTransitionAndTask: Pair<IBinder, Int>? = null init { desktopMode = DesktopModeImpl() @@ -321,18 +320,29 @@ class DesktopTasksController( fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId) /** - * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on - * top in Desktop Mode. + * Returns true if any of the following is true: + * - Any freeform tasks are visible + * - A transparent fullscreen task exists on top in Desktop Mode + * - PiP on Desktop Windowing is enabled, there is an active PiP window and the desktop + * wallpaper is visible. */ fun isDesktopModeShowing(displayId: Int): Boolean { + val hasVisibleTasks = visibleTaskCount(displayId) > 0 + val hasTopTransparentFullscreenTask = + taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + val hasMinimizedPip = + Flags.enableDesktopWindowingPip() && + taskRepository.isMinimizedPipPresentInDisplay(displayId) && + desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(displayId) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() ) { - return visibleTaskCount(displayId) > 0 || - taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip + } else if (Flags.enableDesktopWindowingPip()) { + return hasVisibleTasks || hasMinimizedPip } - return visibleTaskCount(displayId) > 0 + return hasVisibleTasks } /** Moves focused task to desktop mode for given [displayId]. */ @@ -592,7 +602,7 @@ class DesktopTasksController( ): ((IBinder) -> Unit)? { val taskId = taskInfo.taskId desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) - performDesktopExitCleanupIfNeeded(taskId, displayId, wct) + performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) @@ -624,8 +634,12 @@ class DesktopTasksController( ) val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null) wct.merge(requestRes.second, true) - pendingPipTransitionAndTask = - freeformTaskTransitionStarter.startPipTransition(wct) to taskInfo.taskId + freeformTaskTransitionStarter.startPipTransition(wct) + taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true) + taskRepository.setOnPipAbortedCallback { displayId, taskId -> + minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!) + taskRepository.setTaskInPip(displayId, taskId, enterPip = false) + } return } @@ -636,7 +650,7 @@ class DesktopTasksController( val taskId = taskInfo.taskId val displayId = taskInfo.displayId val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(taskId, displayId, wct) + performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) // Notify immersive handler as it might need to exit immersive state. val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( @@ -898,7 +912,12 @@ class DesktopTasksController( } if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct) + performDesktopExitCleanupIfNeeded( + task.taskId, + task.displayId, + wct, + forceToFullscreen = false, + ) } transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) @@ -1414,7 +1433,9 @@ class DesktopTasksController( taskId: Int, displayId: Int, wct: WindowContainerTransaction, + forceToFullscreen: Boolean, ) { + taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen) if (Flags.enablePerDisplayDesktopWallpaperActivity()) { if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { return @@ -1422,6 +1443,12 @@ class DesktopTasksController( if (displayId != DEFAULT_DISPLAY) { return } + } else if ( + Flags.enableDesktopWindowingPip() && + taskRepository.isMinimizedPipPresentInDisplay(displayId) && + !forceToFullscreen + ) { + return } else { if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) { return @@ -1443,13 +1470,9 @@ class DesktopTasksController( } } - override fun getContext(): Context { - return context - } + override fun getContext(): Context = context - override fun getRemoteCallExecutor(): ShellExecutor { - return mainExecutor - } + override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor override fun startAnimation( transition: IBinder, @@ -1462,21 +1485,6 @@ class DesktopTasksController( return false } - override fun onTransitionConsumed( - transition: IBinder, - aborted: Boolean, - finishT: Transaction?, - ) { - pendingPipTransitionAndTask?.let { (pipTransition, taskId) -> - if (transition == pipTransition) { - if (aborted) { - shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { minimizeTaskInner(it) } - } - pendingPipTransitionAndTask = null - } - } - } - override fun handleRequest( transition: IBinder, request: TransitionRequestInfo, @@ -1650,11 +1658,10 @@ class DesktopTasksController( DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() && isTopActivityExemptFromDesktopWindowing(context, task) - private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean { - return ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && + private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean = + ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() && TransitionUtil.isClosingType(request.type) && request.triggerTask != null - } /** Open an existing instance of an app. */ fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) { @@ -1926,7 +1933,12 @@ class DesktopTasksController( if (!isDesktopModeShowing(task.displayId)) return null val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(task.taskId, task.displayId, wct) + performDesktopExitCleanupIfNeeded( + task.taskId, + task.displayId, + wct, + forceToFullscreen = false, + ) if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) @@ -2053,7 +2065,12 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } - performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct) + performDesktopExitCleanupIfNeeded( + taskInfo.taskId, + taskInfo.displayId, + wct, + forceToFullscreen = true, + ) } private fun cascadeWindow(bounds: Rect, displayLayout: DisplayLayout, displayId: Int) { @@ -2087,7 +2104,12 @@ class DesktopTasksController( // want it overridden in multi-window. wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) - performDesktopExitCleanupIfNeeded(taskInfo.taskId, taskInfo.displayId, wct) + performDesktopExitCleanupIfNeeded( + taskInfo.taskId, + taskInfo.displayId, + wct, + forceToFullscreen = false, + ) } /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ @@ -2158,11 +2180,10 @@ class DesktopTasksController( getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) } } - private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? { - return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> + private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? = + shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo -> taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM } - } /** * Requests a task be transitioned from desktop to split select. Applies needed windowing @@ -2210,14 +2231,10 @@ class DesktopTasksController( } } - private fun getDefaultDensityDpi(): Int { - return context.resources.displayMetrics.densityDpi - } + private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi /** Creates a new instance of the external interface to pass to another process. */ - private fun createExternalInterface(): ExternalInterfaceBinder { - return IDesktopModeImpl(this) - } + private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this) /** Get connection interface between sysui and shell */ fun asDesktopMode(): DesktopMode { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index c2dd4d28305b..e4a28e9efe60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -138,11 +138,10 @@ class DesktopTasksLimiter( ) } - private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? { - return info.changes.find { change -> + private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = + info.changes.find { change -> change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK } - } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { if (activeTransitionTokensAndTasks.remove(merged) != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 9bf5555fc194..b9a65fee7d4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -21,9 +21,11 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.os.IBinder import android.view.SurfaceControl -import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY import android.window.TransitionInfo @@ -39,6 +41,8 @@ import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP +import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP /** * A [Transitions.TransitionObserver] that observes shell transitions and updates the @@ -57,6 +61,8 @@ class DesktopTasksTransitionObserver( ) : Transitions.TransitionObserver { private var transitionToCloseWallpaper: IBinder? = null + /* Pending PiP transition and its associated display id and task id. */ + private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null private var currentProfileId: Int init { @@ -90,6 +96,33 @@ class DesktopTasksTransitionObserver( removeTaskIfNeeded(info) } removeWallpaperOnLastTaskClosingIfNeeded(transition, info) + + val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) + info.changes.forEach { change -> + change.taskInfo?.let { taskInfo -> + if ( + Flags.enableDesktopWindowingPip() && + desktopRepository.isTaskMinimizedPipInDisplay( + taskInfo.displayId, + taskInfo.taskId, + ) + ) { + when (info.type) { + TRANSIT_PIP -> + pendingPipTransitionAndPipTask = + Triple(transition, taskInfo.displayId, taskInfo.taskId) + + TRANSIT_EXIT_PIP, + TRANSIT_REMOVE_PIP -> + desktopRepository.setTaskInPip( + taskInfo.displayId, + taskInfo.taskId, + enterPip = false, + ) + } + } + } + } } private fun removeTaskIfNeeded(info: TransitionInfo) { @@ -108,7 +141,7 @@ class DesktopTasksTransitionObserver( desktopRepository.isActiveTask(taskInfo.taskId) && taskInfo.windowingMode != WINDOWING_MODE_FREEFORM ) { - desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId) } } } @@ -252,6 +285,18 @@ class DesktopTasksTransitionObserver( } } transitionToCloseWallpaper = null + } else if (pendingPipTransitionAndPipTask?.first == transition) { + val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) + if (aborted) { + pendingPipTransitionAndPipTask?.let { + desktopRepository.onPipAborted( + /*displayId=*/ it.second, + /* taskId=*/ it.third, + ) + } + } + desktopRepository.setOnPipAbortedCallback(null) + pendingPipTransitionAndPipTask = null } } @@ -263,11 +308,15 @@ class DesktopTasksTransitionObserver( change.taskInfo?.let { taskInfo -> if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { when (change.mode) { - WindowManager.TRANSIT_OPEN -> { + TRANSIT_OPEN -> { desktopWallpaperActivityTokenProvider.setToken( taskInfo.token, taskInfo.displayId, ) + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = true, + taskInfo.displayId, + ) // After the task for the wallpaper is created, set it non-trimmable. // This is important to prevent recents from trimming and removing the // task. @@ -278,6 +327,16 @@ class DesktopTasksTransitionObserver( } TRANSIT_CLOSE -> desktopWallpaperActivityTokenProvider.removeToken(taskInfo.displayId) + TRANSIT_TO_FRONT -> + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = true, + taskInfo.displayId, + ) + TRANSIT_TO_BACK -> + desktopWallpaperActivityTokenProvider.setWallpaperActivityIsVisible( + isVisible = false, + taskInfo.displayId, + ) else -> {} } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index 7f3133e141ef..13576aa42737 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -34,6 +34,7 @@ import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener import java.io.PrintWriter import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** Manages per-user DesktopRepository instances. */ class DesktopUserRepositories( @@ -43,7 +44,7 @@ class DesktopUserRepositories( private val persistentRepository: DesktopPersistentRepository, private val repositoryInitializer: DesktopRepositoryInitializer, @ShellMainThread private val mainCoroutineScope: CoroutineScope, - userManager: UserManager, + private val userManager: UserManager, ) : UserChangeListener { private var userId: Int private var userIdToProfileIdsMap: MutableMap<Int, List<Int>> = mutableMapOf() @@ -100,6 +101,9 @@ class DesktopUserRepositories( override fun onUserChanged(newUserId: Int, userContext: Context) { logD("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId) userId = newUserId + if (Flags.enableDesktopWindowingHsum()) { + sanitizeUsers() + } } override fun onUserProfilesChanged(profiles: MutableList<UserInfo>) { @@ -110,10 +114,34 @@ class DesktopUserRepositories( } } + private fun sanitizeUsers() { + val aliveUserIds = userManager.getAliveUsers().map { it.id } + val usersToDelete = userIdToProfileIdsMap.keys.filterNot { it in aliveUserIds } + + usersToDelete.forEach { uid -> + userIdToProfileIdsMap.remove(uid) + desktopRepoByUserId.remove(uid) + } + mainCoroutineScope.launch { + try { + persistentRepository.removeUsers(usersToDelete) + } catch (exception: Exception) { + logE( + "An exception occurred while updating the persistent repository \n%s", + exception.stackTrace, + ) + } + } + } + private fun logD(msg: String, vararg arguments: Any?) { ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } + private fun logE(msg: String, vararg arguments: Any?) { + ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + companion object { private const val TAG = "DesktopUserRepositories" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 1380a9ca164f..91f10dc4faf5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -765,9 +765,8 @@ sealed class DragToDesktopTransitionHandler( transitionState = null } - private fun isSplitTask(taskId: Int): Boolean { - return splitScreenController.isTaskInSplitScreen(taskId) - } + private fun isSplitTask(taskId: Int): Boolean = + splitScreenController.isTaskInSplitScreen(taskId) private fun getOtherSplitTask(taskId: Int): Int? { val splitPos = splitScreenController.getSplitPosition(taskId) @@ -781,9 +780,8 @@ sealed class DragToDesktopTransitionHandler( return splitScreenController.getTaskInfo(otherTaskPos)?.taskId } - protected fun requireTransitionState(): TransitionState { - return transitionState ?: error("Expected non-null transition state") - } + protected fun requireTransitionState(): TransitionState = + transitionState ?: error("Expected non-null transition state") /** * Represents the layering (Z order) that will be given to any window based on its type during diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt index a47e9370b58d..5e84019b14f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt @@ -156,13 +156,11 @@ class ToggleResizeDesktopTaskTransitionHandler( return matchingChanges.first() } - private fun isWallpaper(change: TransitionInfo.Change): Boolean { - return (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 - } + private fun isWallpaper(change: TransitionInfo.Change): Boolean = + (change.flags and TransitionInfo.FLAG_IS_WALLPAPER) != 0 - private fun isValidTaskChange(change: TransitionInfo.Change): Boolean { - return change.taskInfo != null && change.taskInfo?.taskId != -1 - } + private fun isValidTaskChange(change: TransitionInfo.Change): Boolean = + change.taskInfo != null && change.taskInfo?.taskId != -1 companion object { private const val RESIZE_DURATION_MS = 300L diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt index a87004c07d43..2bd7a9873a5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode.desktopwallpaperactivity import android.util.SparseArray +import android.util.SparseBooleanArray import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerToken @@ -24,6 +25,7 @@ import android.window.WindowContainerToken class DesktopWallpaperActivityTokenProvider { private val wallpaperActivityTokenByDisplayId = SparseArray<WindowContainerToken>() + private val wallpaperActivityVisByDisplayId = SparseBooleanArray() fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) { wallpaperActivityTokenByDisplayId[displayId] = token @@ -36,4 +38,16 @@ class DesktopWallpaperActivityTokenProvider { fun removeToken(displayId: Int = DEFAULT_DISPLAY) { wallpaperActivityTokenByDisplayId.delete(displayId) } + + fun setWallpaperActivityIsVisible( + isVisible: Boolean = false, + displayId: Int = DEFAULT_DISPLAY, + ) { + wallpaperActivityVisByDisplayId.put(displayId, isVisible) + } + + fun isWallpaperActivityVisible(displayId: Int = DEFAULT_DISPLAY): Boolean { + return wallpaperActivityTokenByDisplayId[displayId] != null && + wallpaperActivityVisByDisplayId.get(displayId, false) + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index a6998e1179fa..9e41270c21f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -141,6 +141,22 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis } } + suspend fun removeUsers(uids: List<Int>) { + try { + dataStore.updateData { persistentRepositories: DesktopPersistentRepositories -> + val persistentRepositoriesBuilder = persistentRepositories.toBuilder() + uids.forEach { uid -> persistentRepositoriesBuilder.removeDesktopRepoByUser(uid) } + persistentRepositoriesBuilder.build() + } + } catch (exception: Exception) { + Log.e( + TAG, + "Error in removing user related data, data is stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE", + exception, + ) + } + } + private fun getDesktop(currentRepository: DesktopRepositoryState, desktopId: Int): Desktop = // If there are no desktops set up, create one on the default display currentRepository.getDesktopOrDefault( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java index a62dd1c83520..4df9ae4b2ee3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java @@ -107,8 +107,11 @@ public class DragUtils { /** * Returns a list of the mime types provided in the clip description. */ - public static String getMimeTypesConcatenated(ClipDescription description) { + public static String getMimeTypesConcatenated(@Nullable ClipDescription description) { String mimeTypes = ""; + if (description == null) { + return mimeTypes; + } for (int i = 0; i < description.getMimeTypeCount(); i++) { if (i > 0) { mimeTypes += ", "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index 4b59efbcecf2..0d5aa0105659 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -127,7 +127,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, // triggered on a task and the task is closing. It will be marked as minimized in // [DesktopTasksTransitionObserver] before it gets here. repository.removeClosingTask(taskInfo.taskId); - repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId); + repository.removeTask(taskInfo.displayId, taskInfo.taskId); } } mWindowDecorationViewModel.onTaskVanished(taskInfo); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index bd676ce69cfe..bba778d9f438 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -68,12 +68,12 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.pip.PipContentOverlay; @@ -1359,7 +1359,7 @@ public class PipTransition extends PipTransitionController { public boolean isPackageActiveInPip(@Nullable String packageName) { final TaskInfo inPipTask = mPipOrganizer.getTaskInfo(); return packageName != null && inPipTask != null && mPipOrganizer.isInPip() - && packageName.equals(SplitScreenUtils.getPackageName(inPipTask.baseIntent)); + && packageName.equals(ComponentUtils.getPackageName(inPipTask.baseIntent)); } private void updatePipForUnhandledTransition(@NonNull TransitionInfo.Change pipChange, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 7f673d2efc68..ea8dac982703 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -40,6 +40,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -59,6 +60,8 @@ public class PipScheduler { private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private PipTransitionController mPipTransitionController; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory @@ -73,12 +76,16 @@ public class PipScheduler { ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; mSurfaceControlTransactionFactory = @@ -260,10 +267,18 @@ public class PipScheduler { /** Returns whether PiP is exiting while we're in desktop mode. */ private boolean isPipExitingToDesktopMode() { - return Flags.enableDesktopWindowingPip() && mDesktopUserRepositoriesOptional.isPresent() - && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId) > 0 - || isDisplayInFreeform()); + // Early return if PiP in Desktop Windowing is not supported. + if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty() + || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) { + return false; + } + final int displayId = Objects.requireNonNull( + mPipTransitionState.getPipTaskInfo()).displayId; + return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId) + > 0 + || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible( + displayId) + || isDisplayInFreeform(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 8061ee9090b6..0a42c71ce095 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -57,13 +57,15 @@ import androidx.annotation.Nullable; import com.android.internal.util.Preconditions; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.common.split.SplitScreenUtils; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; @@ -110,6 +112,8 @@ public class PipTransition extends PipTransitionController implements private final PipTransitionState mPipTransitionState; private final PipDisplayLayoutState mPipDisplayLayoutState; private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; // // Transition caches @@ -145,7 +149,9 @@ public class PipTransition extends PipTransitionController implements PipTransitionState pipTransitionState, PipDisplayLayoutState pipDisplayLayoutState, PipUiStateChangeController pipUiStateChangeController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional) { + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -157,6 +163,8 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.addPipTransitionStateChangedListener(this); mPipDisplayLayoutState = pipDisplayLayoutState; mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; } @Override @@ -826,13 +834,14 @@ public class PipTransition extends PipTransitionController implements return false; } - // Since opening a new task while in Desktop Mode always first open in Fullscreen // until DesktopMode Shell code resolves it to Freeform, PipTransition will get a // possibility to handle it also. In this case return false to not have it enter PiP. final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty() - && mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - pipTask.displayId) > 0; + && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( + pipTask.displayId) > 0 + || mDesktopUserRepositoriesOptional.get().getCurrent() + .isMinimizedPipPresentInDisplay(pipTask.displayId)); if (isInDesktopSession) { return false; } @@ -968,6 +977,27 @@ public class PipTransition extends PipTransitionController implements "Unexpected bundle for " + mPipTransitionState); break; case PipTransitionState.EXITED_PIP: + final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo(); + final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip() + && mDesktopUserRepositoriesOptional.isPresent() + && mDesktopWallpaperActivityTokenProviderOptional.isPresent(); + if (desktopPipEnabled && pipTask != null) { + final DesktopRepository desktopRepository = + mDesktopUserRepositoriesOptional.get().getCurrent(); + final boolean wallpaperIsVisible = + mDesktopWallpaperActivityTokenProviderOptional.get() + .isWallpaperActivityVisible(pipTask.displayId); + if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0 + && wallpaperIsVisible) { + mTransitions.startTransition( + TRANSIT_TO_BACK, + new WindowContainerTransaction().reorder( + mDesktopWallpaperActivityTokenProviderOptional.get() + .getToken(pipTask.displayId), /* onTop= */ false), + null + ); + } + } mPipTransitionState.setPinnedTaskLeash(null); mPipTransitionState.setPipTaskInfo(null); break; @@ -978,6 +1008,6 @@ public class PipTransition extends PipTransitionController implements public boolean isPackageActiveInPip(@Nullable String packageName) { final TaskInfo inPipTask = mPipTransitionState.getPipTaskInfo(); return packageName != null && inPipTask != null && mPipTransitionState.isInPip() - && packageName.equals(SplitScreenUtils.getPackageName(inPipTask.baseIntent)); + && packageName.equals(ComponentUtils.getPackageName(inPipTask.baseIntent)); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 0869caa55369..975b65023f20 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -103,6 +103,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private final RecentTasksImpl mImpl = new RecentTasksImpl(); private final ActivityTaskManager mActivityTaskManager; private final TaskStackTransitionObserver mTaskStackTransitionObserver; + private final RecentsShellCommandHandler mRecentsShellCommandHandler; private RecentsTransitionHandler mTransitionHandler = null; private IRecentTasksListener mListener; private final boolean mPcFeatureEnabled; @@ -167,6 +168,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mDesktopUserRepositories = desktopUserRepositories; mTaskStackTransitionObserver = taskStackTransitionObserver; mMainExecutor = mainExecutor; + mRecentsShellCommandHandler = new RecentsShellCommandHandler(this); shellInit.addInitCallback(this::onInit, this); } @@ -183,6 +185,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mShellController.addExternalInterface(IRecentTasks.DESCRIPTOR, this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); + mShellCommandHandler.addCommandCallback("recents", mRecentsShellCommandHandler, this); mUserId = ActivityManager.getCurrentUser(); mDesktopUserRepositories.ifPresent( desktopUserRepositories -> @@ -299,7 +302,7 @@ public class RecentTasksController implements TaskStackListenerCallback, @Override public void onRecentTaskRemovedForAddTask(int taskId) { mDesktopUserRepositories.ifPresent( - desktopUserRepositories -> desktopUserRepositories.getCurrent().removeFreeformTask( + desktopUserRepositories -> desktopUserRepositories.getCurrent().removeTask( INVALID_DISPLAY, taskId)); } @@ -656,6 +659,11 @@ public class RecentTasksController implements TaskStackListenerCallback, return mActivityTaskManager.removeTask(taskId); } + /** Removes all recent tasks that are visible. */ + public void removeAllVisibleRecentTasks() throws RemoteException { + ActivityTaskManager.getService().removeAllVisibleRecentTasks(); + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsShellCommandHandler.kt new file mode 100644 index 000000000000..f786e079ed93 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsShellCommandHandler.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.recents + +import android.os.RemoteException +import com.android.wm.shell.sysui.ShellCommandHandler.ShellCommandActionHandler +import java.io.PrintWriter + +class RecentsShellCommandHandler( + private val recentTasksController: RecentTasksController +) : ShellCommandActionHandler { + override fun onShellCommand(args: Array<out String>, pw: PrintWriter): Boolean { + when (args[0]) { + "clearAll" -> return runClearAll(pw) + else -> { + pw.println("Invalid command: " + args[0]) + return false + } + } + } + + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { + pw.println("${prefix}clearAll") + pw.println("$prefix Clears all visible recent tasks.") + } + + private fun runClearAll(pw: PrintWriter): Boolean { + try { + recentTasksController.removeAllVisibleRecentTasks() + } catch (e: RemoteException) { + pw.println("Exception while removing visible recent tasks:") + e.printStackTrace(pw) + return false + } + return true + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 76496b06a4dd..db582aa30f6a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -411,10 +411,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mInstanceId = System.identityHashCode(this); mListener = listener; mDeathHandler = () -> { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); - finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, - "deathRecipient"); + mExecutor.execute(() -> { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + "[%d] RecentsController.DeathRecipient: binder died", mInstanceId); + finishInner(mWillFinishToHome, false /* leaveHint */, null /* finishCb */, + "deathRecipient"); + }); }; try { mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */); @@ -1273,6 +1275,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "requested")); } + /** + * @param runnerFinishCb The remote finish callback to run after finish is complete, this is + * not the same as mFinishCb which reports the transition is finished + * to WM. + */ private void finishInner(boolean toHome, boolean sendUserLeaveHint, IResultReceiver runnerFinishCb, String reason) { if (finishSyntheticTransition(runnerFinishCb, reason)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index c724135aeced..9e88a260ac44 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -82,6 +82,7 @@ import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -682,7 +683,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, final String packageName1 = shortcutInfo.getPackage(); // NOTE: This doesn't correctly pull out packageName2 if taskId is referring to a task in // recents that hasn't launched and is not being organized - final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); + final String packageName2 = ComponentUtils.getPackageName(taskId, mTaskOrganizer); final int userId1 = shortcutInfo.getUserId(); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); if (samePackage(packageName1, packageName2, userId1, userId2)) { @@ -727,10 +728,10 @@ public class SplitScreenController implements SplitDragPolicy.Starter, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { Intent fillInIntent = null; - final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent); + final String packageName1 = ComponentUtils.getPackageName(pendingIntent); // NOTE: This doesn't correctly pull out packageName2 if taskId is referring to a task in // recents that hasn't launched and is not being organized - final String packageName2 = SplitScreenUtils.getPackageName(taskId, mTaskOrganizer); + final String packageName2 = ComponentUtils.getPackageName(taskId, mTaskOrganizer); final int userId2 = SplitScreenUtils.getUserId(taskId, mTaskOrganizer); boolean setSecondIntentMultipleTask = false; if (samePackage(packageName1, packageName2, userId1, userId2)) { @@ -766,8 +767,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, InstanceId instanceId) { Intent fillInIntent1 = null; Intent fillInIntent2 = null; - final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1); - final String packageName2 = SplitScreenUtils.getPackageName(pendingIntent2); + final String packageName1 = ComponentUtils.getPackageName(pendingIntent1); + final String packageName2 = ComponentUtils.getPackageName(pendingIntent2); final ActivityOptions activityOptions1 = options1 != null ? ActivityOptions.fromBundle(options1) : ActivityOptions.makeBasic(); final ActivityOptions activityOptions2 = options2 != null @@ -835,7 +836,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, if (fillInIntent == null) fillInIntent = new Intent(); fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION); - final String packageName1 = SplitScreenUtils.getPackageName(intent); + final String packageName1 = ComponentUtils.getPackageName(intent); final String packageName2 = getPackageName(reverseSplitPosition(position), hideTaskToken); final int userId2 = getUserId(reverseSplitPosition(position), hideTaskToken); final ComponentName component = intent.getIntent().getComponent(); @@ -900,7 +901,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, } } - return taskInfo != null ? SplitScreenUtils.getPackageName(taskInfo.baseIntent) : null; + return taskInfo != null ? ComponentUtils.getPackageName(taskInfo.baseIntent) : null; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 5aa329108596..c27ef295db51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -131,6 +131,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -140,7 +141,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.OffscreenTouchZone; import com.android.wm.shell.common.split.SplitDecorManager; import com.android.wm.shell.common.split.SplitLayout; -import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.desktopmode.DesktopTasksController; @@ -2974,9 +2974,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int transitType = info.getType(); TransitionInfo.Change pipChange = null; int closingSplitTaskId = -1; - // This array tracks if we are sending stages TO_BACK in this transition. - // TODO (b/349828130): Update for n apps - boolean[] stagesSentToBack = new boolean[2]; + // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition. + // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1). + // Also make sure having multiple changes per stage (2+ tasks in one stage) is being + // handled properly. + int[] stageChanges = new int[2]; for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); @@ -3040,18 +3042,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, + " with " + taskId + " before startAnimation()."); } } - if (isClosingType(change.getMode()) && - getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED) { - - // Record which stages are getting sent to back - if (change.getMode() == TRANSIT_TO_BACK) { - stagesSentToBack[getStageOfTask(taskId)] = true; - } + final int stageOfTaskId = getStageOfTask(taskId); + if (stageOfTaskId == STAGE_TYPE_UNDEFINED) { + continue; + } + if (isClosingType(change.getMode())) { // (For PiP transitions) If either one of the 2 stages is closing we're assuming // we'll break split closingSplitTaskId = taskId; } + if (transitType == WindowManager.TRANSIT_WAKE) { + // Record which stages are receiving which changes + if ((change.getMode() == TRANSIT_TO_BACK + || change.getMode() == TRANSIT_TO_FRONT) + && (stageOfTaskId == STAGE_TYPE_MAIN + || stageOfTaskId == STAGE_TYPE_SIDE)) { + stageChanges[stageOfTaskId] = change.getMode(); + } + } } if (pipChange != null) { @@ -3076,19 +3085,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return true; } - // If keyguard is active, check to see if we have our TO_BACK transitions in order. - // This array should either be all false (no split stages sent to back) or all true - // (all stages sent to back). In any other case (which can happen with SHOW_ABOVE_LOCKED - // apps) we should break split. - if (mKeyguardActive) { - boolean isFirstStageSentToBack = stagesSentToBack[0]; - for (boolean b : stagesSentToBack) { - // Compare each boolean to the first one. If any are different, break split. - if (b != isFirstStageSentToBack) { - dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); - break; - } - } + // If keyguard is active, check to see if we have all our stages showing. If one stage + // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should + // break split. + if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); } final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage(); @@ -3535,12 +3536,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mSplitRequest.mActivateTaskId == taskInfo.taskId) { return mSplitRequest.mActivatePosition; } - final String packageName1 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent); - final String basePackageName = SplitScreenUtils.getPackageName(taskInfo.baseIntent); + final String packageName1 = ComponentUtils.getPackageName(mSplitRequest.mStartIntent); + final String basePackageName = ComponentUtils.getPackageName(taskInfo.baseIntent); if (packageName1 != null && packageName1.equals(basePackageName)) { return mSplitRequest.mActivatePosition; } - final String packageName2 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent2); + final String packageName2 = ComponentUtils.getPackageName(mSplitRequest.mStartIntent2); if (packageName2 != null && packageName2.equals(basePackageName)) { return mSplitRequest.mActivatePosition; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 8dd14983e122..5c7dd078ee45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -536,9 +536,12 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { } return; } + // Cache it to avoid NPE and make sure to remove it from recents history. + // mTaskToken can be cleared in onTaskVanished() when the task is removed. + final WindowContainerToken taskToken = mTaskToken; mShellExecutor.execute(() -> { WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.removeTask(mTaskToken); + wct.removeTask(taskToken); mTaskViewTransitions.closeTaskView(wct, this); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 41c0a11827bb..2177986bccd5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -43,7 +43,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; -import com.android.wm.shell.common.split.SplitScreenUtils; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; @@ -645,7 +645,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, // task enter split. if (mPipHandler != null) { return mPipHandler - .isPackageActiveInPip(SplitScreenUtils.getPackageName(intent.getIntent())); + .isPackageActiveInPip(ComponentUtils.getPackageName(intent.getIntent())); } return false; } @@ -657,7 +657,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, // task enter split. if (mPipHandler != null) { return mPipHandler.isPackageActiveInPip( - SplitScreenUtils.getPackageName(taskId, shellTaskOrganizer)); + ComponentUtils.getPackageName(taskId, shellTaskOrganizer)); } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java index d8884f6d8d38..f5aaaad93229 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -33,6 +33,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransactionPool; import java.util.ArrayList; +import java.util.function.Consumer; public class DefaultSurfaceAnimator { @@ -58,42 +59,12 @@ public class DefaultSurfaceAnimator { // Animation length is already expected to be scaled. va.overrideDurationScale(1.0f); va.setDuration(anim.computeDurationHint()); - va.addUpdateListener(updateListener); - va.addListener(new AnimatorListenerAdapter() { - // It is possible for the end/cancel to be called more than once, which may cause - // issues if the animating surface has already been released. Track the finished - // state here to skip duplicate callbacks. See b/252872225. - private boolean mFinished; - - @Override - public void onAnimationEnd(Animator animation) { - onFinish(); - } - - @Override - public void onAnimationCancel(Animator animation) { - onFinish(); - } - - private void onFinish() { - if (mFinished) return; - mFinished = true; - // Apply transformation of end state in case the animation is canceled. - if (va.getAnimatedFraction() < 1f) { - va.setCurrentFraction(1f); - } - - pool.release(transaction); - mainExecutor.execute(() -> { - animations.remove(va); - finishCallback.run(); - }); - // The update listener can continue to be called after the animation has ended if - // end() is called manually again before the finisher removes the animation. - // Remove it manually here to prevent animating a released surface. - // See b/252872225. - va.removeUpdateListener(updateListener); - } + setupValueAnimator(va, updateListener, (vanim) -> { + pool.release(transaction); + mainExecutor.execute(() -> { + animations.remove(vanim); + finishCallback.run(); + }); }); animations.add(va); } @@ -188,4 +159,50 @@ public class DefaultSurfaceAnimator { } } } + + /** + * Setup some callback logic on a value-animator. This helper ensures that a value animator + * finishes at its final fraction (1f) and that relevant callbacks are only called once. + */ + public static ValueAnimator setupValueAnimator(ValueAnimator animator, + ValueAnimator.AnimatorUpdateListener updateListener, + Consumer<ValueAnimator> afterFinish) { + animator.addUpdateListener(updateListener); + animator.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. + private boolean mFinished; + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + onFinish(); + } + + @Override + public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { + if (mFinished) return; + mFinished = true; + // Apply transformation of end state in case the animation is canceled. + if (animator.getAnimatedFraction() < 1f) { + animator.setCurrentFraction(1f); + } + afterFinish.accept(animator); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + animator.removeUpdateListener(updateListener); + } + }); + return animator; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 1689bb5778ae..36c3e9711f5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -55,6 +55,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; @@ -69,6 +70,7 @@ import static com.android.wm.shell.transition.TransitionAnimationHelper.isCovere import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation; import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -104,6 +106,7 @@ import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.animation.SizeChangeAnimation; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -422,6 +425,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish); continue; } + + if (Flags.portWindowSizeAnimation() && isTask + && TransitionInfo.isIndependent(change, info) + && change.getSnapshot() != null) { + startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish, + mMainExecutor); + continue; + } } // Hide the invisible surface directly without animating it if there is a display @@ -734,6 +745,21 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } + private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT, + @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change, + @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) { + final SizeChangeAnimation sca = + new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds()); + sca.initialize(change.getLeash(), change.getSnapshot(), startT); + final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(), + (animator) -> mainExecutor.execute(() -> { + animations.remove(animator); + finishCb.run(); + })); + va.setDuration(DEFAULT_APP_TRANSITION_DURATION); + animations.add(va); + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragExistingWindowsTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragExistingWindowsTest.kt new file mode 100644 index 000000000000..2b26bbfb68cb --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragExistingWindowsTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.EnterDesktopWithDragExistingWindows +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [EnterDesktopWithDragExistingWindows]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class EnterDesktopWithDragExistingWindowsTest : EnterDesktopWithDragExistingWindows() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenuExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenuExistingWindows.kt new file mode 100644 index 000000000000..2de0830dedb5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenuExistingWindows.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class EnterDesktopWithAppHandleMenuExistingWindows { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val imeApp = ImeAppHelper(instrumentation) + private val newTaskApp = NewTasksAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopMode(wmHelper, device) + imeApp.launchViaIntent(wmHelper) + newTaskApp.launchViaIntent(wmHelper) + testApp.launchViaIntent(wmHelper) + testApp.exitDesktopWithDragToTopDragZone(wmHelper, device) + } + + @Test + open fun reenterDesktopWithAppHandleMenu() { + testApp.enterDesktopModeFromAppHandleMenu(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + newTaskApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt new file mode 100644 index 000000000000..814478af67c1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class EnterDesktopWithDragExistingWindows +constructor( + val rotation: Rotation = Rotation.ROTATION_0, + isResizeable: Boolean = true, + isLandscapeApp: Boolean = true, +) : DesktopScenarioCustomAppTestBase(isResizeable, isLandscapeApp) { + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + private val imeApp = ImeAppHelper(instrumentation) + private val newTaskApp = NewTasksAppHelper(instrumentation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + ChangeDisplayOrientationRule.setRotation(rotation) + tapl.enableTransientTaskbar(false) + + testApp.enterDesktopMode(wmHelper, device) + imeApp.launchViaIntent(wmHelper) + newTaskApp.launchViaIntent(wmHelper) + testApp.launchViaIntent(wmHelper) + testApp.exitDesktopWithDragToTopDragZone(wmHelper, device) + } + + @Test + open fun reenterDesktopWithDrag() { + // By default this method uses drag to desktop + testApp.enterDesktopMode(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + newTaskApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt index ba4654260864..31d89f92f744 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -61,7 +62,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt index d774a31220da..1af6cac39085 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -74,7 +75,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt index 5aa161911a9a..8ad8c7bd7a7f 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -60,7 +61,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt index 668f3678bb38..da0ace472153 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -61,7 +62,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt index 636549fa0662..a6f8150ffc55 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt @@ -176,7 +176,6 @@ open class EnterPipToOtherOrientation(flicker: LegacyFlickerTest) : PipTransitio * transition */ @Ignore("TODO(b/356277166): enable the tablet test") - @Postsubmit @Test open fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { assumeTrue(tapl.isTablet) diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt index 4987ab7b9344..d65f158e00d6 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipToOtherOrientation.kt @@ -78,7 +78,6 @@ class BottomHalfEnterPipToOtherOrientation(flicker: LegacyFlickerTest) : } @Ignore("TODO(b/356277166): enable the tablet test") - @Presubmit @Test override fun pipAppLayerPlusLetterboxCoversFullScreenOnStartTablet() { // Test app and pip app should covers the entire screen on start. diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt new file mode 100644 index 000000000000..aa262f9dfd6a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.utils + +import android.app.Instrumentation + +object RecentTasksUtils { + fun clearAllVisibleRecentTasks(instrumentation: Instrumentation) { + instrumentation.uiAutomation.executeShellCommand( + "dumpsys activity service SystemUIService WMShell recents clearAll" + ) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java new file mode 100644 index 000000000000..e91a1238a390 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/appzoomout/AppZoomOutControllerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.appzoomout; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.testing.AndroidTestingRunner; +import android.view.Display; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.sysui.ShellInit; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class AppZoomOutControllerTest extends ShellTestCase { + + @Mock private ShellTaskOrganizer mTaskOrganizer; + @Mock private DisplayController mDisplayController; + @Mock private AppZoomOutDisplayAreaOrganizer mDisplayAreaOrganizer; + @Mock private ShellExecutor mExecutor; + @Mock private ActivityManager.RunningTaskInfo mRunningTaskInfo; + + private AppZoomOutController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + Display display = mContext.getDisplay(); + DisplayLayout displayLayout = new DisplayLayout(mContext, display); + when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(displayLayout); + + ShellInit shellInit = spy(new ShellInit(mExecutor)); + mController = spy(new AppZoomOutController(mContext, shellInit, mTaskOrganizer, + mDisplayController, mDisplayAreaOrganizer, mExecutor)); + } + + @Test + public void isHomeTaskFocused_zoomOutForHome() { + mRunningTaskInfo.isFocused = true; + when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME); + mController.onFocusTaskChanged(mRunningTaskInfo); + + verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(true); + } + + @Test + public void isHomeTaskNotFocused_zoomOutForApp() { + mRunningTaskInfo.isFocused = false; + when(mRunningTaskInfo.getActivityType()).thenReturn(ACTIVITY_TYPE_HOME); + mController.onFocusTaskChanged(mRunningTaskInfo); + + verify(mDisplayAreaOrganizer).setIsHomeTaskFocused(false); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index 1e5e153fdfe1..d3de0f7c09b4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.hardware.display.DisplayManager; import android.view.IWindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -50,12 +51,14 @@ public class DisplayControllerTests extends ShellTestCase { private @Mock IWindowManager mWM; private @Mock ShellInit mShellInit; private @Mock ShellExecutor mMainExecutor; + private @Mock DisplayManager mDisplayManager; private DisplayController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor); + mController = new DisplayController( + mContext, mWM, mShellInit, mMainExecutor, mDisplayManager); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java index d467b399ebbb..b0a455d1bcf8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java @@ -33,7 +33,9 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Insets; +import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.RectF; import android.view.DisplayCutout; import android.view.DisplayInfo; @@ -58,6 +60,7 @@ import org.mockito.quality.Strictness; @SmallTest public class DisplayLayoutTest extends ShellTestCase { private MockitoSession mMockitoSession; + private static final float DELTA = 0.1f; // Constant for assertion delta @Before public void setup() { @@ -130,6 +133,39 @@ public class DisplayLayoutTest extends ShellTestCase { assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets()); } + @Test + public void testDpPxConversion() { + int px = 100; + float dp = 53.33f; + int xPx = 100; + int yPx = 200; + float xDp = 164.33f; + float yDp = 328.66f; + + Resources res = createResources(40, 50, false); + DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0); + DisplayLayout dl = new DisplayLayout(info, res, false, false); + dl.setGlobalBoundsDp(new RectF(111f, 222f, 300f, 400f)); + + // Test pxToDp + float resultDp = dl.pxToDp(px); + assertEquals(dp, resultDp, DELTA); + + // Test dpToPx + float resultPx = dl.dpToPx(dp); + assertEquals(px, resultPx, DELTA); + + // Test localPxToGlobalDp + PointF resultGlobalDp = dl.localPxToGlobalDp(xPx, yPx); + assertEquals(xDp, resultGlobalDp.x, DELTA); + assertEquals(yDp, resultGlobalDp.y, DELTA); + + // Test globalDpToLocalPx + PointF resultLocalPx = dl.globalDpToLocalPx(xDp, yDp); + assertEquals(xPx, resultLocalPx.x, DELTA); + assertEquals(yPx, resultLocalPx.y, DELTA); + } + private Resources createResources(int navLand, int navPort, boolean navMoves) { Configuration cfg = new Configuration(); cfg.uiMode = UI_MODE_TYPE_NORMAL; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 784e1907894d..b5c9fa151dac 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -62,11 +62,12 @@ import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import dagger.Lazy; +import java.util.Optional; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -76,8 +77,6 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.Optional; - /** * Tests for {@link CompatUIController}. * @@ -128,8 +127,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { private DesktopUserRepositories mDesktopUserRepositories; @Mock private DesktopRepository mDesktopRepository; - @Mock - private FocusTransitionObserver mFocusTransitionObserver; @Captor ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; @@ -165,8 +162,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { mMockDisplayController, mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader, mCompatUIConfiguration, mCompatUIShellCommandHandler, mAccessibilityManager, - mCompatUIStatusManager, Optional.of(mDesktopUserRepositories), - mFocusTransitionObserver) { + mCompatUIStatusManager, Optional.of(mDesktopUserRepositories)) { @Override CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) { @@ -284,7 +280,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { doReturn(false).when(mMockRestartDialogLayout).updateCompatInfo(any(), any(), anyBoolean()); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); verify(mController).createCompatUiWindowManager(any(), eq(taskInfo), eq(mMockTaskListener)); @@ -416,7 +411,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Verify button remains hidden while IME is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -449,7 +443,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Verify button remains hidden while keyguard is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); verify(mMockCompatLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); @@ -530,7 +523,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutRecreatedIfNeeded() { final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); doReturn(true).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); @@ -546,7 +538,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK) public void testRestartLayoutNotRecreatedIfNotNeeded() { final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); doReturn(false).when(mMockRestartDialogLayout) .needsToBeRecreated(any(TaskInfo.class), any(ShellTaskOrganizer.TaskListener.class)); @@ -567,8 +558,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -584,8 +574,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_newTask_notVisibleOrFocused_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate task being shown mController.updateActiveTaskInfo(taskInfo); @@ -603,8 +592,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create visible but NOT focused task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); + /* isVisible */ true, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -616,8 +604,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create focused but NOT visible task final TaskInfo taskInfo2 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ false, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo2); @@ -629,8 +616,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create NOT focused but NOT visible task final TaskInfo taskInfo3 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); + /* isVisible */ false, /* isFocused */ false); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo3); @@ -646,8 +632,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_sameTask_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -675,8 +660,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_transparentTask_notUpdated() { // Create new task final TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, - /* isVisible */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo); @@ -694,8 +678,7 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { // Create transparent task final TaskInfo taskInfo1 = createTaskInfo(DISPLAY_ID, newTaskId, /* hasSizeCompat= */ true, - /* isVisible */ true, /* isTopActivityTransparent */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(true); + /* isVisible */ true, /* isFocused */ true, /* isTopActivityTransparent */ true); // Simulate new task being shown mController.updateActiveTaskInfo(taskInfo1); @@ -711,7 +694,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(false); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -725,7 +707,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagEnabled() { TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -744,7 +725,6 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { public void testUpdateActiveTaskInfo_removeAllComponentWhenInDesktopModeFlagDisabled() { when(mDesktopUserRepositories.getCurrent().getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true); - when(mFocusTransitionObserver.hasGlobalFocus((RunningTaskInfo) taskInfo)).thenReturn(false); mController.onCompatInfoChanged(new CompatUIInfo(taskInfo, mMockTaskListener)); @@ -759,22 +739,23 @@ public class CompatUIControllerTest extends CompatUIShellTestCase { private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) { return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false, - /* isTopActivityTransparent */ false); + /* isFocused */ false, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible) { + boolean isVisible, boolean isFocused) { return createTaskInfo(displayId, taskId, hasSizeCompat, - isVisible, /* isTopActivityTransparent */ false); + isVisible, isFocused, /* isTopActivityTransparent */ false); } private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat, - boolean isVisible, boolean isTopActivityTransparent) { + boolean isVisible, boolean isFocused, boolean isTopActivityTransparent) { RunningTaskInfo taskInfo = new RunningTaskInfo(); taskInfo.taskId = taskId; taskInfo.displayId = displayId; taskInfo.appCompatTaskInfo.setTopActivityInSizeCompat(hasSizeCompat); taskInfo.isVisible = isVisible; + taskInfo.isFocused = isFocused; taskInfo.isTopActivityTransparent = isTopActivityTransparent; taskInfo.appCompatTaskInfo.setLetterboxEducationEnabled(true); taskInfo.appCompatTaskInfo.setTopActivityLetterboxed(true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index c0ff2f0652b3..eb6f1d75e6ca 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -52,6 +52,7 @@ import org.junit.Test import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.whenever /** Tests for [DesktopModeEventLogger]. */ @@ -90,20 +91,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val sessionId = desktopModeEventLogger.currentSessionId.get() assertThat(sessionId).isNotEqualTo(NO_SESSION_ID) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER), - /* enter_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER), - /* exit_reason */ - eq(0), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER, + 0, + sessionId, + ) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), @@ -122,20 +115,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val sessionId = desktopModeEventLogger.currentSessionId.get() assertThat(sessionId).isNotEqualTo(NO_SESSION_ID) assertThat(sessionId).isNotEqualTo(previousSessionId) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER), - /* enter_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER), - /* exit_reason */ - eq(0), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER, + /* exit_reason */ + 0, + sessionId, + ) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), @@ -149,7 +135,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logSessionExit_noOngoingSession_doesNotLog() { desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -159,20 +145,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logSessionExit(ExitReason.DRAG_TO_EXIT) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), - /* event */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT), - /* enter_reason */ - eq(0), - /* exit_reason */ - eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT), - /* sessionId */ - eq(sessionId), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneUiChangedLogging( + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT, + /* enter_reason */ + 0, + FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT, + sessionId, + ) verify { EventLogTags.writeWmShellExitDesktopMode( eq(ExitReason.DRAG_TO_EXIT.reason), @@ -187,7 +166,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskAdded_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskAdded(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -197,32 +176,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskAdded(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), @@ -245,7 +211,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskRemoved_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskRemoved(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -255,32 +221,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskRemoved(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), @@ -303,7 +256,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskInfoChanged_noOngoingSession_doesNotLog() { desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -313,35 +266,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { desktopModeEventLogger.logTaskInfoChanged(TASK_UPDATE) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - eq(UNSET_MINIMIZE_REASON), - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -371,37 +308,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskUpdate(minimizeReason = MinimizeReason.TASK_LIMIT) ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - /* minimize_reason */ - eq(MinimizeReason.TASK_LIMIT.reason), - /* unminimize_reason */ - eq(UNSET_UNMINIMIZE_REASON), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + MinimizeReason.TASK_LIMIT.reason, + UNSET_UNMINIMIZE_REASON, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -431,37 +350,19 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskUpdate(unminimizeReason = UnminimizeReason.TASKBAR_TAP) ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), - /* task_event */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED - ), - /* instance_id */ - eq(TASK_UPDATE.instanceId), - /* uid */ - eq(TASK_UPDATE.uid), - /* task_height */ - eq(TASK_UPDATE.taskHeight), - /* task_width */ - eq(TASK_UPDATE.taskWidth), - /* task_x */ - eq(TASK_UPDATE.taskX), - /* task_y */ - eq(TASK_UPDATE.taskY), - /* session_id */ - eq(sessionId), - /* minimize_reason */ - eq(UNSET_MINIMIZE_REASON), - /* unminimize_reason */ - eq(UnminimizeReason.TASKBAR_TAP.reason), - /* visible_task_count */ - eq(TASK_COUNT), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskUpdateLogging( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED, + TASK_UPDATE.instanceId, + TASK_UPDATE.uid, + TASK_UPDATE.taskHeight, + TASK_UPDATE.taskWidth, + TASK_UPDATE.taskX, + TASK_UPDATE.taskY, + sessionId, + UNSET_MINIMIZE_REASON, + UnminimizeReason.TASKBAR_TAP.reason, + TASK_COUNT, + ) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( eq( @@ -491,7 +392,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskInfo(), ) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -509,39 +410,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() { displayController, ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), - /* resize_trigger */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER - ), - /* resizing_stage */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE - ), - /* input_method */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD - ), - /* desktop_mode_session_id */ - eq(sessionId), - /* instance_id */ - eq(TASK_SIZE_UPDATE.instanceId), - /* uid */ - eq(TASK_SIZE_UPDATE.uid), - /* task_width */ - eq(TASK_SIZE_UPDATE.taskWidth), - /* task_height */ - eq(TASK_SIZE_UPDATE.taskHeight), - /* display_area */ - eq(DISPLAY_AREA), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskSizeUpdatedLogging( + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD, + sessionId, + TASK_SIZE_UPDATE.instanceId, + TASK_SIZE_UPDATE.uid, + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + DISPLAY_AREA, + ) } @Test @@ -552,7 +431,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { createTaskInfo(), ) - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyNoLogging() verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) } @@ -568,39 +447,17 @@ class DesktopModeEventLoggerTest : ShellTestCase() { displayController = displayController, ) - verify { - FrameworkStatsLog.write( - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), - /* resize_trigger */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER - ), - /* resizing_stage */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE - ), - /* input_method */ - eq( - FrameworkStatsLog - .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD - ), - /* desktop_mode_session_id */ - eq(sessionId), - /* instance_id */ - eq(TASK_SIZE_UPDATE.instanceId), - /* uid */ - eq(TASK_SIZE_UPDATE.uid), - /* task_width */ - eq(TASK_SIZE_UPDATE.taskWidth), - /* task_height */ - eq(TASK_SIZE_UPDATE.taskHeight), - /* display_area */ - eq(DISPLAY_AREA), - ) - } - verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) + verifyOnlyOneTaskSizeUpdatedLogging( + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE, + FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD, + sessionId, + TASK_SIZE_UPDATE.instanceId, + TASK_SIZE_UPDATE.uid, + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + DISPLAY_AREA, + ) } private fun startDesktopModeSession(): Int { @@ -644,12 +501,176 @@ class DesktopModeEventLoggerTest : ShellTestCase() { } } - private fun createTaskInfo(): RunningTaskInfo { - return TestRunningTaskInfoBuilder() + private fun createTaskInfo(): RunningTaskInfo = + TestRunningTaskInfoBuilder() .setTaskId(TASK_ID) .setUid(TASK_UID) .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT)) .build() + + private fun verifyNoLogging() { + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + verify( + { + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }, + never(), + ) + } + + private fun verifyOnlyOneUiChangedLogging( + event: Int, + enterReason: Int, + exitReason: Int, + sessionId: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + eq(event), + eq(enterReason), + eq(exitReason), + eq(sessionId), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + + private fun verifyOnlyOneTaskUpdateLogging( + taskEvent: Int, + instanceId: Int, + uid: Int, + taskHeight: Int, + taskWidth: Int, + taskX: Int, + taskY: Int, + sessionId: Int, + minimizeReason: Int, + unminimizeReason: Int, + visibleTaskCount: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + eq(taskEvent), + eq(instanceId), + eq(uid), + eq(taskHeight), + eq(taskWidth), + eq(taskX), + eq(taskY), + eq(sessionId), + eq(minimizeReason), + eq(unminimizeReason), + eq(visibleTaskCount), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) + } + + private fun verifyOnlyOneTaskSizeUpdatedLogging( + resizeTrigger: Int, + resizingStage: Int, + inputMethod: Int, + sessionId: Int, + instanceId: Int, + uid: Int, + taskWidth: Int, + taskHeight: Int, + displayArea: Int, + ) { + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + eq(resizeTrigger), + eq(resizingStage), + eq(inputMethod), + eq(sessionId), + eq(instanceId), + eq(uid), + eq(taskWidth), + eq(taskHeight), + eq(displayArea), + ) + }) + verify({ + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + ) + }) } private companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 5629127b8c54..6003a219d4db 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -25,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor @@ -668,10 +669,10 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() { + fun removeTask_invalidDisplay_removesTaskFromFreeformTasks() { repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) + repo.removeTask(INVALID_DISPLAY, taskId = 1) val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY) assertThat(invalidDisplayTasks).isEmpty() @@ -681,11 +682,11 @@ class DesktopRepositoryTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - fun removeFreeformTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { + fun removeTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { runTest(StandardTestDispatcher()) { repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) + repo.removeTask(INVALID_DISPLAY, taskId = 1) verify(persistentRepository) .addOrUpdateDesktop( @@ -707,10 +708,10 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() { + fun removeTask_validDisplay_removesTaskFromFreeformTasks() { repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) + repo.removeTask(DEFAULT_DISPLAY, taskId = 1) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks).isEmpty() @@ -718,11 +719,11 @@ class DesktopRepositoryTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - fun removeFreeformTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { + fun removeTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { runTest(StandardTestDispatcher()) { repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) + repo.removeTask(DEFAULT_DISPLAY, taskId = 1) verify(persistentRepository) .addOrUpdateDesktop( @@ -744,10 +745,10 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() { + fun removeTask_validDisplay_differentDisplay_doesNotRemovesTask() { repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) + repo.removeTask(SECOND_DISPLAY, taskId = 1) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks).containsExactly(1) @@ -755,11 +756,11 @@ class DesktopRepositoryTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - fun removeFreeformTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { + fun removeTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { runTest(StandardTestDispatcher()) { repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) + repo.removeTask(SECOND_DISPLAY, taskId = 1) verify(persistentRepository) .addOrUpdateDesktop( @@ -781,57 +782,57 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { + fun removeTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) - repo.removeFreeformTask(THIRD_DISPLAY, taskId) + repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() } @Test - fun removeFreeformTask_removesTaskBoundsBeforeImmersive() { + fun removeTask_removesTaskBoundsBeforeImmersive() { val taskId = 1 repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeFullImmersive(taskId, Rect(0, 0, 200, 200)) - repo.removeFreeformTask(THIRD_DISPLAY, taskId) + repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.removeBoundsBeforeFullImmersive(taskId)).isNull() } @Test - fun removeFreeformTask_removesActiveTask() { + fun removeTask_removesActiveTask() { val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) - repo.removeFreeformTask(THIRD_DISPLAY, taskId) + repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.isActiveTask(taskId)).isFalse() assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) } @Test - fun removeFreeformTask_unminimizesTask() { + fun removeTask_unminimizesTask() { val taskId = 1 repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.minimizeTask(DEFAULT_DISPLAY, taskId) - repo.removeFreeformTask(DEFAULT_DISPLAY, taskId) + repo.removeTask(DEFAULT_DISPLAY, taskId) assertThat(repo.isMinimizedTask(taskId)).isFalse() } @Test - fun removeFreeformTask_updatesTaskVisibility() { + fun removeTask_updatesTaskVisibility() { val taskId = 1 repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) - repo.removeFreeformTask(THIRD_DISPLAY, taskId) + repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.isVisibleTask(taskId)).isFalse() } @@ -1067,6 +1068,67 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2) } + @Test + fun setTaskInPip_savedAsMinimizedPipInDisplay() { + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + } + + @Test + fun removeTaskInPip_removedAsMinimizedPipInDisplay() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isFalse() + } + + @Test + fun setTaskInPip_multipleDisplays_bothAreInPip() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true) + + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() + assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun setPipShouldKeepDesktopActive_shouldKeepDesktopActive() { + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = true) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun setPipShouldNotKeepDesktopActive_shouldNotKeepDesktopActive() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + + repo.setPipShouldKeepDesktopActive(DEFAULT_DESKTOP_ID, keepActive = false) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun removeTaskInPip_shouldNotKeepDesktopActive() { + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isTrue() + + repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = false) + + assertThat(repo.shouldDesktopBeActiveForPip(DEFAULT_DESKTOP_ID)).isFalse() + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt index 19ab9113bc7a..c7c0dfc5be6d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt @@ -68,8 +68,7 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { verify(desktopUserRepositories.current, never()) .addTask(task.displayId, task.taskId, task.isVisible) - verify(desktopUserRepositories.current, never()) - .removeFreeformTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current, never()).removeTask(task.displayId, task.taskId) } @Test @@ -79,7 +78,7 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { desktopTaskChangeListener.onTaskOpening(task) - verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId) } @Test @@ -109,7 +108,7 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { desktopTaskChangeListener.onTaskChanging(task) - verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId) } @Test @@ -141,7 +140,7 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { desktopTaskChangeListener.onTaskMovingToFront(task) - verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId) } @Test @@ -169,7 +168,7 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { verify(desktopUserRepositories.current, never()).minimizeTask(task.displayId, task.taskId) verify(desktopUserRepositories.current).removeClosingTask(task.taskId) - verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId) } @Test @@ -182,6 +181,6 @@ class DesktopTaskChangeListenerTest : ShellTestCase() { desktopTaskChangeListener.onTaskClosing(task) verify(desktopUserRepositories.current).removeClosingTask(task.taskId) - verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current).removeTask(task.displayId, task.taskId) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 692b50303038..95ed8b4d4511 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -82,6 +82,7 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT @@ -352,8 +353,8 @@ class DesktopTasksControllerTest : ShellTestCase() { taskRepository = userRepositories.current } - private fun createController(): DesktopTasksController { - return DesktopTasksController( + private fun createController() = + DesktopTasksController( context, shellInit, shellCommandHandler, @@ -387,7 +388,6 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), ) - } @After fun tearDown() { @@ -569,6 +569,38 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_minimizedPipTask_wallpaperVisible_returnsTrue() { + val pipTask = setUpPipTask(autoEnterEnabled = true) + whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible()) + .thenReturn(true) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_minimizedPipTask_wallpaperNotVisible_returnsFalse() { + val pipTask = setUpPipTask(autoEnterEnabled = true) + whenever(desktopWallpaperActivityTokenProvider.isWallpaperActivityVisible()) + .thenReturn(false) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun isDesktopModeShowing_pipTaskNotMinimizedNorVisible_returnsFalse() { + setUpPipTask(autoEnterEnabled = true) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isFalse() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { val homeTask = setUpHomeTask(SECOND_DISPLAY) @@ -2039,6 +2071,41 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopWindowClose_minimizedPipPresent_doesNotExitDesktop() { + val freeformTask = setUpFreeformTask().apply { isFocused = true } + val pipTask = setUpPipTask(autoEnterEnabled = true) + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = true) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) + + verifyExitDesktopWCTNotExecuted() + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() { + val freeformTask = setUpFreeformTask() + val pipTask = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(pipTask) + verifyExitDesktopWCTNotExecuted() + + taskRepository.setTaskInPip(DEFAULT_DISPLAY, pipTask.taskId, enterPip = false) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) + + // Remove wallpaper operation + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = false) val transition = Binder() @@ -2055,10 +2122,9 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() { + fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) - whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder()) whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) @@ -2069,7 +2135,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() { + fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() { val task = setUpPipTask(autoEnterEnabled = false) whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) .thenReturn(Binder()) @@ -2081,6 +2147,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun onPipTaskMinimize_doesntRemoveWallpaper() { + val task = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = true) val transition = Binder() @@ -3125,6 +3207,31 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() { + val freeformTask = setUpFreeformTask() + val pipTask = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(pipTask) + verifyExitDesktopWCTNotExecuted() + + freeformTask.isFocused = true + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Remove wallpaper operation + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeDesktop_multipleTasks_removesAll() { val task1 = setUpFreeformTask() @@ -4850,12 +4957,12 @@ class DesktopTasksControllerTest : ShellTestCase() { return task } - private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { - return setUpFreeformTask().apply { + private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo = + // active = false marks the task as non-visible; PiP window doesn't count as visible tasks + setUpFreeformTask(active = false).apply { pictureInPictureParams = PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() } - } private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createHomeTask(displayId) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 89ab65a42bbf..ca1e3edb3fd3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -22,6 +22,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Binder import android.os.IBinder import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule @@ -29,7 +30,9 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_BACK +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IWindowContainerToken import android.window.TransitionInfo import android.window.TransitionInfo.Change @@ -38,6 +41,7 @@ import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.MockToken import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController @@ -47,6 +51,8 @@ import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpape import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP +import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -217,7 +223,7 @@ class DesktopTasksTransitionObserverTest { ) verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId) - verify(taskRepository).removeFreeformTask(task.displayId, task.taskId) + verify(taskRepository).removeTask(task.displayId, task.taskId) } @Test @@ -235,7 +241,7 @@ class DesktopTasksTransitionObserverTest { ) verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId) - verify(taskRepository).removeFreeformTask(task.displayId, task.taskId) + verify(taskRepository).removeTask(task.displayId, task.taskId) } @Test @@ -300,6 +306,115 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId) } + @Test + fun transitOpenWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId) + } + + @Test + fun transitToFrontWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createToFrontTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = true, wallpaperTask.displayId) + } + + @Test + fun transitToBackWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createToBackTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider) + .setWallpaperActivityIsVisible(isVisible = false, wallpaperTask.displayId) + } + + @Test + fun transitCloseWallpaper_wallpaperActivityVisibilitySaved() { + val wallpaperTask = createWallpaperTaskInfo() + + transitionObserver.onTransitionReady( + transition = mock(), + info = createCloseTransition(wallpaperTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(desktopWallpaperActivityTokenProvider).removeToken(wallpaperTask.displayId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun pendingPipTransitionAborted_taskRepositoryOnPipAbortedInvoked() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + val pipTransition = Binder() + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = pipTransition, + info = createOpenChangeTransition(task, TRANSIT_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + transitionObserver.onTransitionFinished(transition = pipTransition, aborted = true) + + verify(taskRepository).onPipAborted(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun exitPipTransition_taskRepositoryClearTaskInPip() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(task, type = TRANSIT_EXIT_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + fun removePipTransition_taskRepositoryClearTaskInPip() { + val task = createTaskInfo(1, WINDOWING_MODE_FREEFORM) + whenever(taskRepository.isTaskMinimizedPipInDisplay(any(), any())).thenReturn(true) + + transitionObserver.onTransitionReady( + transition = mock(), + info = createOpenChangeTransition(task, type = TRANSIT_REMOVE_PIP), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, @@ -331,7 +446,7 @@ class DesktopTasksTransitionObserverTest { task: RunningTaskInfo?, type: Int = TRANSIT_OPEN, ): TransitionInfo { - return TransitionInfo(TRANSIT_OPEN, /* flags= */ 0).apply { + return TransitionInfo(type, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_OPEN @@ -343,8 +458,8 @@ class DesktopTasksTransitionObserverTest { } } - private fun createCloseTransition(task: RunningTaskInfo?): TransitionInfo { - return TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply { + private fun createCloseTransition(task: RunningTaskInfo?) = + TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_CLOSE @@ -354,10 +469,9 @@ class DesktopTasksTransitionObserverTest { } ) } - } - private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo { - return TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply { + private fun createToBackTransition(task: RunningTaskInfo?) = + TransitionInfo(TRANSIT_TO_BACK, /* flags= */ 0).apply { addChange( Change(mock(), mock()).apply { mode = TRANSIT_TO_BACK @@ -367,6 +481,18 @@ class DesktopTasksTransitionObserverTest { } ) } + + private fun createToFrontTransition(task: RunningTaskInfo?): TransitionInfo { + return TransitionInfo(TRANSIT_TO_FRONT, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_FRONT + parent = null + taskInfo = task + flags = flags + } + ) + } } private fun getLatestWct( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 341df0299a97..bf9cf00050dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -676,8 +676,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } } - private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo): TransitionInfo { - return TransitionInfo(type, /* flags= */ 0).apply { + private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) = + TransitionInfo(type, /* flags= */ 0).apply { addChange( // Home. TransitionInfo.Change(mock(), homeTaskLeash).apply { parent = null @@ -700,7 +700,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } ) } - } private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt index eae206664021..5f92326b5e5e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt @@ -85,11 +85,11 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { val desk = createDesktop(task) val repositoryState = DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk) - val DesktopPersistentRepositories = + val desktopPersistentRepositories = DesktopPersistentRepositories.newBuilder() .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build()) .build() - testDatastore.updateData { DesktopPersistentRepositories } + testDatastore.updateData { desktopPersistentRepositories } val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) @@ -102,8 +102,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { runTest(StandardTestDispatcher()) { // Create a basic repository state val task = createDesktopTask(1) - val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) - testDatastore.updateData { DesktopPersistentRepositories } + val desktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { desktopPersistentRepositories } // Create a new state to be initialized val visibleTasks = ArraySet(listOf(1, 2)) val minimizedTasks = ArraySet<Int>() @@ -124,11 +124,46 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { } @Test + fun removeUsers_removesUsersData() { + runTest(StandardTestDispatcher()) { + val task = createDesktopTask(1) + val desktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { desktopPersistentRepositories } + // Create a new state to be initialized + val visibleTasks = ArraySet(listOf(1)) + val minimizedTasks = ArraySet(listOf(1)) + val freeformTasksInZOrder = ArrayList(listOf(1)) + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder, + userId = DEFAULT_USER_ID, + ) + datastoreRepository.addOrUpdateDesktop( + visibleTasks = visibleTasks, + minimizedTasks = minimizedTasks, + freeformTasksInZOrder = freeformTasksInZOrder, + userId = USER_ID_2, + ) + + datastoreRepository.removeUsers(mutableListOf(USER_ID_2)) + + val removedDesktopRepositoryState = + datastoreRepository.getDesktopRepositoryState(USER_ID_2) + assertThat(removedDesktopRepositoryState).isEqualTo(null) + + val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) + assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState) + .isEqualTo(DesktopTaskState.MINIMIZED) + } + } + + @Test fun addOrUpdateTask_changeTaskStateToMinimize_taskStateIsMinimized() { runTest(StandardTestDispatcher()) { val task = createDesktopTask(1) - val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) - testDatastore.updateData { DesktopPersistentRepositories } + val desktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { desktopPersistentRepositories } // Create a new state to be initialized val visibleTasks = ArraySet(listOf(1)) val minimizedTasks = ArraySet(listOf(1)) @@ -152,8 +187,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { fun removeTask_previouslyAddedTaskIsRemoved() { runTest(StandardTestDispatcher()) { val task = createDesktopTask(1) - val DesktopPersistentRepositories = createRepositoryWithOneDesk(task) - testDatastore.updateData { DesktopPersistentRepositories } + val desktopPersistentRepositories = createRepositoryWithOneDesk(task) + testDatastore.updateData { desktopPersistentRepositories } // Create a new state to be initialized val visibleTasks = ArraySet<Int>() val minimizedTasks = ArraySet<Int>() @@ -176,17 +211,18 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { private companion object { const val DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE = "desktop_repo_test.pb" const val DEFAULT_USER_ID = 1000 + const val USER_ID_2 = 2000 const val DEFAULT_DESKTOP_ID = 0 fun createRepositoryWithOneDesk(task: DesktopTask): DesktopPersistentRepositories { val desk = createDesktop(task) val repositoryState = DesktopRepositoryState.newBuilder().putDesktop(DEFAULT_DESKTOP_ID, desk) - val DesktopPersistentRepositories = + val desktopPersistentRepositories = DesktopPersistentRepositories.newBuilder() .putDesktopRepoByUser(DEFAULT_USER_ID, repositoryState.build()) .build() - return DesktopPersistentRepositories + return desktopPersistentRepositories } fun createDesktop(task: DesktopTask): Desktop? = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 32bb8bbdbbe3..1b1a5a909220 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -152,17 +152,16 @@ public class DragAndDropControllerTest extends ShellTestCase { } @Test - public void testOnDragStarted_withNoClipData() { + public void testOnDragStarted_withNoClipDataOrDescription() { final View dragLayout = mock(View.class); final Display display = mock(Display.class); doReturn(display).when(dragLayout).getDisplay(); doReturn(DEFAULT_DISPLAY).when(display).getDisplayId(); - final ClipData clipData = createAppClipData(MIMETYPE_APPLICATION_SHORTCUT); final DragEvent event = mock(DragEvent.class); doReturn(ACTION_DRAG_STARTED).when(event).getAction(); doReturn(null).when(event).getClipData(); - doReturn(clipData.getDescription()).when(event).getClipDescription(); + doReturn(null).when(event).getClipDescription(); // Ensure there's a target so that onDrag will execute mController.addDisplayDropTarget(0, mContext, mock(WindowManager.class), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt index d410151b4602..5389c94bc15d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt @@ -43,7 +43,7 @@ import org.mockito.kotlin.verify */ @SmallTest @RunWith(AndroidJUnit4::class) -class UnhandledDragControllerTest : ShellTestCase() { +class GlobalDragListenerTest : ShellTestCase() { private val mIWindowManager = mock<IWindowManager>() private val mMainExecutor = mock<ShellExecutor>() @@ -74,7 +74,7 @@ class UnhandledDragControllerTest : ShellTestCase() { @Test fun onUnhandledDrop_noListener_expectNotifyUnhandled() { // Simulate an unhandled drop - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null, null, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) @@ -98,7 +98,7 @@ class UnhandledDragControllerTest : ShellTestCase() { // Simulate an unhandled drop val dragSurface = mock<SurfaceControl>() - val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null, + val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, 0, null, null, null, dragSurface, null, false) val wmCallback = mock<IUnhandledDragCallback>() mController.onUnhandledDrop(dropEvent, wmCallback) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index b8629b2f00d2..fa5989a3ca99 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -224,7 +224,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { task.displayId = INVALID_DISPLAY; mFreeformTaskListener.onTaskVanished(task); - verify(mDesktopUserRepositories.getCurrent(), never()).removeFreeformTask(task.displayId, + verify(mDesktopUserRepositories.getCurrent(), never()).removeTask(task.displayId, task.taskId); } @@ -248,7 +248,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { .minimizeTask(task.displayId, task.taskId); verify(mDesktopUserRepositories.getCurrent()).removeClosingTask(task.taskId); verify(mDesktopUserRepositories.getCurrent()) - .removeFreeformTask(task.displayId, task.taskId); + .removeTask(task.displayId, task.taskId); } @Test @@ -265,7 +265,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase { verify(mDesktopUserRepositories.getCurrent(), never()) .removeClosingTask(task.taskId); verify(mDesktopUserRepositories.getCurrent(), never()) - .removeFreeformTask(task.displayId, task.taskId); + .removeTask(task.displayId, task.taskId); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index a8aa25700c7e..c42f6c35bcb0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -30,6 +30,7 @@ import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.times; import static org.mockito.kotlin.VerificationKt.verify; +import android.app.TaskInfo; import android.content.Context; import android.content.res.Resources; import android.graphics.Matrix; @@ -45,7 +46,9 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; @@ -56,6 +59,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -83,7 +87,8 @@ public class PipSchedulerTest { @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; - @Mock private Optional<DesktopUserRepositories> mMockOptionalDesktopUserRepositories; + @Mock private DesktopUserRepositories mMockDesktopUserRepositories; + @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider; @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @@ -100,9 +105,13 @@ public class PipSchedulerTest { when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) .thenReturn(mMockTransaction); + when(mMockDesktopUserRepositories.getCurrent()) + .thenReturn(Mockito.mock(DesktopRepository.class)); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class)); mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, - mMockPipTransitionState, mMockOptionalDesktopUserRepositories, + mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories), + Optional.of(mMockDesktopWallpaperActivityTokenProvider), mRootTaskDisplayAreaOrganizer); mPipScheduler.setPipTransitionController(mMockPipTransitionController); mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index e693fcfd3918..dbb891455ddd 100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -162,13 +162,10 @@ const std::string& ApkAssets::GetDebugName() const { return assets_provider_->GetDebugName(); } -UpToDate ApkAssets::IsUpToDate() const { +bool ApkAssets::IsUpToDate() const { // Loaders are invalidated by the app, not the system, so assume they are up to date. - if (IsLoader()) { - return UpToDate::Always; - } - const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always; - return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); }); + return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) + && assets_provider_->IsUpToDate()); } } // namespace android diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp index 11b12eb030a6..2d3c06506a1f 100644 --- a/libs/androidfw/AssetsProvider.cpp +++ b/libs/androidfw/AssetsProvider.cpp @@ -24,8 +24,9 @@ #include <ziparchive/zip_archive.h> namespace android { - -static constexpr std::string_view kEmptyDebugString = "<empty>"; +namespace { +constexpr const char* kEmptyDebugString = "<empty>"; +} // namespace std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, bool* file_exists) const { @@ -85,9 +86,11 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const { } ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, - package_property_t flags, ModDate last_mod_time) - : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) { -} + package_property_t flags, time_t last_mod_time) + : zip_handle_(handle), + name_(std::move(path)), + flags_(flags), + last_mod_time_(last_mod_time) {} std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, package_property_t flags, @@ -101,10 +104,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, return {}; } - ModDate mod_date = kInvalidModDate; + struct stat sb{.st_mtime = -1}; // Skip all up-to-date checks if the file won't ever change. - if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) { - if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) { + if (!isReadonlyFilesystem(path.c_str())) { + if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -113,7 +116,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, } return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date)); + new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime)); } std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, @@ -134,10 +137,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, return {}; } - ModDate mod_date = kInvalidModDate; + struct stat sb{.st_mtime = -1}; // Skip all up-to-date checks if the file won't ever change. if (!isReadonlyFilesystem(released_fd)) { - if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) { + if (fstat(released_fd, &sb) < 0) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -147,7 +150,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, } return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider( - handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date)); + handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime)); } std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, @@ -279,16 +282,21 @@ const std::string& ZipAssetsProvider::GetDebugName() const { return name_.GetDebugName(); } -UpToDate ZipAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == kInvalidModDate) { - return UpToDate::Always; +bool ZipAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == -1) { + return true; + } + struct stat sb{}; + if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { + // If fstat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; } - return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get()))); + return last_mod_time_ == sb.st_mtime; } -DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time) - : dir_(std::move(path)), last_mod_time_(last_mod_time) { -} +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) + : dir_(std::move(path)), last_mod_time_(last_mod_time) {} std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { struct stat sb; @@ -309,7 +317,7 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st const bool isReadonly = isReadonlyFilesystem(path.c_str()); return std::unique_ptr<DirectoryAssetsProvider>( - new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb))); + new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime)); } std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, @@ -338,11 +346,17 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const { return dir_; } -UpToDate DirectoryAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == kInvalidModDate) { - return UpToDate::Always; +bool DirectoryAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == -1) { + return true; + } + struct stat sb; + if (stat(dir_.c_str(), &sb) < 0) { + // If stat fails on the zip archive, return true so the zip archive the resource system does + // attempt to refresh the ApkAsset. + return true; } - return fromBool(last_mod_time_ == getFileModDate(dir_.c_str())); + return last_mod_time_ == sb.st_mtime; } MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, @@ -355,14 +369,8 @@ MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& prima std::unique_ptr<AssetsProvider> MultiAssetsProvider::Create( std::unique_ptr<AssetsProvider>&& primary, std::unique_ptr<AssetsProvider>&& secondary) { - if (primary == nullptr && secondary == nullptr) { - return EmptyAssetsProvider::Create(); - } - if (!primary) { - return secondary; - } - if (!secondary) { - return primary; + if (primary == nullptr || secondary == nullptr) { + return nullptr; } return std::unique_ptr<MultiAssetsProvider>(new MultiAssetsProvider(std::move(primary), std::move(secondary))); @@ -389,8 +397,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const { return debug_name_; } -UpToDate MultiAssetsProvider::IsUpToDate() const { - return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); }); +bool MultiAssetsProvider::IsUpToDate() const { + return primary_->IsUpToDate() && secondary_->IsUpToDate(); } EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) : @@ -430,12 +438,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const { if (path_.has_value()) { return *path_; } - constexpr static std::string kEmpty{kEmptyDebugString}; + const static std::string kEmpty = kEmptyDebugString; return kEmpty; } -UpToDate EmptyAssetsProvider::IsUpToDate() const { - return UpToDate::Always; +bool EmptyAssetsProvider::IsUpToDate() const { + return true; } } // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index 262e7df185b7..3ecd82b074a1 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -22,10 +22,9 @@ #include "android-base/logging.h" #include "android-base/stringprintf.h" #include "android-base/utf8.h" -#include "androidfw/AssetManager.h" +#include "androidfw/misc.h" #include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" -#include "androidfw/misc.h" #include "utils/ByteOrder.h" #include "utils/Trace.h" @@ -269,16 +268,11 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head configurations_(configs), overlay_entries_(overlay_entries), string_pool_(std::move(string_pool)), + idmap_fd_( + android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)), overlay_apk_path_(overlay_apk_path), target_apk_path_(target_apk_path), - idmap_last_mod_time_(kInvalidModDate) { - if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) || - !(target_apk_path_ == AssetManager::TARGET_APK_PATH || - isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) { - idmap_fd_.reset( - android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)); - idmap_last_mod_time_ = getFileModDate(idmap_fd_); - } + idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) { } std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) { @@ -387,11 +381,8 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path)); } -UpToDate LoadedIdmap::IsUpToDate() const { - if (idmap_last_mod_time_ == kInvalidModDate) { - return UpToDate::Always; - } - return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get())); +bool LoadedIdmap::IsUpToDate() const { + return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()); } } // namespace android diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index a8eb062a2ece..de9991a8be5e 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -152,11 +152,12 @@ static void fill9patchOffsets(Res_png_9patch* patch) { patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t)); } -void Res_value::copyFrom_dtoh_slow(const Res_value& src) { - size = dtohs(src.size); - res0 = src.res0; - dataType = src.dataType; - data = dtohl(src.data); +void Res_value::copyFrom_dtoh(const Res_value& src) +{ + size = dtohs(src.size); + res0 = src.res0; + dataType = src.dataType; + data = dtohl(src.data); } void Res_png_9patch::deviceToFile() @@ -2030,6 +2031,16 @@ status_t ResXMLTree::validateNode(const ResXMLTree_node* node) const // -------------------------------------------------------------------- // -------------------------------------------------------------------- +void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) { + const size_t size = dtohl(o.size); + if (size >= sizeof(ResTable_config)) { + *this = o; + } else { + memcpy(this, &o, size); + memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size); + } +} + /* static */ size_t unpackLanguageOrRegion(const char in[2], const char base, char out[4]) { if (in[0] & 0x80) { @@ -2094,33 +2105,34 @@ size_t ResTable_config::unpackRegion(char region[4]) const { return unpackLanguageOrRegion(this->country, '0', region); } -void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) { - copyFromDeviceNoSwap(o); - size = sizeof(ResTable_config); - mcc = dtohs(mcc); - mnc = dtohs(mnc); - density = dtohs(density); - screenWidth = dtohs(screenWidth); - screenHeight = dtohs(screenHeight); - sdkVersion = dtohs(sdkVersion); - minorVersion = dtohs(minorVersion); - smallestScreenWidthDp = dtohs(smallestScreenWidthDp); - screenWidthDp = dtohs(screenWidthDp); - screenHeightDp = dtohs(screenHeightDp); -} - -void ResTable_config::swapHtoD_slow() { - size = htodl(size); - mcc = htods(mcc); - mnc = htods(mnc); - density = htods(density); - screenWidth = htods(screenWidth); - screenHeight = htods(screenHeight); - sdkVersion = htods(sdkVersion); - minorVersion = htods(minorVersion); - smallestScreenWidthDp = htods(smallestScreenWidthDp); - screenWidthDp = htods(screenWidthDp); - screenHeightDp = htods(screenHeightDp); + +void ResTable_config::copyFromDtoH(const ResTable_config& o) { + copyFromDeviceNoSwap(o); + size = sizeof(ResTable_config); + mcc = dtohs(mcc); + mnc = dtohs(mnc); + density = dtohs(density); + screenWidth = dtohs(screenWidth); + screenHeight = dtohs(screenHeight); + sdkVersion = dtohs(sdkVersion); + minorVersion = dtohs(minorVersion); + smallestScreenWidthDp = dtohs(smallestScreenWidthDp); + screenWidthDp = dtohs(screenWidthDp); + screenHeightDp = dtohs(screenHeightDp); +} + +void ResTable_config::swapHtoD() { + size = htodl(size); + mcc = htods(mcc); + mnc = htods(mnc); + density = htods(density); + screenWidth = htods(screenWidth); + screenHeight = htods(screenHeight); + sdkVersion = htods(sdkVersion); + minorVersion = htods(minorVersion); + smallestScreenWidthDp = htods(smallestScreenWidthDp); + screenWidthDp = htods(screenWidthDp); + screenHeightDp = htods(screenHeightDp); } /* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) { @@ -2133,7 +2145,7 @@ void ResTable_config::swapHtoD_slow() { // systems should happen very infrequently (if at all.) // The comparison code relies on memcmp low-level optimizations that make it // more efficient than strncmp. - static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; + const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript; const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript; diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index 86c459fb4647..be55fe8b4bb6 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -32,18 +32,13 @@ namespace android { namespace util { void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) { - static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; - if constexpr (kDeviceEndiannessSame) { - *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)}); - } else { - char buf[5]; - while (*src && len != 0) { - char16_t c = static_cast<char16_t>(dtohs(*src)); - utf16_to_utf8(&c, 1, buf, sizeof(buf)); - out->append(buf, strlen(buf)); - ++src; - --len; - } + char buf[5]; + while (*src && len != 0) { + char16_t c = static_cast<char16_t>(dtohs(*src)); + utf16_to_utf8(&c, 1, buf, sizeof(buf)); + out->append(buf, strlen(buf)); + ++src; + --len; } } @@ -68,10 +63,8 @@ std::string Utf16ToUtf8(StringPiece16 utf16) { } std::string utf8; - utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) { - utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1); - return size; - }); + utf8.resize(utf8_length); + utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1); return utf8; } diff --git a/libs/androidfw/ZipUtils.cpp b/libs/androidfw/ZipUtils.cpp index a1385f2cf7b1..f7f62c51a25b 100644 --- a/libs/androidfw/ZipUtils.cpp +++ b/libs/androidfw/ZipUtils.cpp @@ -87,19 +87,29 @@ class BufferReader final : public zip_archive::Reader { } bool ReadAtOffset(uint8_t* buf, size_t len, off64_t offset) const override { - if (mInputSize < len || offset > mInputSize - len) { - return false; - } - - const incfs::map_ptr<uint8_t> pos = mInput.offset(offset); - if (!pos.verify(len)) { + auto in = AccessAtOffset(buf, len, offset); + if (!in) { return false; } - - memcpy(buf, pos.unsafe_ptr(), len); + memcpy(buf, in, len); return true; } + const uint8_t* AccessAtOffset(uint8_t*, size_t len, off64_t offset) const override { + if (offset > mInputSize - len) { + return nullptr; + } + const incfs::map_ptr<uint8_t> pos = mInput.offset(offset); + if (!pos.verify(len)) { + return nullptr; + } + return pos.unsafe_ptr(); + } + + bool IsZeroCopy() const override { + return true; + } + private: const incfs::map_ptr<uint8_t> mInput; const size_t mInputSize; @@ -107,7 +117,7 @@ class BufferReader final : public zip_archive::Reader { class BufferWriter final : public zip_archive::Writer { public: - BufferWriter(void* output, size_t outputSize) : Writer(), + BufferWriter(void* output, size_t outputSize) : mOutput(reinterpret_cast<uint8_t*>(output)), mOutputSize(outputSize), mBytesWritten(0) { } @@ -121,6 +131,12 @@ class BufferWriter final : public zip_archive::Writer { return true; } + Buffer GetBuffer(size_t length) override { + const auto remaining_size = mOutputSize - mBytesWritten; + return remaining_size >= length + ? Buffer(mOutput + mBytesWritten, remaining_size) : Buffer(); + } + private: uint8_t* const mOutput; const size_t mOutputSize; diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index 3f6f4661f2f7..231808beb718 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -116,7 +116,7 @@ class ApkAssets : public RefBase { return resources_asset_ != nullptr && resources_asset_->isAllocated(); } - UpToDate IsUpToDate() const; + bool IsUpToDate() const; // DANGER! // This is a destructive method that rips the assets provider out of ApkAssets object. diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h index e3b3ae41f7f4..d33c325ff369 100644 --- a/libs/androidfw/include/androidfw/AssetsProvider.h +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -14,7 +14,8 @@ * limitations under the License. */ -#pragma once +#ifndef ANDROIDFW_ASSETSPROVIDER_H +#define ANDROIDFW_ASSETSPROVIDER_H #include <memory> #include <string> @@ -57,7 +58,7 @@ struct AssetsProvider { WARN_UNUSED virtual const std::string& GetDebugName() const = 0; // Returns whether the interface provides the most recent version of its files. - WARN_UNUSED virtual UpToDate IsUpToDate() const = 0; + WARN_UNUSED virtual bool IsUpToDate() const = 0; // Creates an Asset from a file on disk. static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); @@ -94,7 +95,7 @@ struct ZipAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const; ~ZipAssetsProvider() override = default; @@ -105,7 +106,7 @@ struct ZipAssetsProvider : public AssetsProvider { private: struct PathOrDebugName; ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags, - ModDate last_mod_time); + time_t last_mod_time); struct PathOrDebugName { static PathOrDebugName Path(std::string value) { @@ -134,7 +135,7 @@ struct ZipAssetsProvider : public AssetsProvider { std::unique_ptr<ZipArchive, ZipCloser> zip_handle_; PathOrDebugName name_; package_property_t flags_; - ModDate last_mod_time_; + time_t last_mod_time_; }; // Supplies assets from a root directory. @@ -146,7 +147,7 @@ struct DirectoryAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; ~DirectoryAssetsProvider() override = default; protected: @@ -155,23 +156,23 @@ struct DirectoryAssetsProvider : public AssetsProvider { bool* file_exists) const override; private: - explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time); + explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); std::string dir_; - ModDate last_mod_time_; + time_t last_mod_time_; }; // Supplies assets from a `primary` asset provider and falls back to supplying assets from the // `secondary` asset provider if the asset cannot be found in the `primary`. struct MultiAssetsProvider : public AssetsProvider { static std::unique_ptr<AssetsProvider> Create(std::unique_ptr<AssetsProvider>&& primary, - std::unique_ptr<AssetsProvider>&& secondary = {}); + std::unique_ptr<AssetsProvider>&& secondary); bool ForEachFile(const std::string& root_path, base::function_ref<void(StringPiece, FileType)> f) const override; WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; ~MultiAssetsProvider() override = default; protected: @@ -198,7 +199,7 @@ struct EmptyAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED UpToDate IsUpToDate() const override; + WARN_UNUSED bool IsUpToDate() const override; ~EmptyAssetsProvider() override = default; protected: @@ -211,3 +212,5 @@ struct EmptyAssetsProvider : public AssetsProvider { }; } // namespace android + +#endif /* ANDROIDFW_ASSETSPROVIDER_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 87f3c9df9a91..ac75eb3bb98c 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -14,7 +14,8 @@ * limitations under the License. */ -#pragma once +#ifndef IDMAP_H_ +#define IDMAP_H_ #include <memory> #include <string> @@ -31,31 +32,6 @@ namespace android { -// An enum that tracks more states than just 'up to date' or 'not' for a resources container: -// there are several cases where we know for sure that the object can't change and won't get -// out of date. Reporting those states to the managed layer allows it to stop checking here -// completely, speeding up the cache lookups by dozens of milliseconds. -enum class UpToDate : int { False, True, Always }; - -// Combines two UpToDate values, and only accesses the second one if it matters to the result. -template <class Getter> -UpToDate combine(UpToDate first, Getter secondGetter) { - switch (first) { - case UpToDate::False: - return UpToDate::False; - case UpToDate::True: { - const auto second = secondGetter(); - return second == UpToDate::False ? UpToDate::False : UpToDate::True; - } - case UpToDate::Always: - return secondGetter(); - } -} - -inline UpToDate fromBool(bool value) { - return value ? UpToDate::True : UpToDate::False; -} - class LoadedIdmap; class IdmapResMap; struct Idmap_header; @@ -220,7 +196,7 @@ class LoadedIdmap { // Returns whether the idmap file on disk has not been modified since the construction of this // LoadedIdmap. - UpToDate IsUpToDate() const; + bool IsUpToDate() const; protected: // Exposed as protected so that tests can subclass and mock this class out. @@ -255,3 +231,5 @@ class LoadedIdmap { }; } // namespace android + +#endif // IDMAP_H_ diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 819fe4b38c87..e330410ed1a0 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -47,8 +47,6 @@ namespace android { -constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; - constexpr const uint32_t kIdmapMagic = 0x504D4449u; constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au; @@ -410,16 +408,7 @@ struct Res_value typedef uint32_t data_type; data_type data; - void copyFrom_dtoh(const Res_value& src) { - if constexpr (kDeviceEndiannessSame) { - *this = src; - } else { - copyFrom_dtoh_slow(src); - } - } - - private: - void copyFrom_dtoh_slow(const Res_value& src); + void copyFrom_dtoh(const Res_value& src); }; /** @@ -1265,32 +1254,11 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; - void copyFromDeviceNoSwap(const ResTable_config& o) { - const auto o_size = dtohl(o.size); - if (o_size >= sizeof(ResTable_config)) [[likely]] { - *this = o; - } else { - memcpy(this, &o, o_size); - memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size); - } - this->size = sizeof(*this); - } - - void copyFromDtoH(const ResTable_config& o) { - if constexpr (kDeviceEndiannessSame) { - copyFromDeviceNoSwap(o); - } else { - copyFromDtoH_slow(o); - } - } - - void swapHtoD() { - if constexpr (kDeviceEndiannessSame) { - ; // noop - } else { - swapHtoD_slow(); - } - } + void copyFromDeviceNoSwap(const ResTable_config& o); + + void copyFromDtoH(const ResTable_config& o); + + void swapHtoD(); int compare(const ResTable_config& o) const; int compareLogical(const ResTable_config& o) const; @@ -1416,10 +1384,6 @@ struct ResTable_config bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; String8 toString() const; - - private: - void copyFromDtoH_slow(const ResTable_config& o); - void swapHtoD_slow(); }; /** diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index d8ca64a174a2..c9ba8a01a5e9 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -15,7 +15,6 @@ */ #pragma once -#include <sys/stat.h> #include <time.h> // @@ -65,15 +64,10 @@ ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ ModDate getFileModDate(int fd); -// Extract the modification date from the stat structure. -ModDate getModDate(const struct ::stat& st); - // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); -bool isKnownWritablePath(const char* path); - } // namespace android // Whoever uses getFileModDate() will need this as well diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 26eb320805c9..32f3624a3aee 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -16,10 +16,10 @@ #define LOG_TAG "misc" -#include "androidfw/misc.h" - -#include <errno.h> -#include <sys/stat.h> +// +// Miscellaneous utility functions. +// +#include <androidfw/misc.h> #include "android-base/logging.h" @@ -28,7 +28,9 @@ #include <sys/vfs.h> #endif // __linux__ -#include <array> +#include <errno.h> +#include <sys/stat.h> + #include <cstdio> #include <cstring> #include <tuple> @@ -38,26 +40,28 @@ namespace android { /* * Get a file's type. */ -FileType getFileType(const char* fileName) { - struct stat sb; - if (stat(fileName, &sb) < 0) { - if (errno == ENOENT || errno == ENOTDIR) - return kFileTypeNonexistent; - else { - PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; - return kFileTypeUnknown; - } - } else { - if (S_ISREG(sb.st_mode)) - return kFileTypeRegular; - else if (S_ISDIR(sb.st_mode)) - return kFileTypeDirectory; - else if (S_ISCHR(sb.st_mode)) - return kFileTypeCharDev; - else if (S_ISBLK(sb.st_mode)) - return kFileTypeBlockDev; - else if (S_ISFIFO(sb.st_mode)) - return kFileTypeFifo; +FileType getFileType(const char* fileName) +{ + struct stat sb; + + if (stat(fileName, &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) + return kFileTypeNonexistent; + else { + PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; + return kFileTypeUnknown; + } + } else { + if (S_ISREG(sb.st_mode)) + return kFileTypeRegular; + else if (S_ISDIR(sb.st_mode)) + return kFileTypeDirectory; + else if (S_ISCHR(sb.st_mode)) + return kFileTypeCharDev; + else if (S_ISBLK(sb.st_mode)) + return kFileTypeBlockDev; + else if (S_ISFIFO(sb.st_mode)) + return kFileTypeFifo; #if defined(S_ISLNK) else if (S_ISLNK(sb.st_mode)) return kFileTypeSymlink; @@ -71,7 +75,7 @@ FileType getFileType(const char* fileName) { } } -ModDate getModDate(const struct stat& st) { +static ModDate getModDate(const struct stat& st) { #ifdef _WIN32 return st.st_mtime; #elif defined(__APPLE__) @@ -109,14 +113,8 @@ bool isReadonlyFilesystem(const char*) { bool isReadonlyFilesystem(int) { return false; } -bool isKnownWritablePath(const char*) { - return false; -} #else // __linux__ bool isReadonlyFilesystem(const char* path) { - if (isKnownWritablePath(path)) { - return false; - } struct statfs sfs; if (::statfs(path, &sfs)) { PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed"; @@ -133,13 +131,6 @@ bool isReadonlyFilesystem(int fd) { } return (sfs.f_flags & ST_RDONLY) != 0; } - -bool isKnownWritablePath(const char* path) { - // We know that all paths in /data/ are writable. - static constexpr char kRwPrefix[] = "/data/"; - return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0; -} - #endif // __linux__ } // namespace android diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 22b9e69500d9..cb2e56f5f5e4 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -218,11 +218,10 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { auto apk_assets = ApkAssets::LoadOverlay(temp_file.path); ASSERT_NE(nullptr, apk_assets); - ASSERT_TRUE(apk_assets->IsOverlay()); - ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate()); + ASSERT_TRUE(apk_assets->IsUpToDate()); unlink(temp_file.path); - ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); + ASSERT_FALSE(apk_assets->IsUpToDate()); const auto sleep_duration = std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); @@ -231,27 +230,7 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { base::WriteStringToFile("hello", temp_file.path); std::this_thread::sleep_for(sleep_duration); - ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); -} - -TEST(IdmapTestUpToDate, Combine) { - ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] { - ADD_FAILURE(); // Shouldn't get called at all. - return UpToDate::False; - })); - - ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; })); - - ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; })); - ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; })); - ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; })); - - ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; })); -} - -TEST(IdmapTestUpToDate, FromBool) { - ASSERT_EQ(UpToDate::False, fromBool(false)); - ASSERT_EQ(UpToDate::True, fromBool(true)); + ASSERT_FALSE(apk_assets->IsUpToDate()); } } // namespace diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index f8fb6b7207a0..139ccfd22b0e 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -36,8 +36,7 @@ package com.android.extensions.appfunctions { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.content.pm.SigningInfo, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); - method @Deprecated @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); + method @MainThread public abstract void onExecuteFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java index a09451ede4fc..81d9d81c4f58 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionService.java @@ -25,7 +25,6 @@ import android.annotation.Nullable; import android.annotation.SdkConstant; import android.app.Service; import android.content.Intent; -import android.content.pm.SigningInfo; import android.os.Binder; import android.os.CancellationSignal; import android.os.IBinder; @@ -82,7 +81,6 @@ public abstract class AppFunctionService extends Service { SidecarConverter.getSidecarExecuteAppFunctionRequest( platformRequest), callingPackage, - callingPackageSigningInfo, cancellationSignal, new OutcomeReceiver<>() { @Override @@ -129,52 +127,10 @@ public abstract class AppFunctionService extends Service { * * @param request The function execution request. * @param callingPackage The package name of the app that is requesting the execution. - * @param callingPackageSigningInfo The signing information of the app that is requesting the - * execution. * @param cancellationSignal A signal to cancel the execution. * @param callback A callback to report back the result or error. */ @MainThread - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull String callingPackage, - @NonNull SigningInfo callingPackageSigningInfo, - @NonNull CancellationSignal cancellationSignal, - @NonNull OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> callback) { - onExecuteFunction(request, callingPackage, cancellationSignal, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is the entry point for handling all app function requests in an app. When the - * system needs your AppFunctionService to perform a function, it will invoke this method. - * - * <p>Each function you've registered is identified by a unique identifier. This identifier - * doesn't need to be globally unique, but it must be unique within your app. For example, a - * function to order food could be identified as "orderFood". In most cases, this identifier is - * automatically generated by the AppFunctions SDK. - * - * <p>You can determine which function to execute by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. This allows your service to route the - * incoming request to the appropriate logic for handling the specific function. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel - * the execution of function if requested by the system. - * - * @param request The function execution request. - * @param callingPackage The package name of the app that is requesting the execution. - * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result or error. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, SigningInfo, - * CancellationSignal, OutcomeReceiver)} instead. - */ - @MainThread - @Deprecated public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 76ad2acccf89..5e71d3360f39 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -34,13 +34,6 @@ flag { } flag { - name: "high_contrast_text_luminance" - namespace: "accessibility" - description: "Use luminance to determine how to make text more high contrast, instead of RGB heuristic" - bug: "186567103" -} - -flag { name: "high_contrast_text_small_text_rect" namespace: "accessibility" description: "Draw a solid rectangle background behind text instead of a stroke outline" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index e13e136550ca..e05c3d695463 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -34,9 +34,6 @@ namespace flags = com::android::graphics::hwui::flags; #else namespace flags { -constexpr bool high_contrast_text_luminance() { - return false; -} constexpr bool high_contrast_text_small_text_rect() { return false; } @@ -114,15 +111,10 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - bool darken; - // This equation should match the one in core/java/android/text/Layout.java - if (flags::high_contrast_text_luminance()) { - uirenderer::Lab lab = uirenderer::sRGBToLab(color); - darken = lab.L <= 50; - } else { - int channelSum = SkColorGetR(color) + SkColorGetG(color) + SkColorGetB(color); - darken = channelSum < (128 * 3); - } + // LINT.IfChange(hct_darken) + uirenderer::Lab lab = uirenderer::sRGBToLab(color); + bool darken = lab.L <= 50; + // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 7b45070af312..290df997a8ed 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -36,7 +36,7 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, const Typeface* resolvedFace = Typeface::resolveDefault(typeface); const SkFont& font = paint->getSkFont(); - minikin::MinikinPaint minikinPaint(resolvedFace->fFontCollection); + minikin::MinikinPaint minikinPaint(resolvedFace->getFontCollection()); /* Prepare minikin Paint */ minikinPaint.size = font.isLinearMetrics() ? font.getSize() : static_cast<int>(font.getSize()); @@ -46,9 +46,9 @@ minikin::MinikinPaint MinikinUtils::prepareMinikinPaint(const Paint* paint, minikinPaint.wordSpacing = paint->getWordSpacing(); minikinPaint.fontFlags = MinikinFontSkia::packFontFlags(font); minikinPaint.localeListId = paint->getMinikinLocaleListId(); - minikinPaint.fontStyle = resolvedFace->fStyle; + minikinPaint.fontStyle = resolvedFace->getFontStyle(); minikinPaint.fontFeatureSettings = paint->getFontFeatureSettings(); - if (!resolvedFace->fIsVariationInstance) { + if (!resolvedFace->isVariationInstance()) { // This is an optimization for direct private API use typically done by System UI. // In the public API surface, if Typeface is already configured for variation instance // (Target SDK <= 35) the font variation settings of Paint is not set. @@ -132,7 +132,7 @@ minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin:: bool MinikinUtils::hasVariationSelector(const Typeface* typeface, uint32_t codepoint, uint32_t vs) { const Typeface* resolvedFace = Typeface::resolveDefault(typeface); - return resolvedFace->fFontCollection->hasVariationSelector(codepoint, vs); + return resolvedFace->getFontCollection()->hasVariationSelector(codepoint, vs); } float MinikinUtils::xOffsetForTextAlign(Paint* paint, const minikin::Layout& layout) { diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index 4dfe05377a48..a73aac632752 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -70,74 +70,45 @@ const Typeface* Typeface::resolveDefault(const Typeface* src) { Typeface* Typeface::createRelative(Typeface* src, Typeface::Style style) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface; - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = style; - result->fStyle = computeRelativeStyle(result->fBaseWeight, style); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), + computeRelativeStyle(resolvedFace->getBaseWeight(), style), style, + resolvedFace->getBaseWeight(), resolvedFace->isVariationInstance()); } Typeface* Typeface::createAbsolute(Typeface* base, int weight, bool italic) { const Typeface* resolvedFace = Typeface::resolveDefault(base); - Typeface* result = new Typeface(); - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = computeAPIStyle(weight, italic); - result->fStyle = computeMinikinStyle(weight, italic); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), computeMinikinStyle(weight, italic), + computeAPIStyle(weight, italic), resolvedFace->getBaseWeight(), + resolvedFace->isVariationInstance()); } Typeface* Typeface::createFromTypefaceWithVariation(Typeface* src, const minikin::VariationSettings& variations) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface(); - if (result != nullptr) { - result->fFontCollection = - resolvedFace->fFontCollection->createCollectionWithVariation(variations); - if (result->fFontCollection == nullptr) { + const std::shared_ptr<minikin::FontCollection>& fc = + resolvedFace->getFontCollection()->createCollectionWithVariation(variations); + return new Typeface( // None of passed axes are supported by this collection. // So we will reuse the same collection with incrementing reference count. - result->fFontCollection = resolvedFace->fFontCollection; - } - // Do not update styles. - // TODO: We may want to update base weight if the 'wght' is specified. - result->fBaseWeight = resolvedFace->fBaseWeight; - result->fAPIStyle = resolvedFace->fAPIStyle; - result->fStyle = resolvedFace->fStyle; - result->fIsVariationInstance = true; - } - return result; + fc ? fc : resolvedFace->getFontCollection(), + // Do not update styles. + // TODO: We may want to update base weight if the 'wght' is specified. + resolvedFace->fStyle, resolvedFace->getAPIStyle(), resolvedFace->getBaseWeight(), true); } Typeface* Typeface::createWithDifferentBaseWeight(Typeface* src, int weight) { const Typeface* resolvedFace = Typeface::resolveDefault(src); - Typeface* result = new Typeface; - if (result != nullptr) { - result->fFontCollection = resolvedFace->fFontCollection; - result->fBaseWeight = weight; - result->fAPIStyle = resolvedFace->fAPIStyle; - result->fStyle = computeRelativeStyle(weight, result->fAPIStyle); - result->fIsVariationInstance = resolvedFace->fIsVariationInstance; - } - return result; + return new Typeface(resolvedFace->getFontCollection(), + computeRelativeStyle(weight, resolvedFace->getAPIStyle()), + resolvedFace->getAPIStyle(), weight, resolvedFace->isVariationInstance()); } Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic, const Typeface* fallback) { - Typeface* result = new Typeface; - if (fallback == nullptr) { - result->fFontCollection = minikin::FontCollection::create(std::move(families)); - } else { - result->fFontCollection = - fallback->fFontCollection->createCollectionWithFamilies(std::move(families)); - } + const std::shared_ptr<minikin::FontCollection>& fc = + fallback ? fallback->getFontCollection()->createCollectionWithFamilies( + std::move(families)) + : minikin::FontCollection::create(std::move(families)); if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) { int weightFromFont; @@ -171,11 +142,8 @@ Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::Font weight = SkFontStyle::kNormal_Weight; } - result->fBaseWeight = weight; - result->fAPIStyle = computeAPIStyle(weight, italic); - result->fStyle = computeMinikinStyle(weight, italic); - result->fIsVariationInstance = false; - return result; + return new Typeface(fc, computeMinikinStyle(weight, italic), computeAPIStyle(weight, italic), + weight, false); } void Typeface::setDefault(const Typeface* face) { @@ -205,11 +173,8 @@ void Typeface::setRobotoTypefaceForTest() { std::shared_ptr<minikin::FontCollection> collection = minikin::FontCollection::create(minikin::FontFamily::create(std::move(fonts))); - Typeface* hwTypeface = new Typeface(); - hwTypeface->fFontCollection = collection; - hwTypeface->fAPIStyle = Typeface::kNormal; - hwTypeface->fBaseWeight = SkFontStyle::kNormal_Weight; - hwTypeface->fStyle = minikin::FontStyle(); + Typeface* hwTypeface = new Typeface(collection, minikin::FontStyle(), Typeface::kNormal, + SkFontStyle::kNormal_Weight, false); Typeface::setDefault(hwTypeface); #endif diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h index 97d1bf4ef011..e8233a6bc6d8 100644 --- a/libs/hwui/hwui/Typeface.h +++ b/libs/hwui/hwui/Typeface.h @@ -32,21 +32,39 @@ constexpr int RESOLVE_BY_FONT_TABLE = -1; struct ANDROID_API Typeface { public: - std::shared_ptr<minikin::FontCollection> fFontCollection; + enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; + Typeface(const std::shared_ptr<minikin::FontCollection> fc, minikin::FontStyle style, + Style apiStyle, int baseWeight, bool isVariationInstance) + : fFontCollection(fc) + , fStyle(style) + , fAPIStyle(apiStyle) + , fBaseWeight(baseWeight) + , fIsVariationInstance(isVariationInstance) {} + + const std::shared_ptr<minikin::FontCollection>& getFontCollection() const { + return fFontCollection; + } // resolved style actually used for rendering - minikin::FontStyle fStyle; + minikin::FontStyle getFontStyle() const { return fStyle; } // style used in the API - enum Style : uint8_t { kNormal = 0, kBold = 0x01, kItalic = 0x02, kBoldItalic = 0x03 }; - Style fAPIStyle; + Style getAPIStyle() const { return fAPIStyle; } // base weight in CSS-style units, 1..1000 - int fBaseWeight; + int getBaseWeight() const { return fBaseWeight; } // True if the Typeface is already created for variation settings. - bool fIsVariationInstance; + bool isVariationInstance() const { return fIsVariationInstance; } +private: + std::shared_ptr<minikin::FontCollection> fFontCollection; + minikin::FontStyle fStyle; + Style fAPIStyle; + int fBaseWeight; + bool fIsVariationInstance = false; + +public: static const Typeface* resolveDefault(const Typeface* src); // The following three functions create new Typeface from an existing Typeface with a different diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index 8d3a5eb2b4af..f6fdec1c82bc 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -609,7 +609,8 @@ namespace PaintGlue { SkFont* font = &paint->getSkFont(); const Typeface* typeface = paint->getAndroidTypeface(); typeface = Typeface::resolveDefault(typeface); - minikin::FakedFont baseFont = typeface->fFontCollection->baseFontFaked(typeface->fStyle); + minikin::FakedFont baseFont = + typeface->getFontCollection()->baseFontFaked(typeface->getFontStyle()); float saveSkewX = font->getSkewX(); bool savefakeBold = font->isEmbolden(); MinikinFontSkia::populateSkFont(font, baseFont.typeface().get(), baseFont.fakery); @@ -641,7 +642,7 @@ namespace PaintGlue { if (useLocale) { minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface); minikin::MinikinExtent extent = - typeface->fFontCollection->getReferenceExtentForLocale(minikinPaint); + typeface->getFontCollection()->getReferenceExtentForLocale(minikinPaint); metrics->fAscent = std::min(extent.ascent, metrics->fAscent); metrics->fDescent = std::max(extent.descent, metrics->fDescent); metrics->fTop = std::min(metrics->fAscent, metrics->fTop); diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index c5095c1a0704..63906de80745 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -99,17 +99,17 @@ static jlong Typeface_getReleaseFunc(CRITICAL_JNI_PARAMS) { // CriticalNative static jint Typeface_getStyle(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fAPIStyle; + return toTypeface(faceHandle)->getAPIStyle(); } // CriticalNative static jint Typeface_getWeight(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fStyle.weight(); + return toTypeface(faceHandle)->getFontStyle().weight(); } // Critical Native static jboolean Typeface_isVariationInstance(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - return toTypeface(faceHandle)->fIsVariationInstance; + return toTypeface(faceHandle)->isVariationInstance(); } static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray, @@ -128,18 +128,18 @@ static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArr // CriticalNative static void Typeface_setDefault(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { Typeface::setDefault(toTypeface(faceHandle)); - minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->fFontCollection); + minikin::SystemFonts::registerDefault(toTypeface(faceHandle)->getFontCollection()); } static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) { Typeface* face = toTypeface(faceHandle); - const size_t length = face->fFontCollection->getSupportedAxesCount(); + const size_t length = face->getFontCollection()->getSupportedAxesCount(); if (length == 0) { return nullptr; } std::vector<jint> tagVec(length); for (size_t i = 0; i < length; i++) { - tagVec[i] = face->fFontCollection->getSupportedAxisAt(i); + tagVec[i] = face->getFontCollection()->getSupportedAxisAt(i); } std::sort(tagVec.begin(), tagVec.end()); const jintArray result = env->NewIntArray(length); @@ -150,7 +150,7 @@ static jobject Typeface_getSupportedAxes(JNIEnv *env, jobject, jlong faceHandle) static void Typeface_registerGenericFamily(JNIEnv *env, jobject, jstring familyName, jlong ptr) { ScopedUtfChars familyNameChars(env, familyName); minikin::SystemFonts::registerFallback(familyNameChars.c_str(), - toTypeface(ptr)->fFontCollection); + toTypeface(ptr)->getFontCollection()); } #ifdef __ANDROID__ @@ -315,18 +315,19 @@ static jint Typeface_writeTypefaces(JNIEnv* env, jobject, jobject buffer, jint p std::vector<std::shared_ptr<minikin::FontCollection>> fontCollections; std::unordered_map<std::shared_ptr<minikin::FontCollection>, size_t> fcToIndex; for (Typeface* typeface : typefaces) { - bool inserted = fcToIndex.emplace(typeface->fFontCollection, fontCollections.size()).second; + bool inserted = + fcToIndex.emplace(typeface->getFontCollection(), fontCollections.size()).second; if (inserted) { - fontCollections.push_back(typeface->fFontCollection); + fontCollections.push_back(typeface->getFontCollection()); } } minikin::FontCollection::writeVector(&writer, fontCollections); writer.write<uint32_t>(typefaces.size()); for (Typeface* typeface : typefaces) { - writer.write<uint32_t>(fcToIndex.find(typeface->fFontCollection)->second); - typeface->fStyle.writeTo(&writer); - writer.write<Typeface::Style>(typeface->fAPIStyle); - writer.write<int>(typeface->fBaseWeight); + writer.write<uint32_t>(fcToIndex.find(typeface->getFontCollection())->second); + typeface->getFontStyle().writeTo(&writer); + writer.write<Typeface::Style>(typeface->getAPIStyle()); + writer.write<int>(typeface->getBaseWeight()); } return static_cast<jint>(writer.size()); } @@ -349,12 +350,10 @@ static jlongArray Typeface_readTypefaces(JNIEnv* env, jobject, jobject buffer, j std::vector<jlong> faceHandles; faceHandles.reserve(typefaceCount); for (uint32_t i = 0; i < typefaceCount; i++) { - Typeface* typeface = new Typeface; - typeface->fFontCollection = fontCollections[reader.read<uint32_t>()]; - typeface->fStyle = minikin::FontStyle(&reader); - typeface->fAPIStyle = reader.read<Typeface::Style>(); - typeface->fBaseWeight = reader.read<int>(); - typeface->fIsVariationInstance = false; + Typeface* typeface = + new Typeface(fontCollections[reader.read<uint32_t>()], minikin::FontStyle(&reader), + reader.read<Typeface::Style>(), reader.read<int>(), + false /* isVariationInstance */); faceHandles.push_back(toJLong(typeface)); } const jlongArray result = env->NewLongArray(typefaceCount); @@ -382,7 +381,8 @@ static void Typeface_warmUpCache(JNIEnv* env, jobject, jstring jFilePath) { // Critical Native static void Typeface_addFontCollection(CRITICAL_JNI_PARAMS_COMMA jlong faceHandle) { - std::shared_ptr<minikin::FontCollection> collection = toTypeface(faceHandle)->fFontCollection; + std::shared_ptr<minikin::FontCollection> collection = + toTypeface(faceHandle)->getFontCollection(); minikin::SystemFonts::addFontMap(std::move(collection)); } diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index d1782b285b34..7a4ae8330de8 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -104,7 +104,7 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou } else { fontId = fonts.size(); // This is new to us. Create new one. std::shared_ptr<minikin::Font> font; - if (resolvedFace->fIsVariationInstance) { + if (resolvedFace->isVariationInstance()) { // The optimization for target SDK 35 or before because the variation instance // is already created and no runtime variation resolution happens on such // environment. diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index 859cc57dea9f..4c9656792dac 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -20,6 +20,7 @@ #include <private/performance_hint_private.h> #include <future> +#include <memory> #include <optional> #include <vector> diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index 6571d92aeafa..a67aea466c1c 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -729,7 +729,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { VkSemaphore semaphore; VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore); ALOGE_IF(VK_SUCCESS != err, - "VulkanManager::makeSwapSemaphore(): Failed to create semaphore"); + "VulkanManager::finishFrame(): Failed to create semaphore"); if (err == VK_SUCCESS) { sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore); @@ -777,7 +777,7 @@ VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) { int fenceFd = -1; VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd); - ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd"); + ALOGE_IF(VK_SUCCESS != err, "VulkanManager::finishFrame(): Failed to get semaphore Fd"); drawResult.presentFence.reset(fenceFd); } else { ALOGE("VulkanManager::finishFrame(): Semaphore submission failed"); diff --git a/libs/hwui/tests/common/TestUtils.cpp b/libs/hwui/tests/common/TestUtils.cpp index 93118aeafaaf..b51414fd3c02 100644 --- a/libs/hwui/tests/common/TestUtils.cpp +++ b/libs/hwui/tests/common/TestUtils.cpp @@ -183,8 +183,11 @@ SkRect TestUtils::getLocalClipBounds(const SkCanvas* canvas) { } SkFont TestUtils::defaultFont() { - const std::shared_ptr<minikin::MinikinFont>& minikinFont = - Typeface::resolveDefault(nullptr)->fFontCollection->getFamilyAt(0)->getFont(0)->baseTypeface(); + const std::shared_ptr<minikin::MinikinFont>& minikinFont = Typeface::resolveDefault(nullptr) + ->getFontCollection() + ->getFamilyAt(0) + ->getFont(0) + ->baseTypeface(); SkTypeface* skTypeface = reinterpret_cast<const MinikinFontSkia*>(minikinFont.get())->GetSkTypeface(); LOG_ALWAYS_FATAL_IF(skTypeface == nullptr); return SkFont(sk_ref_sp(skTypeface)); diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp index c71c4d243a8b..7bcd937397b0 100644 --- a/libs/hwui/tests/unit/TypefaceTests.cpp +++ b/libs/hwui/tests/unit/TypefaceTests.cpp @@ -90,40 +90,40 @@ TEST(TypefaceTest, resolveDefault_and_setDefaultTest) { TEST(TypefaceTest, createWithDifferentBaseWeight) { std::unique_ptr<Typeface> bold(Typeface::createWithDifferentBaseWeight(nullptr, 700)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, bold->getAPIStyle()); std::unique_ptr<Typeface> light(Typeface::createWithDifferentBaseWeight(nullptr, 300)); - EXPECT_EQ(300, light->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, light->fAPIStyle); + EXPECT_EQ(300, light->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, light->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, light->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromRegular) { // In Java, Typeface.create(Typeface.DEFAULT, Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(nullptr, Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(nullptr, Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(nullptr, Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic(Typeface::createRelative(nullptr, Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_BoldBase) { @@ -132,31 +132,31 @@ TEST(TypefaceTest, createRelativeTest_BoldBase) { // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(700, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(700, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(1000, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(1000, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(700, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-bold"), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(1000, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(1000, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_LightBase) { @@ -165,31 +165,31 @@ TEST(TypefaceTest, createRelativeTest_LightBase) { // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(300, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(300, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(600, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(600, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.ITLIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(300, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(300, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, Typeface.create(Typeface.create("sans-serif-light"), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(600, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(600, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromBoldStyled) { @@ -198,32 +198,32 @@ TEST(TypefaceTest, createRelativeTest_fromBoldStyled) { // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.BOLD), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromItalicStyled) { @@ -233,33 +233,33 @@ TEST(TypefaceTest, createRelativeTest_fromItalicStyled) { // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, Typeface.create(Typeface.create(Typeface.DEFAULT, // Typeface.ITALIC), Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface.create(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC), // Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { @@ -270,27 +270,27 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.NORMAL); std::unique_ptr<Typeface> normal(Typeface::createRelative(base.get(), Typeface::kNormal)); - EXPECT_EQ(400, normal->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, normal->fAPIStyle); + EXPECT_EQ(400, normal->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, normal->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, normal->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.BOLD); std::unique_ptr<Typeface> bold(Typeface::createRelative(base.get(), Typeface::kBold)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") // .setWeight(700).setItalic(false).build(); // Typeface.create(typeface, Typeface.ITALIC); std::unique_ptr<Typeface> italic(Typeface::createRelative(base.get(), Typeface::kItalic)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // Typeface typeface = new Typeface.Builder(invalid).setFallback("sans-serif") @@ -298,9 +298,9 @@ TEST(TypefaceTest, createRelativeTest_fromSpecifiedStyled) { // Typeface.create(typeface, Typeface.BOLD_ITALIC); std::unique_ptr<Typeface> boldItalic( Typeface::createRelative(base.get(), Typeface::kBoldItalic)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); } TEST(TypefaceTest, createAbsolute) { @@ -309,45 +309,45 @@ TEST(TypefaceTest, createAbsolute) { // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(false) // .build(); std::unique_ptr<Typeface> regular(Typeface::createAbsolute(nullptr, 400, false)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(false) // .build(); std::unique_ptr<Typeface> bold(Typeface::createAbsolute(nullptr, 700, false)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(400).setItalic(true) // .build(); std::unique_ptr<Typeface> italic(Typeface::createAbsolute(nullptr, 400, true)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(700).setItalic(true) // .build(); std::unique_ptr<Typeface> boldItalic(Typeface::createAbsolute(nullptr, 700, true)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kBoldItalic, boldItalic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBoldItalic, boldItalic->getAPIStyle()); // In Java, // new // Typeface.Builder(invalid).setFallback("sans-serif").setWeight(1100).setItalic(true) // .build(); std::unique_ptr<Typeface> over1000(Typeface::createAbsolute(nullptr, 1100, false)); - EXPECT_EQ(1000, over1000->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); + EXPECT_EQ(1000, over1000->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Single) { @@ -355,43 +355,43 @@ TEST(TypefaceTest, createFromFamilies_Single) { // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build(); std::unique_ptr<Typeface> regular(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build(); std::unique_ptr<Typeface> bold(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, new // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build(); std::unique_ptr<Typeface> italic(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build(); std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, // new // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build(); std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies( makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */)); - EXPECT_EQ(1000, over1000->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle); + EXPECT_EQ(1000, over1000->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, over1000->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { @@ -399,33 +399,33 @@ TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) { std::unique_ptr<Typeface> regular( Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); - EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); + EXPECT_EQ(Typeface::kNormal, regular->getAPIStyle()); // In Java, new Typeface.Builder("Family-Bold.ttf").build(); std::unique_ptr<Typeface> bold( Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, bold->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant()); - EXPECT_EQ(Typeface::kBold, bold->fAPIStyle); + EXPECT_EQ(700, bold->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->getFontStyle().slant()); + EXPECT_EQ(Typeface::kBold, bold->getAPIStyle()); // In Java, new Typeface.Builder("Family-Italic.ttf").build(); std::unique_ptr<Typeface> italic( Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, italic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(400, italic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build(); std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies( makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, boldItalic->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant()); - EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle); + EXPECT_EQ(700, boldItalic->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->getFontStyle().slant()); + EXPECT_EQ(Typeface::kItalic, italic->getAPIStyle()); } TEST(TypefaceTest, createFromFamilies_Family) { @@ -435,8 +435,8 @@ TEST(TypefaceTest, createFromFamilies_Family) { std::unique_ptr<Typeface> typeface( Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(400, typeface->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); + EXPECT_EQ(400, typeface->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant()); } TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { @@ -445,8 +445,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) { std::unique_ptr<Typeface> typeface( Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, nullptr /* fallback */)); - EXPECT_EQ(700, typeface->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant()); + EXPECT_EQ(700, typeface->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->getFontStyle().slant()); } TEST(TypefaceTest, createFromFamilies_Family_withFallback) { @@ -458,8 +458,8 @@ TEST(TypefaceTest, createFromFamilies_Family_withFallback) { std::unique_ptr<Typeface> regular( Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, fallback.get())); - EXPECT_EQ(400, regular->fStyle.weight()); - EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant()); + EXPECT_EQ(400, regular->getFontStyle().weight()); + EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->getFontStyle().slant()); } } // namespace diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING index e52e0b16eca3..6a21496f1165 100644 --- a/media/TEST_MAPPING +++ b/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "mediaroutertest" diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 54a87ad7fd39..2a740f85aa72 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -32,6 +32,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -83,6 +84,7 @@ interface IAudioService { // When a method's argument list is changed, BpAudioManager's corresponding serialization code // (if any) in frameworks/native/services/audiomanager/IAudioManager.cpp must be updated. + IAudioManagerNative getNativeInterface(); int trackPlayer(in PlayerBase.PlayerIdCard pic); oneway void playerAttributes(in int piid, in AudioAttributes attr); diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index bbb03e77c8c9..88981eac9bb5 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -961,8 +961,7 @@ public final class MediaRoute2Info implements Parcelable { * * @hide */ - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) - public boolean isVisibleTo(String packageName) { + public boolean isVisibleTo(@NonNull String packageName) { return !mIsVisibilityRestricted || TextUtils.equals(getProviderPackageName(), packageName) || mAllowedPackages.contains(packageName); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 3738312b762f..e57148fe5a6a 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -19,7 +19,6 @@ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES; -import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME; import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL; import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2; import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING; @@ -1406,7 +1405,6 @@ public final class MediaRouter2 { requestCreateController(controller, route, managerRequestId); } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getSortedRoutes( List<MediaRoute2Info> routes, List<String> packageOrder) { if (packageOrder.isEmpty()) { @@ -1427,7 +1425,6 @@ public final class MediaRouter2 { } @GuardedBy("mLock") - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked( List<MediaRoute2Info> routes) { @@ -3654,7 +3651,6 @@ public final class MediaRouter2 { } } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) @Override public List<MediaRoute2Info> filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) { diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 3854747f46e0..3f18eef2f9aa 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -20,11 +20,9 @@ import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING; import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; -import static com.android.media.flags.Flags.FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME; import android.Manifest; import android.annotation.CallbackExecutor; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -287,7 +285,6 @@ public final class MediaRouter2Manager { (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute()); } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) { if (!preference.shouldRemoveDuplicates()) { synchronized (mRoutesLock) { @@ -311,7 +308,6 @@ public final class MediaRouter2Manager { return routes; } - @FlaggedApi(FLAG_ENABLE_MEDIA_ROUTE_2_INFO_PROVIDER_PACKAGE_NAME) private List<MediaRoute2Info> getFilteredRoutes( @NonNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 4398b261377b..c48b5f4e4aea 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -11,6 +11,16 @@ flag { } flag { + name: "disable_set_bluetooth_ad2p_on_calls" + namespace: "media_better_together" + description: "Prevents calls to AudioService.setBluetoothA2dpOn(), known to cause incorrect audio routing to the built-in speakers." + bug: "294968421" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_audio_input_device_routing_and_volume_control" namespace: "media_better_together" description: "Allows audio input devices routing and volume control via system settings." diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index 191b938b2e26..aeb028ccd0a6 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -54,14 +54,17 @@ public final class MediaQualityManager { private final IMediaQualityManager mService; private final Context mContext; private final UserHandle mUserHandle; - private final Object mLock = new Object(); - // @GuardedBy("mLock") + private final Object mPpLock = new Object(); + private final Object mSpLock = new Object(); + private final Object mAbLock = new Object(); + private final Object mApLock = new Object(); + // @GuardedBy("mPpLock") private final List<PictureProfileCallbackRecord> mPpCallbackRecords = new ArrayList<>(); - // @GuardedBy("mLock") + // @GuardedBy("mSpLock") private final List<SoundProfileCallbackRecord> mSpCallbackRecords = new ArrayList<>(); - // @GuardedBy("mLock") + // @GuardedBy("mAbLock") private final List<AmbientBacklightCallbackRecord> mAbCallbackRecords = new ArrayList<>(); - // @GuardedBy("mLock") + // @GuardedBy("mApLock") private final List<ActiveProcessingPictureListenerRecord> mApListenerRecords = new ArrayList<>(); @@ -82,7 +85,7 @@ public final class MediaQualityManager { IPictureProfileCallback ppCallback = new IPictureProfileCallback.Stub() { @Override public void onPictureProfileAdded(String profileId, PictureProfile profile) { - synchronized (mLock) { + synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record record.postPictureProfileAdded(profileId, profile); @@ -91,7 +94,7 @@ public final class MediaQualityManager { } @Override public void onPictureProfileUpdated(String profileId, PictureProfile profile) { - synchronized (mLock) { + synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record record.postPictureProfileUpdated(profileId, profile); @@ -100,7 +103,7 @@ public final class MediaQualityManager { } @Override public void onPictureProfileRemoved(String profileId, PictureProfile profile) { - synchronized (mLock) { + synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record record.postPictureProfileRemoved(profileId, profile); @@ -110,7 +113,7 @@ public final class MediaQualityManager { @Override public void onParameterCapabilitiesChanged( String profileId, List<ParameterCapability> caps) { - synchronized (mLock) { + synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record record.postParameterCapabilitiesChanged(profileId, caps); @@ -119,7 +122,7 @@ public final class MediaQualityManager { } @Override public void onError(String profileId, int err) { - synchronized (mLock) { + synchronized (mPpLock) { for (PictureProfileCallbackRecord record : mPpCallbackRecords) { // TODO: filter callback record record.postError(profileId, err); @@ -130,7 +133,7 @@ public final class MediaQualityManager { ISoundProfileCallback spCallback = new ISoundProfileCallback.Stub() { @Override public void onSoundProfileAdded(String profileId, SoundProfile profile) { - synchronized (mLock) { + synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { // TODO: filter callback record record.postSoundProfileAdded(profileId, profile); @@ -139,7 +142,7 @@ public final class MediaQualityManager { } @Override public void onSoundProfileUpdated(String profileId, SoundProfile profile) { - synchronized (mLock) { + synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { // TODO: filter callback record record.postSoundProfileUpdated(profileId, profile); @@ -148,7 +151,7 @@ public final class MediaQualityManager { } @Override public void onSoundProfileRemoved(String profileId, SoundProfile profile) { - synchronized (mLock) { + synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { // TODO: filter callback record record.postSoundProfileRemoved(profileId, profile); @@ -158,7 +161,7 @@ public final class MediaQualityManager { @Override public void onParameterCapabilitiesChanged( String profileId, List<ParameterCapability> caps) { - synchronized (mLock) { + synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { // TODO: filter callback record record.postParameterCapabilitiesChanged(profileId, caps); @@ -167,7 +170,7 @@ public final class MediaQualityManager { } @Override public void onError(String profileId, int err) { - synchronized (mLock) { + synchronized (mSpLock) { for (SoundProfileCallbackRecord record : mSpCallbackRecords) { // TODO: filter callback record record.postError(profileId, err); @@ -178,7 +181,7 @@ public final class MediaQualityManager { IAmbientBacklightCallback abCallback = new IAmbientBacklightCallback.Stub() { @Override public void onAmbientBacklightEvent(AmbientBacklightEvent event) { - synchronized (mLock) { + synchronized (mAbLock) { for (AmbientBacklightCallbackRecord record : mAbCallbackRecords) { record.postAmbientBacklightEvent(event); } @@ -205,7 +208,7 @@ public final class MediaQualityManager { @NonNull PictureProfileCallback callback) { Preconditions.checkNotNull(callback); Preconditions.checkNotNull(executor); - synchronized (mLock) { + synchronized (mPpLock) { mPpCallbackRecords.add(new PictureProfileCallbackRecord(callback, executor)); } } @@ -215,7 +218,7 @@ public final class MediaQualityManager { */ public void unregisterPictureProfileCallback(@NonNull final PictureProfileCallback callback) { Preconditions.checkNotNull(callback); - synchronized (mLock) { + synchronized (mPpLock) { for (Iterator<PictureProfileCallbackRecord> it = mPpCallbackRecords.iterator(); it.hasNext(); ) { PictureProfileCallbackRecord record = it.next(); @@ -416,7 +419,7 @@ public final class MediaQualityManager { @NonNull SoundProfileCallback callback) { Preconditions.checkNotNull(callback); Preconditions.checkNotNull(executor); - synchronized (mLock) { + synchronized (mSpLock) { mSpCallbackRecords.add(new SoundProfileCallbackRecord(callback, executor)); } } @@ -426,7 +429,7 @@ public final class MediaQualityManager { */ public void unregisterSoundProfileCallback(@NonNull final SoundProfileCallback callback) { Preconditions.checkNotNull(callback); - synchronized (mLock) { + synchronized (mSpLock) { for (Iterator<SoundProfileCallbackRecord> it = mSpCallbackRecords.iterator(); it.hasNext(); ) { SoundProfileCallbackRecord record = it.next(); @@ -785,7 +788,7 @@ public final class MediaQualityManager { @NonNull AmbientBacklightCallback callback) { Preconditions.checkNotNull(callback); Preconditions.checkNotNull(executor); - synchronized (mLock) { + synchronized (mAbLock) { mAbCallbackRecords.add(new AmbientBacklightCallbackRecord(callback, executor)); } } @@ -797,7 +800,7 @@ public final class MediaQualityManager { public void unregisterAmbientBacklightCallback( @NonNull final AmbientBacklightCallback callback) { Preconditions.checkNotNull(callback); - synchronized (mLock) { + synchronized (mAbLock) { for (Iterator<AmbientBacklightCallbackRecord> it = mAbCallbackRecords.iterator(); it.hasNext(); ) { AmbientBacklightCallbackRecord record = it.next(); @@ -1128,7 +1131,7 @@ public final class MediaQualityManager { @NonNull Consumer<List<ActiveProcessingPicture>> listener) { Preconditions.checkNotNull(listener); Preconditions.checkNotNull(executor); - synchronized (mLock) { + synchronized (mApLock) { mApListenerRecords.add( new ActiveProcessingPictureListenerRecord(listener, executor, false)); } @@ -1147,7 +1150,7 @@ public final class MediaQualityManager { @NonNull Consumer<List<ActiveProcessingPicture>> listener) { Preconditions.checkNotNull(listener); Preconditions.checkNotNull(executor); - synchronized (mLock) { + synchronized (mApLock) { mApListenerRecords.add( new ActiveProcessingPictureListenerRecord(listener, executor, true)); } @@ -1160,7 +1163,7 @@ public final class MediaQualityManager { public void removeActiveProcessingPictureListener( @NonNull Consumer<List<ActiveProcessingPicture>> listener) { Preconditions.checkNotNull(listener); - synchronized (mLock) { + synchronized (mApLock) { for (Iterator<ActiveProcessingPictureListenerRecord> it = mApListenerRecords.iterator(); it.hasNext(); ) { ActiveProcessingPictureListenerRecord record = it.next(); diff --git a/media/tests/MediaRouter/Android.bp b/media/tests/MediaRouter/Android.bp index e4f88a65ed1a..e63c59d53f2a 100644 --- a/media/tests/MediaRouter/Android.bp +++ b/media/tests/MediaRouter/Android.bp @@ -9,7 +9,7 @@ package { android_test { name: "mediaroutertest", - team: "trendy_team_android_media_solutions", + team: "trendy_team_android_media_better_together", srcs: ["**/*.java"], diff --git a/mime/Android.bp b/mime/Android.bp index 20110f1dfb47..b609548fcbab 100644 --- a/mime/Android.bp +++ b/mime/Android.bp @@ -49,6 +49,17 @@ java_library { ], } +java_library { + name: "mimemap-testing-alt", + defaults: ["mimemap-defaults"], + static_libs: ["mimemap-testing-alt-res.jar"], + jarjar_rules: "jarjar-rules-alt.txt", + visibility: [ + "//cts/tests/tests/mimemap:__subpackages__", + "//frameworks/base:__subpackages__", + ], +} + // The mimemap-res.jar and mimemap-testing-res.jar genrules produce a .jar that // has the resource file in a subdirectory res/ and testres/, respectively. // They need to be in different paths because one of them ends up in a @@ -86,6 +97,19 @@ java_genrule { cmd: "mkdir $(genDir)/testres/ && cp $(in) $(genDir)/testres/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres/", } +// The same as mimemap-testing-res.jar except that the resources are placed in a different directory. +// They get bundled with CTS so that CTS can compare a device's MimeMap implementation vs. +// the stock Android one from when CTS was built. +java_genrule { + name: "mimemap-testing-alt-res.jar", + tools: [ + "soong_zip", + ], + srcs: [":mime.types.minimized-alt"], + out: ["mimemap-testing-alt-res.jar"], + cmd: "mkdir $(genDir)/testres-alt/ && cp $(in) $(genDir)/testres-alt/ && $(location soong_zip) -C $(genDir) -o $(out) -D $(genDir)/testres-alt/", +} + // Combination of all *mime.types.minimized resources. filegroup { name: "mime.types.minimized", @@ -99,6 +123,19 @@ filegroup { ], } +// Combination of all *mime.types.minimized resources. +filegroup { + name: "mime.types.minimized-alt", + visibility: [ + "//visibility:private", + ], + device_common_srcs: [ + ":debian.mime.types.minimized-alt", + ":android.mime.types.minimized", + ":vendor.mime.types.minimized", + ], +} + java_genrule { name: "android.mime.types.minimized", visibility: [ diff --git a/mime/jarjar-rules-alt.txt b/mime/jarjar-rules-alt.txt new file mode 100644 index 000000000000..9a7644325336 --- /dev/null +++ b/mime/jarjar-rules-alt.txt @@ -0,0 +1 @@ +rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidAltMimeMapFactory diff --git a/mime/jarjar-rules.txt b/mime/jarjar-rules.txt index 145d1dbf3d11..e1ea8e10314c 100644 --- a/mime/jarjar-rules.txt +++ b/mime/jarjar-rules.txt @@ -1 +1 @@ -rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory
\ No newline at end of file +rule android.content.type.DefaultMimeMapFactory android.content.type.cts.StockAndroidMimeMapFactory diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index b30b779b57b5..49cbd7181d77 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -421,6 +421,7 @@ LIBANDROID { LIBANDROID_PLATFORM { global: AThermal_setIThermalServiceForTesting; + ASystemHealth_setIHintManagerForTesting; APerformanceHint_setIHintManagerForTesting; APerformanceHint_sendHint; APerformanceHint_getThreadIds; diff --git a/native/android/system_health.cpp b/native/android/system_health.cpp index 5c07ac7bfccc..1b43e71c7bf0 100644 --- a/native/android/system_health.cpp +++ b/native/android/system_health.cpp @@ -14,6 +14,8 @@ * limitations under the License. */ +#define LOG_TAG "system_health" + #include <aidl/android/hardware/power/CpuHeadroomParams.h> #include <aidl/android/hardware/power/GpuHeadroomParams.h> #include <aidl/android/os/CpuHeadroomParamsInternal.h> @@ -23,6 +25,17 @@ #include <android/system_health.h> #include <binder/IServiceManager.h> #include <binder/Status.h> +#include <system_health_private.h> + +#include <list> +#include <map> +#include <memory> +#include <mutex> +#include <optional> +#include <utility> + +#include "android-base/thread_annotations.h" +#include "utils/SystemClock.h" using namespace android; using namespace aidl::android::os; @@ -55,9 +68,20 @@ private: IHintManager::HintManagerClientData mClientData; }; +static std::shared_ptr<IHintManager>* gIHintManagerForTesting = nullptr; +static std::shared_ptr<ASystemHealthManager> gSystemHealthManagerForTesting = nullptr; + ASystemHealthManager* ASystemHealthManager::getInstance() { static std::once_flag creationFlag; static ASystemHealthManager* instance = nullptr; + if (gSystemHealthManagerForTesting) { + return gSystemHealthManagerForTesting.get(); + } + if (gIHintManagerForTesting) { + gSystemHealthManagerForTesting = + std::shared_ptr<ASystemHealthManager>(create(*gIHintManagerForTesting)); + return gSystemHealthManagerForTesting.get(); + } std::call_once(creationFlag, []() { instance = create(nullptr); }); return instance; } @@ -121,7 +145,8 @@ int ASystemHealthManager::getCpuHeadroom(const ACpuHeadroomParams* params, float } return EPIPE; } - *outHeadroom = res->get<hal::CpuHeadroomResult::Tag::globalHeadroom>(); + *outHeadroom = res ? res->get<hal::CpuHeadroomResult::Tag::globalHeadroom>() + : std::numeric_limits<float>::quiet_NaN(); return OK; } @@ -155,37 +180,20 @@ int ASystemHealthManager::getGpuHeadroom(const AGpuHeadroomParams* params, float } return EPIPE; } - *outHeadroom = res->get<hal::GpuHeadroomResult::Tag::globalHeadroom>(); + *outHeadroom = res ? res->get<hal::GpuHeadroomResult::Tag::globalHeadroom>() + : std::numeric_limits<float>::quiet_NaN(); return OK; } int ASystemHealthManager::getCpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) { if (!mClientData.supportInfo.headroom.isCpuSupported) return ENOTSUP; - int64_t minIntervalMillis = 0; - ::ndk::ScopedAStatus ret = mHintManager->getCpuHeadroomMinIntervalMillis(&minIntervalMillis); - if (!ret.isOk()) { - ALOGE("ASystemHealth_getCpuHeadroomMinIntervalMillis fails: %s", ret.getMessage()); - if (ret.getExceptionCode() == EX_UNSUPPORTED_OPERATION) { - return ENOTSUP; - } - return EPIPE; - } - *outMinIntervalMillis = minIntervalMillis; + *outMinIntervalMillis = mClientData.supportInfo.headroom.cpuMinIntervalMillis; return OK; } int ASystemHealthManager::getGpuHeadroomMinIntervalMillis(int64_t* outMinIntervalMillis) { if (!mClientData.supportInfo.headroom.isGpuSupported) return ENOTSUP; - int64_t minIntervalMillis = 0; - ::ndk::ScopedAStatus ret = mHintManager->getGpuHeadroomMinIntervalMillis(&minIntervalMillis); - if (!ret.isOk()) { - ALOGE("ASystemHealth_getGpuHeadroomMinIntervalMillis fails: %s", ret.getMessage()); - if (ret.getExceptionCode() == EX_UNSUPPORTED_OPERATION) { - return ENOTSUP; - } - return EPIPE; - } - *outMinIntervalMillis = minIntervalMillis; + *outMinIntervalMillis = mClientData.supportInfo.headroom.gpuMinIntervalMillis; return OK; } @@ -298,7 +306,6 @@ void ACpuHeadroomParams_setTids(ACpuHeadroomParams* _Nonnull params, const int* size_t tidsSize) { LOG_ALWAYS_FATAL_IF(tids == nullptr, "%s: tids should not be null", __FUNCTION__); params->tids.resize(tidsSize); - params->tids.clear(); for (int i = 0; i < (int)tidsSize; ++i) { LOG_ALWAYS_FATAL_IF(tids[i] <= 0, "ACpuHeadroomParams_setTids: Invalid non-positive tid %d", tids[i]); @@ -355,3 +362,10 @@ void ACpuHeadroomParams_destroy(ACpuHeadroomParams* _Nullable params) { void AGpuHeadroomParams_destroy(AGpuHeadroomParams* _Nullable params) { delete params; } + +void ASystemHealth_setIHintManagerForTesting(void* iManager) { + if (iManager == nullptr) { + gSystemHealthManagerForTesting = nullptr; + } + gIHintManagerForTesting = static_cast<std::shared_ptr<IHintManager>*>(iManager); +} diff --git a/native/android/tests/system_health/Android.bp b/native/android/tests/system_health/Android.bp new file mode 100644 index 000000000000..30aeb77375ad --- /dev/null +++ b/native/android/tests/system_health/Android.bp @@ -0,0 +1,66 @@ +// 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"], +} + +cc_test { + name: "NativeSystemHealthUnitTestCases", + + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, + + srcs: ["NativeSystemHealthUnitTest.cpp"], + + shared_libs: [ + "libandroid", + "libbinder", + "libbinder_ndk", + "liblog", + "libpowermanager", + "libutils", + ], + + static_libs: [ + "libbase", + "libgmock", + "libgtest", + ], + stl: "c++_shared", + + test_suites: [ + "device-tests", + ], + + cflags: [ + "-Wall", + "-Werror", + ], + + header_libs: [ + "libandroid_headers_private", + ], +} diff --git a/native/android/tests/system_health/NativeSystemHealthUnitTest.cpp b/native/android/tests/system_health/NativeSystemHealthUnitTest.cpp new file mode 100644 index 000000000000..3f08fc66e392 --- /dev/null +++ b/native/android/tests/system_health/NativeSystemHealthUnitTest.cpp @@ -0,0 +1,231 @@ +/* + * 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. + */ + +#define LOG_TAG "NativeSystemHealthUnitTest" + +#include <aidl/android/os/IHintManager.h> +#include <android/binder_manager.h> +#include <android/binder_status.h> +#include <android/system_health.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <system_health_private.h> + +#include <memory> +#include <optional> +#include <vector> + +using namespace std::chrono_literals; +namespace hal = aidl::android::hardware::power; +using aidl::android::os::CpuHeadroomParamsInternal; +using aidl::android::os::GpuHeadroomParamsInternal; +using aidl::android::os::IHintManager; +using aidl::android::os::IHintSession; +using aidl::android::os::SessionCreationConfig; +using ndk::ScopedAStatus; +using ndk::SpAIBinder; + +using namespace android; +using namespace testing; + +class MockIHintManager : public IHintManager { +public: + MOCK_METHOD(ScopedAStatus, createHintSessionWithConfig, + (const SpAIBinder& token, hal::SessionTag tag, + const SessionCreationConfig& creationConfig, hal::SessionConfig* config, + IHintManager::SessionCreationReturn* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, setHintSessionThreads, + (const std::shared_ptr<IHintSession>& _, const ::std::vector<int32_t>& tids), + (override)); + MOCK_METHOD(ScopedAStatus, getHintSessionThreadIds, + (const std::shared_ptr<IHintSession>& _, ::std::vector<int32_t>* tids), (override)); + MOCK_METHOD(ScopedAStatus, getSessionChannel, + (const ::ndk::SpAIBinder& in_token, + std::optional<hal::ChannelConfig>* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, closeSessionChannel, (), (override)); + MOCK_METHOD(ScopedAStatus, getCpuHeadroom, + (const CpuHeadroomParamsInternal& _, + std::optional<hal::CpuHeadroomResult>* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, getCpuHeadroomMinIntervalMillis, (int64_t*), (override)); + MOCK_METHOD(ScopedAStatus, getGpuHeadroom, + (const GpuHeadroomParamsInternal& _, + std::optional<hal::GpuHeadroomResult>* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, getGpuHeadroomMinIntervalMillis, (int64_t* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, passSessionManagerBinder, (const SpAIBinder& sessionManager)); + MOCK_METHOD(ScopedAStatus, registerClient, + (const std::shared_ptr<aidl::android::os::IHintManager::IHintManagerClient>& _, + aidl::android::os::IHintManager::HintManagerClientData* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, getClientData, + (aidl::android::os::IHintManager::HintManagerClientData * _aidl_return), + (override)); + MOCK_METHOD(SpAIBinder, asBinder, (), (override)); + MOCK_METHOD(bool, isRemote, (), (override)); +}; + +class NativeSystemHealthUnitTest : public Test { +public: + void SetUp() override { + mMockIHintManager = ndk::SharedRefBase::make<NiceMock<MockIHintManager>>(); + ASystemHealth_setIHintManagerForTesting(&mMockIHintManager); + ON_CALL(*mMockIHintManager, getClientData(_)) + .WillByDefault( + DoAll(SetArgPointee<0>(mClientData), [] { return ScopedAStatus::ok(); })); + } + + void TearDown() override { + ASystemHealth_setIHintManagerForTesting(nullptr); + } + + IHintManager::HintManagerClientData mClientData{ + .powerHalVersion = 6, + .maxCpuHeadroomThreads = 10, + .supportInfo{.headroom{ + .isCpuSupported = true, + .isGpuSupported = true, + .cpuMinIntervalMillis = 999, + .gpuMinIntervalMillis = 998, + .cpuMinCalculationWindowMillis = 45, + .cpuMaxCalculationWindowMillis = 9999, + .gpuMinCalculationWindowMillis = 46, + .gpuMaxCalculationWindowMillis = 9998, + }}, + }; + + std::shared_ptr<NiceMock<MockIHintManager>> mMockIHintManager = nullptr; +}; + +TEST_F(NativeSystemHealthUnitTest, headroomParamsValueRange) { + int64_t minIntervalMillis = 0; + int minCalculationWindowMillis = 0; + int maxCalculationWindowMillis = 0; + ASSERT_EQ(OK, ASystemHealth_getCpuHeadroomMinIntervalMillis(&minIntervalMillis)); + ASSERT_EQ(OK, + ASystemHealth_getCpuHeadroomCalculationWindowRange(&minCalculationWindowMillis, + &maxCalculationWindowMillis)); + ASSERT_EQ(minIntervalMillis, mClientData.supportInfo.headroom.cpuMinIntervalMillis); + ASSERT_EQ(minCalculationWindowMillis, + mClientData.supportInfo.headroom.cpuMinCalculationWindowMillis); + ASSERT_EQ(maxCalculationWindowMillis, + mClientData.supportInfo.headroom.cpuMaxCalculationWindowMillis); + + ASSERT_EQ(OK, ASystemHealth_getGpuHeadroomMinIntervalMillis(&minIntervalMillis)); + ASSERT_EQ(OK, + ASystemHealth_getGpuHeadroomCalculationWindowRange(&minCalculationWindowMillis, + &maxCalculationWindowMillis)); + ASSERT_EQ(minIntervalMillis, mClientData.supportInfo.headroom.gpuMinIntervalMillis); + ASSERT_EQ(minCalculationWindowMillis, + mClientData.supportInfo.headroom.gpuMinCalculationWindowMillis); + ASSERT_EQ(maxCalculationWindowMillis, + mClientData.supportInfo.headroom.gpuMaxCalculationWindowMillis); +} + +TEST_F(NativeSystemHealthUnitTest, getCpuHeadroom) { + CpuHeadroomParamsInternal internalParams1; + ACpuHeadroomParams* params2 = ACpuHeadroomParams_create(); + ACpuHeadroomParams_setCalculationWindowMillis(params2, 200); + CpuHeadroomParamsInternal internalParams2; + internalParams2.calculationWindowMillis = 200; + ACpuHeadroomParams* params3 = ACpuHeadroomParams_create(); + ACpuHeadroomParams_setCalculationType(params3, ACPU_HEADROOM_CALCULATION_TYPE_AVERAGE); + CpuHeadroomParamsInternal internalParams3; + internalParams3.calculationType = hal::CpuHeadroomParams::CalculationType::AVERAGE; + ACpuHeadroomParams* params4 = ACpuHeadroomParams_create(); + int tids[3] = {1, 2, 3}; + ACpuHeadroomParams_setTids(params4, tids, 3); + CpuHeadroomParamsInternal internalParams4; + internalParams4.tids = {1, 2, 3}; + + EXPECT_CALL(*mMockIHintManager, getCpuHeadroom(internalParams1, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(hal::CpuHeadroomResult::make< + hal::CpuHeadroomResult::globalHeadroom>(1.0f)), + [] { return ScopedAStatus::ok(); })); + EXPECT_CALL(*mMockIHintManager, getCpuHeadroom(internalParams2, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(hal::CpuHeadroomResult::make< + hal::CpuHeadroomResult::globalHeadroom>(2.0f)), + [] { return ScopedAStatus::ok(); })); + EXPECT_CALL(*mMockIHintManager, getCpuHeadroom(internalParams3, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(std::nullopt), [] { return ScopedAStatus::ok(); })); + EXPECT_CALL(*mMockIHintManager, getCpuHeadroom(internalParams4, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(hal::CpuHeadroomResult::make< + hal::CpuHeadroomResult::globalHeadroom>(4.0f)), + [] { return ScopedAStatus::ok(); })); + + float headroom1 = 0.0f; + float headroom2 = 0.0f; + float headroom3 = 0.0f; + float headroom4 = 0.0f; + ASSERT_EQ(OK, ASystemHealth_getCpuHeadroom(nullptr, &headroom1)); + ASSERT_EQ(OK, ASystemHealth_getCpuHeadroom(params2, &headroom2)); + ASSERT_EQ(OK, ASystemHealth_getCpuHeadroom(params3, &headroom3)); + ASSERT_EQ(OK, ASystemHealth_getCpuHeadroom(params4, &headroom4)); + ASSERT_EQ(1.0f, headroom1); + ASSERT_EQ(2.0f, headroom2); + ASSERT_TRUE(isnan(headroom3)); + ASSERT_EQ(4.0f, headroom4); + + ACpuHeadroomParams_destroy(params2); + ACpuHeadroomParams_destroy(params3); + ACpuHeadroomParams_destroy(params4); +} + +TEST_F(NativeSystemHealthUnitTest, getGpuHeadroom) { + GpuHeadroomParamsInternal internalParams1; + AGpuHeadroomParams* params2 = AGpuHeadroomParams_create(); + AGpuHeadroomParams_setCalculationWindowMillis(params2, 200); + GpuHeadroomParamsInternal internalParams2; + internalParams2.calculationWindowMillis = 200; + AGpuHeadroomParams* params3 = AGpuHeadroomParams_create(); + AGpuHeadroomParams_setCalculationType(params3, AGPU_HEADROOM_CALCULATION_TYPE_AVERAGE); + GpuHeadroomParamsInternal internalParams3; + internalParams3.calculationType = hal::GpuHeadroomParams::CalculationType::AVERAGE; + + EXPECT_CALL(*mMockIHintManager, getGpuHeadroom(internalParams1, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(hal::GpuHeadroomResult::make< + hal::GpuHeadroomResult::globalHeadroom>(1.0f)), + [] { return ScopedAStatus::ok(); })); + EXPECT_CALL(*mMockIHintManager, getGpuHeadroom(internalParams2, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(hal::GpuHeadroomResult::make< + hal::GpuHeadroomResult::globalHeadroom>(2.0f)), + [] { return ScopedAStatus::ok(); })); + EXPECT_CALL(*mMockIHintManager, getGpuHeadroom(internalParams3, _)) + .Times(Exactly(1)) + .WillOnce(DoAll(SetArgPointee<1>(std::nullopt), [] { return ScopedAStatus::ok(); })); + + float headroom1 = 0.0f; + float headroom2 = 0.0f; + float headroom3 = 0.0f; + ASSERT_EQ(OK, ASystemHealth_getGpuHeadroom(nullptr, &headroom1)); + ASSERT_EQ(OK, ASystemHealth_getGpuHeadroom(params2, &headroom2)); + ASSERT_EQ(OK, ASystemHealth_getGpuHeadroom(params3, &headroom3)); + ASSERT_EQ(1.0f, headroom1); + ASSERT_EQ(2.0f, headroom2); + ASSERT_TRUE(isnan(headroom3)); + + AGpuHeadroomParams_destroy(params2); + AGpuHeadroomParams_destroy(params3); +} diff --git a/native/android/tests/system_health/OWNERS b/native/android/tests/system_health/OWNERS new file mode 100644 index 000000000000..e3bbee92057d --- /dev/null +++ b/native/android/tests/system_health/OWNERS @@ -0,0 +1 @@ +include /ADPF_OWNERS diff --git a/packages/CrashRecovery/framework/Android.bp b/packages/CrashRecovery/framework/Android.bp index 1a3446ec56de..5dd42bb633e5 100644 --- a/packages/CrashRecovery/framework/Android.bp +++ b/packages/CrashRecovery/framework/Android.bp @@ -1,8 +1,8 @@ filegroup { name: "framework-crashrecovery-sources", srcs: [ - "java/**/*.java", "java/**/*.aidl", + "java/**/*.java", ], path: "java", visibility: [ @@ -12,11 +12,14 @@ filegroup { java_sdk_library { name: "framework-platformcrashrecovery", - srcs: [":framework-crashrecovery-sources"], + srcs: [ + ":framework-crashrecovery-module-sources", + ":framework-crashrecovery-sources", + ], defaults: ["framework-non-updatable-unbundled-defaults"], permitted_packages: [ - "android.service.watchdog", "android.crashrecovery", + "android.service.watchdog", ], static_libs: ["android.crashrecovery.flags-aconfig-java"], aidl: { diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java index 40bc5f78a9c6..846da194b3c3 100644 --- a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java @@ -29,18 +29,13 @@ import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.crashrecovery.flags.Flags; import android.os.Build; -import android.os.Environment; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.SystemClock; import android.os.SystemProperties; -import android.os.UserHandle; -import android.provider.DeviceConfig; import android.provider.Settings; import android.sysprop.CrashRecoveryProperties; import android.text.TextUtils; -import android.util.ArraySet; -import android.util.ArrayUtils; import android.util.EventLog; import android.util.FileUtils; import android.util.Log; @@ -56,10 +51,7 @@ import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -241,87 +233,11 @@ public class RescueParty { CrashRecoveryProperties.maxRescueLevelAttempted(level); } - private static Set<String> getPresetNamespacesForPackages(List<String> packageNames) { - Set<String> resultSet = new ArraySet<String>(); - if (!Flags.deprecateFlagsAndSettingsResets()) { - try { - String flagVal = DeviceConfig.getString(NAMESPACE_CONFIGURATION, - NAMESPACE_TO_PACKAGE_MAPPING_FLAG, ""); - String[] mappingEntries = flagVal.split(","); - for (int i = 0; i < mappingEntries.length; i++) { - if (TextUtils.isEmpty(mappingEntries[i])) { - continue; - } - String[] splitEntry = mappingEntries[i].split(":"); - if (splitEntry.length != 2) { - throw new RuntimeException("Invalid mapping entry: " + mappingEntries[i]); - } - String namespace = splitEntry[0]; - String packageName = splitEntry[1]; - - if (packageNames.contains(packageName)) { - resultSet.add(namespace); - } - } - } catch (Exception e) { - resultSet.clear(); - Slog.e(TAG, "Failed to read preset package to namespaces mapping.", e); - } finally { - return resultSet; - } - } else { - return resultSet; - } - } - @VisibleForTesting static long getElapsedRealtime() { return SystemClock.elapsedRealtime(); } - private static class RescuePartyMonitorCallback implements DeviceConfig.MonitorCallback { - Context mContext; - - RescuePartyMonitorCallback(Context context) { - this.mContext = context; - } - - public void onNamespaceUpdate(@NonNull String updatedNamespace) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - startObservingPackages(mContext, updatedNamespace); - } - } - - public void onDeviceConfigAccess(@NonNull String callingPackage, - @NonNull String namespace) { - - if (!Flags.deprecateFlagsAndSettingsResets()) { - RescuePartyObserver.getInstance(mContext).recordDeviceConfigAccess( - callingPackage, - namespace); - } - } - } - - private static void startObservingPackages(Context context, @NonNull String updatedNamespace) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - RescuePartyObserver rescuePartyObserver = RescuePartyObserver.getInstance(context); - Set<String> callingPackages = rescuePartyObserver.getCallingPackagesSet( - updatedNamespace); - if (callingPackages == null) { - return; - } - List<String> callingPackageList = new ArrayList<>(); - callingPackageList.addAll(callingPackages); - Slog.i(TAG, "Starting to observe: " + callingPackageList + ", updated namespace: " - + updatedNamespace); - PackageWatchdog.getInstance(context).startExplicitHealthCheck( - callingPackageList, - DEFAULT_OBSERVING_DURATION_MS, - rescuePartyObserver); - } - } - private static int getMaxRescueLevel(boolean mayPerformReboot) { if (Flags.recoverabilityDetection()) { if (!mayPerformReboot @@ -849,34 +765,6 @@ public class RescueParty { } } - private synchronized void recordDeviceConfigAccess(@NonNull String callingPackage, - @NonNull String namespace) { - if (!Flags.deprecateFlagsAndSettingsResets()) { - // Record it in calling packages to namespace map - Set<String> namespaceSet = mCallingPackageNamespaceSetMap.get(callingPackage); - if (namespaceSet == null) { - namespaceSet = new ArraySet<>(); - mCallingPackageNamespaceSetMap.put(callingPackage, namespaceSet); - } - namespaceSet.add(namespace); - // Record it in namespace to calling packages map - Set<String> callingPackageSet = mNamespaceCallingPackageSetMap.get(namespace); - if (callingPackageSet == null) { - callingPackageSet = new ArraySet<>(); - } - callingPackageSet.add(callingPackage); - mNamespaceCallingPackageSetMap.put(namespace, callingPackageSet); - } - } - - private synchronized Set<String> getAffectedNamespaceSet(String failedPackage) { - return mCallingPackageNamespaceSetMap.get(failedPackage); - } - - private synchronized Set<String> getAllAffectedNamespaceSet() { - return new HashSet<String>(mNamespaceCallingPackageSetMap.keySet()); - } - private synchronized Set<String> getCallingPackagesSet(String namespace) { return mNamespaceCallingPackageSetMap.get(namespace); } @@ -894,26 +782,6 @@ public class RescueParty { return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin); } - private static int[] getAllUserIds() { - int systemUserId = UserHandle.SYSTEM.getIdentifier(); - int[] userIds = { systemUserId }; - try { - for (File file : FileUtils.listFilesOrEmpty( - Environment.getDataSystemDeviceProtectedDirectory())) { - try { - final int userId = Integer.parseInt(file.getName()); - if (userId != systemUserId) { - userIds = ArrayUtils.appendInt(userIds, userId); - } - } catch (NumberFormatException ignored) { - } - } - } catch (Throwable t) { - Slog.w(TAG, "Trouble discovering users", t); - } - return userIds; - } - /** * Hacky test to check if the device has an active USB connection, which is * a good proxy for someone doing local development work. diff --git a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java index 0b7b98603419..29ff7cced897 100644 --- a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java +++ b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java @@ -16,13 +16,8 @@ package android.util; -import android.annotation.NonNull; import android.annotation.Nullable; -import java.io.File; -import java.util.List; -import java.util.Objects; - /** * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java * @@ -30,25 +25,6 @@ import java.util.Objects; */ public class ArrayUtils { private ArrayUtils() { /* cannot be instantiated */ } - public static final File[] EMPTY_FILE = new File[0]; - - - /** - * Return first index of {@code value} in {@code array}, or {@code -1} if - * not found. - */ - public static <T> int indexOf(@Nullable T[] array, T value) { - if (array == null) return -1; - for (int i = 0; i < array.length; i++) { - if (Objects.equals(array[i], value)) return i; - } - return -1; - } - - /** @hide */ - public static @NonNull File[] defeatNullable(@Nullable File[] val) { - return (val != null) ? val : EMPTY_FILE; - } /** * Checks if given array is null or has zero elements. @@ -63,53 +39,4 @@ public class ArrayUtils { public static boolean isEmpty(@Nullable byte[] array) { return array == null || array.length == 0; } - - /** - * Converts from List of bytes to byte array - * @param list - * @return byte[] - */ - public static byte[] toPrimitive(List<byte[]> list) { - if (list.size() == 0) { - return new byte[0]; - } - int byteLen = list.get(0).length; - byte[] array = new byte[list.size() * byteLen]; - for (int i = 0; i < list.size(); i++) { - for (int j = 0; j < list.get(i).length; j++) { - array[i * byteLen + j] = list.get(i)[j]; - } - } - return array; - } - - /** - * Adds value to given array if not already present, providing set-like - * behavior. - */ - public static @NonNull int[] appendInt(@Nullable int[] cur, int val) { - return appendInt(cur, val, false); - } - - /** - * Adds value to given array. - */ - public static @NonNull int[] appendInt(@Nullable int[] cur, int val, - boolean allowDuplicates) { - if (cur == null) { - return new int[] { val }; - } - final int n = cur.length; - if (!allowDuplicates) { - for (int i = 0; i < n; i++) { - if (cur[i] == val) { - return cur; - } - } - } - int[] ret = new int[n + 1]; - System.arraycopy(cur, 0, ret, 0, n); - ret[n] = val; - return ret; - } } diff --git a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java index 9c73feeffb6c..d60a9b9847ca 100644 --- a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java +++ b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java @@ -16,7 +16,6 @@ package android.util; -import android.annotation.NonNull; import android.annotation.Nullable; import java.io.BufferedInputStream; @@ -115,14 +114,4 @@ public class FileUtils { } return false; } - - /** - * List the files in the directory or return empty file. - * - * @hide - */ - public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { - return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles()) - : ArrayUtils.EMPTY_FILE; - } } diff --git a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java index 50823f5c9c34..488b531c2b8a 100644 --- a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java +++ b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java @@ -16,21 +16,10 @@ package android.util; -import android.annotation.NonNull; -import android.system.ErrnoException; -import android.system.Os; - -import com.android.modules.utils.TypedXmlPullParser; - -import libcore.util.XmlObjectFactory; - import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; -import java.io.BufferedInputStream; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; /** * Bits and pieces copied from hidden API of @@ -40,8 +29,6 @@ import java.io.InputStream; */ public class XmlUtils { - private static final String STRING_ARRAY_SEPARATOR = ":"; - /** @hide */ public static final void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException { @@ -76,44 +63,4 @@ public class XmlUtils { } } } - - private static XmlPullParser newPullParser() { - try { - XmlPullParser parser = XmlObjectFactory.newXmlPullParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - return parser; - } catch (XmlPullParserException e) { - throw new AssertionError(); - } - } - - /** @hide */ - public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in) - throws IOException { - final byte[] magic = new byte[4]; - if (in instanceof FileInputStream) { - try { - Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0); - } catch (ErrnoException e) { - throw e.rethrowAsIOException(); - } - } else { - if (!in.markSupported()) { - in = new BufferedInputStream(in); - } - in.mark(8); - in.read(magic); - in.reset(); - } - - final TypedXmlPullParser xml; - xml = (TypedXmlPullParser) newPullParser(); - try { - xml.setInput(in, "UTF_8"); - } catch (XmlPullParserException e) { - throw new IOException(e); - } - return xml; - } } diff --git a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java index 068074ae1b89..8e52a00fe545 100644 --- a/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java +++ b/packages/FusedLocation/src/com/android/location/fused/FusedLocationProvider.java @@ -38,6 +38,7 @@ import android.location.provider.LocationProviderBase; import android.location.provider.ProviderProperties; import android.location.provider.ProviderRequest; import android.os.Bundle; +import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; @@ -301,8 +302,13 @@ public class FusedLocationProvider extends LocationProviderBase { .setWorkSource(mRequest.getWorkSource()) .setHiddenFromAppOps(true) .build(); - mLocationManager.requestLocationUpdates(mProvider, request, - mContext.getMainExecutor(), this); + + try { + mLocationManager.requestLocationUpdates( + mProvider, request, mContext.getMainExecutor(), this); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to request location updates"); + } } } } @@ -311,7 +317,11 @@ public class FusedLocationProvider extends LocationProviderBase { synchronized (mLock) { int requestCode = mNextFlushCode++; mPendingFlushes.put(requestCode, callback); - mLocationManager.requestFlush(mProvider, this, requestCode); + try { + mLocationManager.requestFlush(mProvider, this, requestCode); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to request flush"); + } } } diff --git a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm index b384a2418ff2..b0308b5dccd4 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_romanian.kcm @@ -120,78 +120,90 @@ key EQUALS { key Q { label: 'Q' - base, capslock+shift: 'q' + base: 'q' shift, capslock: 'Q' + shift+capslock: 'q' } key W { label: 'W' - base, capslock+shift: 'w' + base: 'w' shift, capslock: 'W' + shift+capslock: 'w' } key E { label: 'E' - base, capslock+shift: 'e' + base: 'e' shift, capslock: 'E' + shift+capslock: 'e' ralt: '\u20ac' } key R { label: 'R' - base, capslock+shift: 'r' + base: 'r' shift, capslock: 'R' + shift+capslock: 'r' } key T { label: 'T' - base, capslock+shift: 't' + base: 't' shift, capslock: 'T' + shift+capslock: 't' } key Y { label: 'Y' - base, capslock+shift: 'y' + base: 'y' shift, capslock: 'Y' + shift+capslock: 'y' } key U { label: 'U' - base, capslock+shift: 'u' + base: 'u' shift, capslock: 'U' + shift+capslock: 'u' } key I { label: 'I' - base, capslock+shift: 'i' + base: 'i' shift, capslock: 'I' + shift+capslock: 'i' } key O { label: 'O' - base, capslock+shift: 'o' + base: 'o' shift, capslock: 'O' + shift+capslock: 'o' } key P { label: 'P' - base, capslock+shift: 'p' + base: 'p' shift, capslock: 'P' + shift+capslock: 'p' ralt: '\u00a7' } key LEFT_BRACKET { label: '\u0102' - base, capslock+shift: '\u0103' + base: '\u0103' shift, capslock: '\u0102' + shift+capslock: '\u0103' ralt: '[' ralt+shift: '{' } key RIGHT_BRACKET { label: '\u00ce' - base, capslock+shift: '\u00ee' + base: '\u00ee' shift, capslock: '\u00ce' + shift+capslock: '\u00ee' ralt: ']' ralt+shift: '}' } @@ -200,21 +212,24 @@ key RIGHT_BRACKET { key A { label: 'A' - base, capslock+shift: 'a' + base: 'a' shift, capslock: 'A' + shift+capslock: 'a' } key S { label: 'S' - base, capslock+shift: 's' + base: 's' shift, capslock: 'S' + shift+capslock: 's' ralt: '\u00df' } key D { label: 'D' - base, capslock+shift: 'd' + base: 'd' shift, capslock: 'D' + shift+capslock: 'd' ralt: '\u0111' ralt+shift, ralt+capslock: '\u0110' ralt+shift+capslock: '\u0111' @@ -222,38 +237,44 @@ key D { key F { label: 'F' - base, capslock+shift: 'f' + base: 'f' shift, capslock: 'F' + shift+capslock: 'f' } key G { label: 'G' - base, capslock+shift: 'g' + base: 'g' shift, capslock: 'G' + shift+capslock: 'g' } key H { label: 'H' - base, capslock+shift: 'h' + base: 'h' shift, capslock: 'H' + shift+capslock: 'h' } key J { label: 'J' - base, capslock+shift: 'j' + base: 'j' shift, capslock: 'J' + shift+capslock: 'j' } key K { label: 'K' - base, capslock+shift: 'k' + base: 'k' shift, capslock: 'K' + shift+capslock: 'k' } key L { label: 'L' - base, capslock+shift: 'l' + base: 'l' shift, capslock: 'L' + shift+capslock: 'l' ralt: '\u0142' ralt+shift, ralt+capslock: '\u0141' ralt+shift+capslock: '\u0142' @@ -261,24 +282,27 @@ key L { key SEMICOLON { label: '\u0218' - base, capslock+shift: '\u0219' + base: '\u0219' shift, capslock: '\u0218' + shift+capslock: '\u0219' ralt: ';' ralt+shift: ':' } key APOSTROPHE { label: '\u021a' - base, capslock+shift: '\u021b' + base: '\u021b' shift, capslock: '\u021a' + shift+capslock: '\u021b' ralt: '\'' ralt+shift: '\u0022' } key BACKSLASH { label: '\u00c2' - base, capslock+shift: '\u00e2' + base: '\u00e2' shift, capslock: '\u00c2' + shift+capslock: '\u00e2' ralt: '\\' ralt+shift: '|' } @@ -293,45 +317,52 @@ key PLUS { key Z { label: 'Z' - base, capslock+shift: 'z' + base: 'z' shift, capslock: 'Z' + shift+capslock: 'z' } key X { label: 'X' - base, capslock+shift: 'x' + base: 'x' shift, capslock: 'X' + shift+capslock: 'x' } key C { label: 'C' - base, capslock+shift: 'c' + base: 'c' shift, capslock: 'C' + shift+capslock: 'c' ralt: '\u00a9' } key V { label: 'V' - base, capslock+shift: 'v' + base: 'v' shift, capslock: 'V' + shift+capslock: 'v' } key B { label: 'B' - base, capslock+shift: 'b' + base: 'b' shift, capslock: 'B' + shift+capslock: 'b' } key N { label: 'N' - base, capslock+shift: 'n' + base: 'n' shift, capslock: 'N' + shift+capslock: 'n' } key M { label: 'M' - base, capslock+shift: 'm' + base: 'm' shift, capslock: 'M' + shift+capslock: 'm' } key COMMA { diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm index 6fa54f9d052f..9df78c9af923 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_cyrillic.kcm @@ -104,149 +104,173 @@ key EQUALS { key Q { label: '\u0409' - base, capslock+shift: '\u0459' + base: '\u0459' shift, capslock: '\u0409' + shift+capslock: '\u0459' } key W { label: '\u040a' - base, capslock+shift: '\u045a' + base: '\u045a' shift, capslock: '\u040a' + shift+capslock: '\u045a' } key E { label: '\u0415' - base, capslock+shift: '\u0435' + base: '\u0435' shift, capslock: '\u0415' + shift+capslock: '\u0435' ralt: '\u20ac' } key R { label: '\u0420' - base, capslock+shift: '\u0440' + base: '\u0440' shift, capslock: '\u0420' + shift+capslock: '\u0440' } key T { label: '\u0422' - base, capslock+shift: '\u0442' + base: '\u0442' shift, capslock: '\u0422' + shift+capslock: '\u0442' } key Y { label: '\u0417' - base, capslock+shift: '\u0437' + base: '\u0437' shift, capslock: '\u0417' + shift+capslock: '\u0437' } key U { label: '\u0423' - base, capslock+shift: '\u0443' + base: '\u0443' shift, capslock: '\u0423' + shift+capslock: '\u0443' } key I { label: '\u0418' - base, capslock+shift: '\u0438' + base: '\u0438' shift, capslock: '\u0418' + shift+capslock: '\u0438' } key O { label: '\u041e' - base, capslock+shift: '\u043e' + base: '\u043e' shift, capslock: '\u041e' + shift+capslock: '\u043e' } key P { label: '\u041f' - base, capslock+shift: '\u043f' + base: '\u043f' shift, capslock: '\u041f' + shift+capslock: '\u043f' } key LEFT_BRACKET { label: '\u0428' - base, capslock+shift: '\u0448' + base: '\u0448' shift, capslock: '\u0428' + shift+capslock: '\u0448' } key RIGHT_BRACKET { label: '\u0402' - base, capslock+shift: '\u0452' + base: '\u0452' shift, capslock: '\u0402' + shift+capslock: '\u0452' } ### ROW 3 key A { label: '\u0410' - base, capslock+shift: '\u0430' + base: '\u0430' shift, capslock: '\u0410' + shift+capslock: '\u0430' } key S { label: '\u0421' - base, capslock+shift: '\u0441' + base: '\u0441' shift, capslock: '\u0421' + shift+capslock: '\u0441' } key D { label: '\u0414' - base, capslock+shift: '\u0434' + base: '\u0434' shift, capslock: '\u0414' + shift+capslock: '\u0434' } key F { label: '\u0424' - base, capslock+shift: '\u0444' + base: '\u0444' shift, capslock: '\u0424' + shift+capslock: '\u0444' } key G { label: '\u0413' - base, capslock+shift: '\u0433' + base: '\u0433' shift, capslock: '\u0413' + shift+capslock: '\u0433' } key H { label: '\u0425' - base, capslock+shift: '\u0445' + base: '\u0445' shift, capslock: '\u0425' + shift+capslock: '\u0445' } key J { label: '\u0408' - base, capslock+shift: '\u0458' + base: '\u0458' shift, capslock: '\u0408' + shift+capslock: '\u0458' } key K { label: '\u041a' - base, capslock+shift: '\u043a' + base: '\u043a' shift, capslock: '\u041a' + shift+capslock: '\u043a' } key L { label: '\u041b' - base, capslock+shift: '\u043b' + base: '\u043b' shift, capslock: '\u041b' + shift+capslock: '\u043b' } key SEMICOLON { label: '\u0427' - base, capslock+shift: '\u0447' + base: '\u0447' shift, capslock: '\u0427' + shift+capslock: '\u0447' } key APOSTROPHE { label: '\u040b' - base, capslock+shift: '\u045b' + base: '\u045b' shift, capslock: '\u040b' + shift+capslock: '\u045b' } key BACKSLASH { label: '\u0416' - base, capslock+shift: '\u0436' + base: '\u0436' shift, capslock: '\u0416' + shift+capslock: '\u0436' } ### ROW 4 @@ -259,44 +283,51 @@ key PLUS { key Z { label: '\u0405' - base, capslock+shift: '\u0455' + base: '\u0455' shift, capslock: '\u0405' + shift+capslock: '\u0455' } key X { label: '\u040f' - base, capslock+shift: '\u045f' + base: '\u045f' shift, capslock: '\u040f' + shift+capslock: '\u045f' } key C { label: '\u0426' - base, capslock+shift: '\u0446' + base: '\u0446' shift, capslock: '\u0426' + shift+capslock: '\u0446' } key V { label: '\u0412' - base, capslock+shift: '\u0432' + base: '\u0432' shift, capslock: '\u0412' + shift+capslock: '\u0432' } key B { label: '\u0411' - base, capslock+shift: '\u0431' + base: '\u0431' shift, capslock: '\u0411' + shift+capslock: '\u0431' } key N { label: '\u041d' - base, capslock+shift: '\u043d' + base: '\u043d' shift, capslock: '\u041d' + shift+capslock: '\u043d' } key M { label: '\u041c' - base, capslock+shift: '\u043c' + base: '\u043c' shift, capslock: '\u041c' + shift+capslock: '\u043c' } key COMMA { @@ -317,4 +348,4 @@ key SLASH { label: '-' base: '-' shift: '_' -}
\ No newline at end of file +} diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm index 8e4d7b147faa..4c8997b16a26 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_and_montenegrin_latin.kcm @@ -120,78 +120,90 @@ key EQUALS { key Q { label: 'Q' - base, capslock+shift: 'q' + base: 'q' shift, capslock: 'Q' + shift+capslock: 'q' ralt: '\\' } key W { label: 'W' - base, capslock+shift: 'w' + base: 'w' shift, capslock: 'W' + shift+capslock: 'w' ralt: '|' } key E { label: 'E' - base, capslock+shift: 'e' + base: 'e' shift, capslock: 'E' + shift+capslock: 'e' ralt: '\u20ac' } key R { label: 'R' - base, capslock+shift: 'r' + base: 'r' shift, capslock: 'R' + shift+capslock: 'r' } key T { label: 'T' - base, capslock+shift: 't' + base: 't' shift, capslock: 'T' + shift+capslock: 't' } key Z { label: 'Z' - base, capslock+shift: 'z' + base: 'z' shift, capslock: 'Z' + shift+capslock: 'z' } key U { label: 'U' - base, capslock+shift: 'u' + base: 'u' shift, capslock: 'U' + shift+capslock: 'u' } key I { label: 'I' - base, capslock+shift: 'i' + base: 'i' shift, capslock: 'I' + shift+capslock: 'i' } key O { label: 'O' - base, capslock+shift: 'o' + base: 'o' shift, capslock: 'O' + shift+capslock: 'o' } key P { label: 'P' - base, capslock+shift: 'p' + base: 'p' shift, capslock: 'P' + shift+capslock: 'p' } key LEFT_BRACKET { label: '\u0160' - base, capslock+shift: '\u0161' + base: '\u0161' shift, capslock: '\u0160' + shift+capslock: '\u0161' ralt: '\u00f7' } key RIGHT_BRACKET { label: '\u0110' - base, capslock+shift: '\u0111' + base: '\u0111' shift, capslock: '\u0110' + shift+capslock: '\u0111' ralt: '\u00d7' } @@ -199,79 +211,91 @@ key RIGHT_BRACKET { key A { label: 'A' - base, capslock+shift: 'a' + base: 'a' shift, capslock: 'A' + shift+capslock: 'a' } key S { label: 'S' - base, capslock+shift: 's' + base: 's' shift, capslock: 'S' + shift+capslock: 's' } key D { label: 'D' - base, capslock+shift: 'd' + base: 'd' shift, capslock: 'D' + shift+capslock: 'd' } key F { label: 'F' - base, capslock+shift: 'f' + base: 'f' shift, capslock: 'F' + shift+capslock: 'f' ralt: '[' } key G { label: 'G' - base, capslock+shift: 'g' + base: 'g' shift, capslock: 'G' + shift+capslock: 'g' ralt: ']' } key H { label: 'H' - base, capslock+shift: 'h' + base: 'h' shift, capslock: 'H' + shift+capslock: 'h' } key J { label: 'J' - base, capslock+shift: 'j' + base: 'j' shift, capslock: 'J' + shift+capslock: 'j' } key K { label: 'K' - base, capslock+shift: 'k' + base: 'k' shift, capslock: 'K' + shift+capslock: 'k' ralt: '\u0142' } key L { label: 'L' - base, capslock+shift: 'l' + base: 'l' shift, capslock: 'L' + shift+capslock: 'l' ralt: '\u0141' } key SEMICOLON { label: '\u010c' - base, capslock+shift: '\u010d' + base: '\u010d' shift, capslock: '\u010c' + shift+capslock: '\u010d' } key APOSTROPHE { label: '\u0106' - base, capslock+shift: '\u0107' + base: '\u0107' shift, capslock: '\u0106' + shift+capslock: '\u0107' ralt: '\u00df' } key BACKSLASH { label: '\u017d' - base, capslock+shift: '\u017e' + base: '\u017e' shift, capslock: '\u017d' + shift+capslock: '\u017e' ralt: '\u00a4' } @@ -285,47 +309,54 @@ key PLUS { key Y { label: 'Y' - base, capslock+shift: 'y' + base: 'y' shift, capslock: 'Y' + shift+capslock: 'y' } key X { label: 'X' - base, capslock+shift: 'x' + base: 'x' shift, capslock: 'X' + shift+capslock: 'x' } key C { label: 'C' - base, capslock+shift: 'c' + base: 'c' shift, capslock: 'C' + shift+capslock: 'c' } key V { label: 'V' - base, capslock+shift: 'v' + base: 'v' shift, capslock: 'V' + shift+capslock: 'v' ralt: '@' } key B { label: 'B' - base, capslock+shift: 'b' + base: 'b' shift, capslock: 'B' + shift+capslock: 'b' ralt: '{' } key N { label: 'N' - base, capslock+shift: 'n' + base: 'n' shift, capslock: 'N' + shift+capslock: 'n' ralt: '}' } key M { label: 'M' - base, capslock+shift: 'm' + base: 'm' shift, capslock: 'M' + shift+capslock: 'm' ralt: '\u00a7' } @@ -347,4 +378,4 @@ key MINUS { label: '-' base: '-' shift: '_' -}
\ No newline at end of file +} diff --git a/packages/PrintSpooler/Android.bp b/packages/PrintSpooler/Android.bp index 6af3c6624f62..5c5ec69a4140 100644 --- a/packages/PrintSpooler/Android.bp +++ b/packages/PrintSpooler/Android.bp @@ -47,18 +47,36 @@ android_library { resource_dirs: ["res"], srcs: [ "src/**/*.java", - "src/com/android/printspooler/renderer/IPdfRenderer.aidl", "src/com/android/printspooler/renderer/IPdfEditor.aidl", + "src/com/android/printspooler/renderer/IPdfRenderer.aidl", ], platform_apis: true, static_libs: [ - "android-support-v7-recyclerview", + "android-support-annotations", "android-support-compat", - "android-support-media-compat", - "android-support-core-utils", "android-support-core-ui", + "android-support-core-utils", "android-support-fragment", - "android-support-annotations", + "android-support-media-compat", + "android-support-v7-recyclerview", + "printspooler_aconfig_flags_java_lib", + ], + flags_packages: [ + "printspooler_aconfig_declarations", ], manifest: "AndroidManifest.xml", } + +aconfig_declarations { + name: "printspooler_aconfig_declarations", + package: "com.android.printspooler.flags", + container: "system", + srcs: [ + "flags/flags.aconfig", + ], +} + +java_aconfig_library { + name: "printspooler_aconfig_flags_java_lib", + aconfig_declarations: "printspooler_aconfig_declarations", +} diff --git a/packages/PrintSpooler/flags/flags.aconfig b/packages/PrintSpooler/flags/flags.aconfig new file mode 100644 index 000000000000..5d45b3f0303d --- /dev/null +++ b/packages/PrintSpooler/flags/flags.aconfig @@ -0,0 +1,20 @@ +package: "com.android.printspooler.flags" +container: "system" + +flag { + name: "log_print_jobs" + namespace: "printing" + description: "Log print job creation and state transitions." + bug: "385340868" +} + +flag { + name: "print_edge2edge" + namespace: "printing" + description: "Enable edge to edge in print spooler" + bug: "378652618" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +}
\ No newline at end of file diff --git a/packages/PrintSpooler/res/layout/select_printer_activity.xml b/packages/PrintSpooler/res/layout/select_printer_activity.xml index 681924b334d0..c5f19b1df6fd 100644 --- a/packages/PrintSpooler/res/layout/select_printer_activity.xml +++ b/packages/PrintSpooler/res/layout/select_printer_activity.xml @@ -15,6 +15,7 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/select_printer" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> diff --git a/packages/PrintSpooler/res/values-night/themes.xml b/packages/PrintSpooler/res/values-night/themes.xml index 76fa7b921e77..495bbcac1fb7 100644 --- a/packages/PrintSpooler/res/values-night/themes.xml +++ b/packages/PrintSpooler/res/values-night/themes.xml @@ -15,7 +15,7 @@ limitations under the License. --> -<resources> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="Theme.AddPrinterActivity" parent="@android:style/Theme.DeviceDefault.Dialog"> <item name="android:listSeparatorTextViewStyle">@style/ListSeparator</item> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> @@ -24,14 +24,14 @@ <style name="Theme.SelectPrinterActivity" parent="android:style/Theme.DeviceDefault"> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item> </style> <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault"> <item name="android:windowIsTranslucent">true</item> <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item> </style> </resources> diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml index 22842f724036..5fcbbf53a57d 100644 --- a/packages/PrintSpooler/res/values/themes.xml +++ b/packages/PrintSpooler/res/values/themes.xml @@ -14,7 +14,7 @@ limitations under the License. --> -<resources> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> <style name="Theme.AddPrinterActivity" parent="@android:style/Theme.DeviceDefault.Light.Dialog"> <item name="android:listSeparatorTextViewStyle">@style/ListSeparator</item> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> @@ -24,7 +24,7 @@ parent="android:style/Theme.DeviceDefault.Light"> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> <item name="android:windowLightStatusBar">true</item> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item> </style> <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault.Light"> @@ -32,7 +32,7 @@ <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> <item name="android:windowLightStatusBar">true</item> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement" android:featureFlag="!com.android.printspooler.flags.print_edge2edge">true</item> </style> </resources> diff --git a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java index bba57d5fe0a2..1a9309c13bd7 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java +++ b/packages/PrintSpooler/src/com/android/printspooler/model/PrintSpoolerService.java @@ -68,6 +68,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.dump.DualDumpOutputStream; import com.android.internal.util.function.pooled.PooledLambda; import com.android.printspooler.R; +import com.android.printspooler.flags.Flags; import com.android.printspooler.util.ApprovedPrintServices; import libcore.io.IoUtils; @@ -493,7 +494,7 @@ public final class PrintSpoolerService extends Service { keepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[ADD] " + printJob); } } @@ -506,7 +507,7 @@ public final class PrintSpoolerService extends Service { PrintJobInfo printJob = mPrintJobs.get(i); if (isObsoleteState(printJob.getState())) { mPrintJobs.remove(i); - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[REMOVE] " + printJob.getId().flattenToString()); } removePrintJobFileLocked(printJob.getId()); @@ -568,7 +569,7 @@ public final class PrintSpoolerService extends Service { checkIfStillKeepAwakeLocked(); } - if (DEBUG_PRINT_JOB_LIFECYCLE) { + if (Flags.logPrintJobs() || DEBUG_PRINT_JOB_LIFECYCLE) { Slog.i(LOG_TAG, "[STATE CHANGED] " + printJob); } diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java index 74acf677918e..559d3ea2e830 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java +++ b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java @@ -66,10 +66,10 @@ import android.widget.Toast; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.printspooler.R; +import com.android.printspooler.flags.Flags; import java.util.ArrayList; import java.util.List; - /** * This is an activity for selecting a printer. */ @@ -134,6 +134,8 @@ public final class SelectPrinterActivity extends Activity implements mPrinterRegistry = new PrinterRegistry(this, null, LOADER_ID_PRINT_REGISTRY, LOADER_ID_PRINT_REGISTRY_INT); + findViewById(R.id.select_printer).setFitsSystemWindows(Flags.printEdge2edge()); + // Hook up the list view. mListView = findViewById(android.R.id.list); final DestinationAdapter adapter = new DestinationAdapter(); diff --git a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java index 6ecffa4752cf..720d5b1f4e35 100644 --- a/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java +++ b/packages/PrintSpooler/src/com/android/printspooler/widget/PrintContentView.java @@ -25,6 +25,7 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import com.android.printspooler.R; +import com.android.printspooler.flags.Flags; /** * This class is a layout manager for the print screen. It has a sliding @@ -93,6 +94,7 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis // The options view is sliding under the static header but appears // after it in the layout, so we will draw in opposite order. setChildrenDrawingOrderEnabled(true); + setFitsSystemWindows(Flags.printEdge2edge()); } public void setOptionsStateChangeListener(OptionsStateChangeListener listener) { @@ -148,6 +150,7 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis mExpandCollapseHandle = findViewById(R.id.expand_collapse_handle); mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon); + mOptionsContainer.setFitsSystemWindows(Flags.printEdge2edge()); mExpandCollapseHandle.setOnClickListener(this); mSummaryContent.setOnClickListener(this); @@ -262,7 +265,7 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis } // The content host can grow vertically as much as needed - we will be covering it. - final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0); + final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); measureChild(mEmbeddedContentContainer, widthMeasureSpec, hostHeightMeasureSpec); setMeasuredDimension(resolveSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec), @@ -271,25 +274,43 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - mStaticContent.layout(left, top, right, mStaticContent.getMeasuredHeight()); + final int childLeft; + final int childRight; + final int childTop; + if (Flags.printEdge2edge()) { + childLeft = left + mPaddingLeft; + childRight = right - mPaddingRight; + childTop = top + mPaddingTop; + } else { + childLeft = left; + childRight = right; + childTop = top; + } + mStaticContent.layout(childLeft, childTop, childRight, + mStaticContent.getMeasuredHeight() + (Flags.printEdge2edge() ? mPaddingTop : 0)); if (mSummaryContent.getVisibility() != View.GONE) { - mSummaryContent.layout(left, mStaticContent.getMeasuredHeight(), right, - mStaticContent.getMeasuredHeight() + mSummaryContent.getMeasuredHeight()); + mSummaryContent.layout(childLeft, + (Flags.printEdge2edge() ? mStaticContent.getBottom() + : mStaticContent.getMeasuredHeight()), childRight, + (Flags.printEdge2edge() ? mStaticContent.getBottom() + : mStaticContent.getMeasuredHeight()) + + mSummaryContent.getMeasuredHeight()); } - final int dynContentTop = mStaticContent.getMeasuredHeight() + mCurrentOptionsOffsetY; + final int dynContentTop = mStaticContent.getBottom() + mCurrentOptionsOffsetY; final int dynContentBottom = dynContentTop + mDynamicContent.getMeasuredHeight(); - mDynamicContent.layout(left, dynContentTop, right, dynContentBottom); + mDynamicContent.layout(childLeft, dynContentTop, childRight, dynContentBottom); MarginLayoutParams params = (MarginLayoutParams) mPrintButton.getLayoutParams(); final int printButtonLeft; if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { - printButtonLeft = right - mPrintButton.getMeasuredWidth() - params.getMarginStart(); + printButtonLeft = childRight - mPrintButton.getMeasuredWidth() + - params.getMarginStart(); } else { - printButtonLeft = left + params.getMarginStart(); + printButtonLeft = childLeft + params.getMarginStart(); } final int printButtonTop = dynContentBottom - mPrintButton.getMeasuredHeight() / 2; final int printButtonRight = printButtonLeft + mPrintButton.getMeasuredWidth(); @@ -297,11 +318,13 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis mPrintButton.layout(printButtonLeft, printButtonTop, printButtonRight, printButtonBottom); - final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY - + mDynamicContent.getMeasuredHeight(); - final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight(); + final int embContentTop = (Flags.printEdge2edge() ? mPaddingTop : 0) + + mStaticContent.getMeasuredHeight() + + mClosedOptionsOffsetY + mDynamicContent.getMeasuredHeight(); + final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight() + - (Flags.printEdge2edge() ? mPaddingBottom : 0); - mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom); + mEmbeddedContentContainer.layout(childLeft, embContentTop, childRight, embContentBottom); } @Override diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt index 145fabea52af..ac36b08512e8 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/DataChangeReason.kt @@ -39,5 +39,7 @@ annotation class DataChangeReason { const val RESTORE = 3 /** Data is synced from another profile (e.g. personal profile to work profile). */ const val SYNC_ACROSS_PROFILES = 4 + + fun isDataChange(reason: Int): Boolean = reason in UNKNOWN..SYNC_ACROSS_PROFILES } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt index 2fac54557bef..6fc6b5405eb2 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterApi.kt @@ -22,6 +22,7 @@ import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.ipc.ApiDescriptor import com.android.settingslib.ipc.ApiHandler import com.android.settingslib.ipc.ApiPermissionChecker +import com.android.settingslib.metadata.PreferenceCoordinate import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceScreenRegistry diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt index ff14eb5aae55..70ce62c8383c 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGetterCodecs.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.os.Parcel import com.android.settingslib.graph.proto.PreferenceProto import com.android.settingslib.ipc.MessageCodec +import com.android.settingslib.metadata.PreferenceCoordinate import java.util.Arrays /** Message codec for [PreferenceGetterRequest]. */ diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index 3c870acf2291..ea795542a5f6 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -34,6 +34,8 @@ import com.android.settingslib.metadata.PreferenceRestrictionProvider import com.android.settingslib.metadata.PreferenceScreenRegistry import com.android.settingslib.metadata.RangeValue import com.android.settingslib.metadata.ReadWritePermit +import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY +import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY /** Request to set preference value. */ data class PreferenceSetterRequest( @@ -187,6 +189,8 @@ fun <T> PersistentPreference<T>.evalWritePermit( callingUid: Int, ): Int = when { + sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY -> + ReadWritePermit.DISALLOW getWritePermissions(context)?.check(context, callingPid, callingUid) == false -> ReadWritePermit.REQUIRE_APP_PERMISSION else -> getWritePermit(context, value, callingPid, callingUid) diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index e5bf41f87999..83725aaec377 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -44,6 +44,24 @@ annotation class ReadWritePermit { } } +/** The reason of preference change. */ +@IntDef( + PreferenceChangeReason.VALUE, + PreferenceChangeReason.STATE, + PreferenceChangeReason.DEPENDENT, +) +@Retention(AnnotationRetention.SOURCE) +annotation class PreferenceChangeReason { + companion object { + /** Preference value is changed. */ + const val VALUE = 1000 + /** Preference state (title/summary, enable state, etc.) is changed. */ + const val STATE = 1001 + /** Dependent preference state is changed. */ + const val DEPENDENT = 1002 + } +} + /** Indicates how sensitive of the data. */ @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.TYPE) diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt index 68aa2d258295..2dd736ae6083 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceCoordinate.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceCoordinate.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 The Android Open Source Project + * Copyright (C) 2025 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.settingslib.graph +package com.android.settingslib.metadata import android.os.Parcel import android.os.Parcelable diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 91abd8b4c9e9..8358ab921fb6 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -25,12 +25,14 @@ import androidx.preference.Preference import androidx.preference.PreferenceDataStore import androidx.preference.PreferenceGroup import androidx.preference.PreferenceScreen +import com.android.settingslib.datastore.DataChangeReason import com.android.settingslib.datastore.HandlerExecutor import com.android.settingslib.datastore.KeyValueStore import com.android.settingslib.datastore.KeyedDataObservable import com.android.settingslib.datastore.KeyedObservable import com.android.settingslib.datastore.KeyedObserver import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceChangeReason import com.android.settingslib.metadata.PreferenceHierarchy import com.android.settingslib.metadata.PreferenceHierarchyNode import com.android.settingslib.metadata.PreferenceLifecycleContext @@ -73,7 +75,7 @@ class PreferenceScreenBindingHelper( ?.keyValueStore override fun notifyPreferenceChange(key: String) = - notifyChange(key, CHANGE_REASON_STATE) + notifyChange(key, PreferenceChangeReason.STATE) @Suppress("DEPRECATION") override fun startActivityForResult( @@ -91,7 +93,13 @@ class PreferenceScreenBindingHelper( private val preferenceObserver: KeyedObserver<String?> private val storageObserver = - KeyedObserver<String> { key, _ -> notifyChange(key, CHANGE_REASON_VALUE) } + KeyedObserver<String> { key, reason -> + if (DataChangeReason.isDataChange(reason)) { + notifyChange(key, PreferenceChangeReason.VALUE) + } else { + notifyChange(key, PreferenceChangeReason.STATE) + } + } init { val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>() @@ -148,7 +156,7 @@ class PreferenceScreenBindingHelper( } // check reason to avoid potential infinite loop - if (reason != CHANGE_REASON_DEPENDENT) { + if (reason != PreferenceChangeReason.DEPENDENT) { notifyDependents(key, mutableSetOf()) } } @@ -157,7 +165,7 @@ class PreferenceScreenBindingHelper( private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) { if (!notifiedKeys.add(key)) return for (dependency in dependencies[key]) { - notifyChange(dependency, CHANGE_REASON_DEPENDENT) + notifyChange(dependency, PreferenceChangeReason.DEPENDENT) notifyDependents(dependency, notifiedKeys) } } @@ -210,13 +218,6 @@ class PreferenceScreenBindingHelper( } companion object { - /** Preference value is changed. */ - const val CHANGE_REASON_VALUE = 0 - /** Preference state (title/summary, enable state, etc.) is changed. */ - const val CHANGE_REASON_STATE = 1 - /** Dependent preference state is changed. */ - const val CHANGE_REASON_DEPENDENT = 2 - /** Updates preference screen that has incomplete hierarchy. */ @JvmStatic fun bind(preferenceScreen: PreferenceScreen) { diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt index 5b7e2a86135a..e6cc8a80ee38 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/common/BytesFormatter.kt @@ -24,6 +24,7 @@ import android.icu.text.NumberFormat import android.icu.text.UnicodeSet import android.icu.text.UnicodeSetSpanner import android.icu.util.Measure +import android.text.BidiFormatter import android.text.format.Formatter import android.text.format.Formatter.RoundedBytesResult import java.math.BigDecimal @@ -40,11 +41,17 @@ class BytesFormatter(resources: Resources) { constructor(context: Context) : this(context.resources) private val locale = resources.configuration.locales[0] + private val bidiFormatter = BidiFormatter.getInstance(locale) fun format(bytes: Long, useCase: UseCase): String { val rounded = RoundedBytesResult.roundBytes(bytes, useCase.flag) val numberFormatter = getNumberFormatter(rounded.fractionDigits) - return numberFormatter.formatRoundedBytesResult(rounded) + val formattedString = numberFormatter.formatRoundedBytesResult(rounded) + return if (useCase == UseCase.FileSize) { + formattedString.bidiWrap() + } else { + formattedString + } } fun formatWithUnits(bytes: Long, useCase: UseCase): Result { @@ -74,6 +81,14 @@ class BytesFormatter(resources: Resources) { } } + /** Wraps the source string in bidi formatting characters in RTL locales. */ + private fun String.bidiWrap(): String = + if (bidiFormatter.isRtlContext) { + bidiFormatter.unicodeWrap(this) + } else { + this + } + private companion object { fun String.removeFirst(removed: String): String = SPACES_AND_CONTROLS.trim(replaceFirst(removed, "")).toString() diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index e1e1ee5a8feb..78d6c31ac783 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -88,6 +88,7 @@ class AppListRepositoryImpl( matchAnyUserForAdmin: Boolean, ): List<ApplicationInfo> = try { coroutineScope { + // TODO(b/382016780): to be removed after flag cleanup. val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } val hideWhenDisabledPackagesDeferred = async { context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) @@ -95,6 +96,7 @@ class AppListRepositoryImpl( val installedApplicationsAsUser = getInstalledApplications(userId, matchAnyUserForAdmin) + // TODO(b/382016780): to be removed after flag cleanup. val hiddenSystemModules = hiddenSystemModulesDeferred.await() val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() installedApplicationsAsUser.filter { app -> @@ -206,6 +208,7 @@ class AppListRepositoryImpl( private fun isSystemApp(app: ApplicationInfo, homeOrLauncherPackages: Set<String>): Boolean = app.isSystemApp && !app.isUpdatedSystemApp && app.packageName !in homeOrLauncherPackages + // TODO(b/382016780): to be removed after flag cleanup. private fun PackageManager.getHiddenSystemModules(): Set<String> { val moduleInfos = getInstalledModules(0).filter { it.isHidden } val hiddenApps = moduleInfos.mapNotNull { it.packageName }.toMutableSet() @@ -218,13 +221,14 @@ class AppListRepositoryImpl( companion object { private const val TAG = "AppListRepository" + // TODO(b/382016780): to be removed after flag cleanup. private fun ApplicationInfo.isInAppList( showInstantApps: Boolean, hiddenSystemModules: Set<String>, hideWhenDisabledPackages: Array<String>, ) = when { !showInstantApps && isInstantApp -> false - packageName in hiddenSystemModules -> false + !Flags.removeHiddenModuleUsage() && (packageName in hiddenSystemModules) -> false packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed enabled -> true else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt new file mode 100644 index 000000000000..6fd470c1e7aa --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepository.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.util.Log +import com.android.settingslib.spaprivileged.framework.common.BytesFormatter +import com.android.settingslib.spaprivileged.framework.common.storageStatsManager + +/** A repository interface for accessing and formatting app storage information. */ +interface AppStorageRepository { + /** + * Formats the size of an application into a human-readable string. + * + * This function retrieves the total size of the application, including APK file and its + * associated data. + * + * This function takes an [ApplicationInfo] object as input and returns a formatted string + * representing the size of the application. The size is formatted in units like kB, MB, GB, + * etc. + * + * @param app The [ApplicationInfo] object representing the application. + * @return A formatted string representing the size of the application. + */ + fun formatSize(app: ApplicationInfo): String + + /** + * Formats the size about an application into a human-readable string. + * + * @param sizeBytes The size in bytes to format. + * @return A formatted string representing the size about application. + */ + fun formatSizeBytes(sizeBytes: Long): String + + /** + * Calculates the size of an application in bytes. + * + * This function retrieves the total size of the application, including APK file and its + * associated data. + * + * @param app The [ApplicationInfo] object representing the application. + * @return The total size of the application in bytes, or null if the size could not be + * determined. + */ + fun calculateSizeBytes(app: ApplicationInfo): Long? +} + +class AppStorageRepositoryImpl(context: Context) : AppStorageRepository { + private val storageStatsManager = context.storageStatsManager + private val bytesFormatter = BytesFormatter(context) + + override fun formatSize(app: ApplicationInfo): String { + val sizeBytes = calculateSizeBytes(app) + return if (sizeBytes != null) formatSizeBytes(sizeBytes) else "" + } + + override fun formatSizeBytes(sizeBytes: Long): String = + bytesFormatter.format(sizeBytes, BytesFormatter.UseCase.FileSize) + + override fun calculateSizeBytes(app: ApplicationInfo): Long? = + try { + val stats = + storageStatsManager.queryStatsForPackage( + app.storageUuid, + app.packageName, + app.userHandle, + ) + stats.codeBytes + stats.dataBytes + } catch (e: Exception) { + Log.w(TAG, "Failed to query stats", e) + null + } + + companion object { + private const val TAG = "AppStorageRepository" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt index 7a4f81cc1321..7c98e9cd813b 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppStorageSize.kt @@ -16,42 +16,30 @@ package com.android.settingslib.spaprivileged.template.app -import android.content.Context import android.content.pm.ApplicationInfo -import android.text.format.Formatter -import android.util.Log +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.android.settingslib.spaprivileged.framework.common.storageStatsManager +import com.android.settingslib.spa.framework.compose.rememberContext import com.android.settingslib.spaprivileged.framework.compose.placeholder -import com.android.settingslib.spaprivileged.model.app.userHandle +import com.android.settingslib.spaprivileged.model.app.AppStorageRepository +import com.android.settingslib.spaprivileged.model.app.AppStorageRepositoryImpl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -private const val TAG = "AppStorageSize" - @Composable -fun ApplicationInfo.getStorageSize(): State<String> { - val context = LocalContext.current - return remember(this) { - flow { - val sizeBytes = calculateSizeBytes(context) - this.emit(if (sizeBytes != null) Formatter.formatFileSize(context, sizeBytes) else "") - }.flowOn(Dispatchers.IO) - }.collectAsStateWithLifecycle(initialValue = placeholder()) -} +fun ApplicationInfo.getStorageSize(): State<String> = + getStorageSize(rememberContext(::AppStorageRepositoryImpl)) -fun ApplicationInfo.calculateSizeBytes(context: Context): Long? { - val storageStatsManager = context.storageStatsManager - return try { - val stats = storageStatsManager.queryStatsForPackage(storageUuid, packageName, userHandle) - stats.codeBytes + stats.dataBytes - } catch (e: Exception) { - Log.w(TAG, "Failed to query stats: $e") - null - } +@VisibleForTesting +@Composable +fun ApplicationInfo.getStorageSize(appStorageRepository: AppStorageRepository): State<String> { + val app = this + return remember(app) { + flow { emit(appStorageRepository.formatSize(app)) }.flowOn(Dispatchers.Default) + } + .collectAsStateWithLifecycle(initialValue = placeholder()) } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index b1baa8601f28..fd4b189c51ff 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -281,6 +281,23 @@ class AppListRepositoryTest { ) } + @EnableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) + @Test + fun loadApps_shouldIncludeAllSystemModuleApps() = runTest { + packageManager.stub { + on { getInstalledModules(any()) } doReturn listOf(HIDDEN_MODULE) + } + mockInstalledApplications( + listOf(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP), + ADMIN_USER_ID + ) + + val appList = repository.loadApps(userId = ADMIN_USER_ID) + + assertThat(appList).containsExactly(NORMAL_APP, HIDDEN_APEX_APP, HIDDEN_MODULE_APP) + } + + @DisableFlags(Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) @EnableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) @Test fun loadApps_hasApkInApexInfo_shouldNotIncludeAllHiddenApps() = runTest { @@ -297,7 +314,7 @@ class AppListRepositoryTest { assertThat(appList).containsExactly(NORMAL_APP) } - @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX) + @DisableFlags(Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX, Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE) @Test fun loadApps_noApkInApexInfo_shouldNotIncludeHiddenSystemModule() = runTest { packageManager.stub { @@ -456,6 +473,7 @@ class AppListRepositoryTest { isArchived = true } + // TODO(b/382016780): to be removed after flag cleanup. val HIDDEN_APEX_APP = ApplicationInfo().apply { packageName = "hidden.apex.package" } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt new file mode 100644 index 000000000000..e8ec974bb0b8 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppStorageRepositoryTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.app.usage.StorageStats +import android.app.usage.StorageStatsManager +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager.NameNotFoundException +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spaprivileged.framework.common.storageStatsManager +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class AppStorageRepositoryTest { + private val app = ApplicationInfo().apply { storageUuid = UUID.randomUUID() } + + private val mockStorageStatsManager = + mock<StorageStatsManager> { + on { queryStatsForPackage(app.storageUuid, app.packageName, app.userHandle) } doReturn + STATS + } + + private val context: Context = + spy(ApplicationProvider.getApplicationContext()) { + on { storageStatsManager } doReturn mockStorageStatsManager + } + + private val repository = AppStorageRepositoryImpl(context) + + @Test + fun calculateSizeBytes() { + val sizeBytes = repository.calculateSizeBytes(app) + + assertThat(sizeBytes).isEqualTo(120) + } + + @Test + fun formatSize() { + val fileSize = repository.formatSize(app) + + assertThat(fileSize).isEqualTo("120 byte") + } + + @Test + fun formatSize_throwException() { + mockStorageStatsManager.stub { + on { queryStatsForPackage(app.storageUuid, app.packageName, app.userHandle) } doThrow + NameNotFoundException() + } + + val fileSize = repository.formatSize(app) + + assertThat(fileSize).isEqualTo("") + } + + @Test + fun formatSizeBytes() { + val fileSize = repository.formatSizeBytes(120) + + assertThat(fileSize).isEqualTo("120 byte") + } + + companion object { + private val STATS = + StorageStats().apply { + codeBytes = 100 + dataBytes = 20 + } + } +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt index 60f3d0ce1be3..4f42c8254c39 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppStorageSizeTest.kt @@ -16,98 +16,37 @@ package com.android.settingslib.spaprivileged.template.app -import android.app.usage.StorageStats -import android.app.usage.StorageStatsManager -import android.content.Context import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager.NameNotFoundException -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.framework.compose.stateOf -import com.android.settingslib.spaprivileged.framework.common.storageStatsManager -import com.android.settingslib.spaprivileged.model.app.userHandle -import java.util.UUID -import org.junit.Before +import com.android.settingslib.spaprivileged.model.app.AppStorageRepository import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Spy -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.mockito.kotlin.whenever +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import java.util.UUID @RunWith(AndroidJUnit4::class) class AppStorageSizeTest { - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() - - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() - @Spy - private val context: Context = ApplicationProvider.getApplicationContext() - - @Mock - private lateinit var storageStatsManager: StorageStatsManager - - private val app = ApplicationInfo().apply { - storageUuid = UUID.randomUUID() - } + private val app = ApplicationInfo().apply { storageUuid = UUID.randomUUID() } - @Before - fun setUp() { - whenever(context.storageStatsManager).thenReturn(storageStatsManager) - whenever( - storageStatsManager.queryStatsForPackage( - app.storageUuid, - app.packageName, - app.userHandle, - ) - ).thenReturn(STATS) - } + private val mockAppStorageRepository = + mock<AppStorageRepository> { on { formatSize(app) } doReturn SIZE } @Test fun getStorageSize() { var storageSize = stateOf("") - composeTestRule.setContent { - CompositionLocalProvider(LocalContext provides context) { - storageSize = app.getStorageSize() - } - } - - composeTestRule.waitUntil { storageSize.value == "120 B" } - } - - @Test - fun getStorageSize_throwException() { - var storageSize = stateOf("Computing") - whenever( - storageStatsManager.queryStatsForPackage( - app.storageUuid, - app.packageName, - app.userHandle, - ) - ).thenThrow(NameNotFoundException()) - - composeTestRule.setContent { - CompositionLocalProvider(LocalContext provides context) { - storageSize = app.getStorageSize() - } - } + composeTestRule.setContent { storageSize = app.getStorageSize(mockAppStorageRepository) } - composeTestRule.waitUntil { storageSize.value == "" } + composeTestRule.waitUntil { storageSize.value == SIZE } } - companion object { - private val STATS = StorageStats().apply { - codeBytes = 100 - dataBytes = 20 - cacheBytes = 3 - } + private companion object { + const val SIZE = "120 kB" } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index c4829951d61a..3390296ef6fc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -137,6 +137,7 @@ public class AppUtils { /** * Returns a boolean indicating whether the given package is a hidden system module + * TODO(b/382016780): to be removed after flag cleanup. */ public static boolean isHiddenSystemModule(Context context, String packageName) { return ApplicationsState.getInstance((Application) context.getApplicationContext()) diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index fd9a008ee078..4110d536da61 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -157,6 +157,7 @@ public class ApplicationsState { int mCurComputingSizeUserId; boolean mSessionsChanged; // Maps all installed modules on the system to whether they're hidden or not. + // TODO(b/382016780): to be removed after flag cleanup. final HashMap<String, Boolean> mSystemModules = new HashMap<>(); // Temporary for dispatching session callbacks. Only touched by main thread. @@ -226,12 +227,14 @@ public class ApplicationsState { mRetrieveFlags = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; - final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */); - for (ModuleInfo info : moduleInfos) { - mSystemModules.put(info.getPackageName(), info.isHidden()); - if (Flags.provideInfoOfApkInApex()) { - for (String apkInApexPackageName : info.getApkInApexPackageNames()) { - mSystemModules.put(apkInApexPackageName, info.isHidden()); + if (!Flags.removeHiddenModuleUsage()) { + final List<ModuleInfo> moduleInfos = mPm.getInstalledModules(0 /* flags */); + for (ModuleInfo info : moduleInfos) { + mSystemModules.put(info.getPackageName(), info.isHidden()); + if (Flags.provideInfoOfApkInApex()) { + for (String apkInApexPackageName : info.getApkInApexPackageNames()) { + mSystemModules.put(apkInApexPackageName, info.isHidden()); + } } } } @@ -336,7 +339,7 @@ public class ApplicationsState { } mHaveDisabledApps = true; } - if (isHiddenModule(info.packageName)) { + if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) { mApplications.remove(i--); continue; } @@ -453,6 +456,7 @@ public class ApplicationsState { return mHaveInstantApps; } + // TODO(b/382016780): to be removed after flag cleanup. boolean isHiddenModule(String packageName) { Boolean isHidden = mSystemModules.get(packageName); if (isHidden == null) { @@ -462,6 +466,7 @@ public class ApplicationsState { return isHidden; } + // TODO(b/382016780): to be removed after flag cleanup. boolean isSystemModule(String packageName) { return mSystemModules.containsKey(packageName); } @@ -755,7 +760,7 @@ public class ApplicationsState { Log.i(TAG, "Looking up entry of pkg " + info.packageName + ": " + entry); } if (entry == null) { - if (isHiddenModule(info.packageName)) { + if (!Flags.removeHiddenModuleUsage() && isHiddenModule(info.packageName)) { if (DEBUG) { Log.i(TAG, "No AppEntry for " + info.packageName + " (hidden module)"); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index b2c279466ee4..e05f0a1bcde0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -483,14 +483,18 @@ public class HearingAidDeviceManager { void onActiveDeviceChanged(CachedBluetoothDevice device) { if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) { - if (device.isConnectedHearingAidDevice()) { + if (device.isConnectedHearingAidDevice() + && (device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { setAudioRoutingConfig(device); } else { clearAudioRoutingConfig(); } } if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { - if (device.isConnectedHearingAidDevice()) { + if (device.isConnectedHearingAidDevice() + && (device.isActiveDevice(BluetoothProfile.HEARING_AID) + || device.isActiveDevice(BluetoothProfile.LE_AUDIO))) { setMicrophoneForCalls(device); } else { clearMicrophoneForCalls(); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 6be4336178eb..155c7e6530aa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -21,6 +21,7 @@ import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; +import android.annotation.CallbackExecutor; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; @@ -39,6 +40,7 @@ import com.android.settingslib.R; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executor; public class LeAudioProfile implements LocalBluetoothProfile { private static final String TAG = "LeAudioProfile"; @@ -317,6 +319,78 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mService.getAudioLocation(device); } + /** + * Sets the fallback group id when broadcast switches to unicast. + * + * @param groupId the target fallback group id + */ + public void setBroadcastToUnicastFallbackGroup(int groupId) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot set fallback group: " + groupId); + return; + } + + mService.setBroadcastToUnicastFallbackGroup(groupId); + } + + /** + * Gets the fallback group id when broadcast switches to unicast. + * + * @return current fallback group id + */ + public int getBroadcastToUnicastFallbackGroup() { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot get fallback group."); + return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + return mService.getBroadcastToUnicastFallbackGroup(); + } + + /** + * Registers a {@link BluetoothLeAudio.Callback} that will be invoked during the + * operation of this profile. + * + * Repeated registration of the same <var>callback</var> object after the first call to this + * method will result with IllegalArgumentException being thrown, even when the + * <var>executor</var> is different. API caller would have to call + * {@link #unregisterCallback(BluetoothLeAudio.Callback)} with the same callback object + * before registering it again. + * + * @param executor an {@link Executor} to execute given callback + * @param callback user implementation of the {@link BluetoothLeAudio.Callback} + * @throws NullPointerException if a null executor, or callback is given, or + * IllegalArgumentException if the same <var>callback</var> is + * already registered. + */ + public void registerCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull BluetoothLeAudio.Callback callback) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot register callback."); + return; + } + mService.registerCallback(executor, callback); + } + + /** + * Unregisters the specified {@link BluetoothLeAudio.Callback}. + * <p>The same {@link BluetoothLeAudio.Callback} object used when calling + * {@link #registerCallback(Executor, BluetoothLeAudio.Callback)} must be used. + * + * <p>Callbacks are automatically unregistered when application process goes away + * + * @param callback user implementation of the {@link BluetoothLeAudio.Callback} + * @throws NullPointerException when callback is null or IllegalArgumentException when no + * callback is registered + */ + public void unregisterCallback(@NonNull BluetoothLeAudio.Callback callback) { + if (mService == null) { + Log.w(TAG, "Proxy not attached to service. Cannot unregister callback."); + return; + } + mService.unregisterCallback(callback); + } + @RequiresApi(Build.VERSION_CODES.S) protected void finalize() { if (DEBUG) { diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index dc40304ba24a..51259e2f311d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -16,6 +16,8 @@ package com.android.settingslib.dream; +import static android.service.dreams.Flags.allowDreamWhenPostured; + import android.annotation.IntDef; import android.content.ComponentName; import android.content.Context; @@ -78,14 +80,21 @@ public class DreamBackend { } @Retention(RetentionPolicy.SOURCE) - @IntDef({WHILE_CHARGING, WHILE_DOCKED, EITHER, NEVER}) + @IntDef({ + WHILE_CHARGING, + WHILE_DOCKED, + WHILE_POSTURED, + WHILE_CHARGING_OR_DOCKED, + NEVER + }) public @interface WhenToDream { } public static final int WHILE_CHARGING = 0; public static final int WHILE_DOCKED = 1; - public static final int EITHER = 2; - public static final int NEVER = 3; + public static final int WHILE_POSTURED = 2; + public static final int WHILE_CHARGING_OR_DOCKED = 3; + public static final int NEVER = 4; /** * The type of dream complications which can be provided by a @@ -134,6 +143,8 @@ public class DreamBackend { .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_CHARGING_ONLY; private static final int WHEN_TO_DREAM_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_DOCKED_ONLY; + private static final int WHEN_TO_DREAM_POSTURED = FrameworkStatsLog + .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_WHILE_POSTURED_ONLY; private static final int WHEN_TO_DREAM_CHARGING_OR_DOCKED = FrameworkStatsLog .DREAM_SETTING_CHANGED__WHEN_TO_DREAM__WHEN_TO_DREAM_EITHER_CHARGING_OR_DOCKED; @@ -143,6 +154,7 @@ public class DreamBackend { private final boolean mDreamsEnabledByDefault; private final boolean mDreamsActivatedOnSleepByDefault; private final boolean mDreamsActivatedOnDockByDefault; + private final boolean mDreamsActivatedOnPosturedByDefault; private final Set<ComponentName> mDisabledDreams; private final List<String> mLoggableDreamPrefixes; private Set<Integer> mSupportedComplications; @@ -168,6 +180,8 @@ public class DreamBackend { com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault); mDreamsActivatedOnDockByDefault = resources.getBoolean( com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault); + mDreamsActivatedOnPosturedByDefault = resources.getBoolean( + com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault); mDisabledDreams = Arrays.stream(resources.getStringArray( com.android.internal.R.array.config_disabledDreamComponents)) .map(ComponentName::unflattenFromString) @@ -280,10 +294,11 @@ public class DreamBackend { @WhenToDream public int getWhenToDreamSetting() { - return isActivatedOnDock() && isActivatedOnSleep() ? EITHER + return isActivatedOnDock() && isActivatedOnSleep() ? WHILE_CHARGING_OR_DOCKED : isActivatedOnDock() ? WHILE_DOCKED - : isActivatedOnSleep() ? WHILE_CHARGING - : NEVER; + : isActivatedOnPostured() ? WHILE_POSTURED + : isActivatedOnSleep() ? WHILE_CHARGING + : NEVER; } public void setWhenToDream(@WhenToDream int whenToDream) { @@ -293,16 +308,25 @@ public class DreamBackend { case WHILE_CHARGING: setActivatedOnDock(false); setActivatedOnSleep(true); + setActivatedOnPostured(false); break; case WHILE_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(false); + setActivatedOnPostured(false); break; - case EITHER: + case WHILE_CHARGING_OR_DOCKED: setActivatedOnDock(true); setActivatedOnSleep(true); + setActivatedOnPostured(false); + break; + + case WHILE_POSTURED: + setActivatedOnPostured(true); + setActivatedOnSleep(false); + setActivatedOnDock(false); break; case NEVER: @@ -407,6 +431,22 @@ public class DreamBackend { setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, value); } + public boolean isActivatedOnPostured() { + return allowDreamWhenPostured() + && getBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + mDreamsActivatedOnPosturedByDefault); + } + + /** + * Sets whether dreams should be activated when the device is postured (stationary and upright) + */ + public void setActivatedOnPostured(boolean value) { + if (allowDreamWhenPostured()) { + logd("setActivatedOnPostured(%s)", value); + setBoolean(Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, value); + } + } + private boolean getBoolean(String key, boolean def) { return Settings.Secure.getInt(mContext.getContentResolver(), key, def ? 1 : 0) == 1; } @@ -548,7 +588,9 @@ public class DreamBackend { return WHEN_TO_DREAM_CHARGING; case WHILE_DOCKED: return WHEN_TO_DREAM_DOCKED; - case EITHER: + case WHILE_POSTURED: + return WHEN_TO_DREAM_POSTURED; + case WHILE_CHARGING_OR_DOCKED: return WHEN_TO_DREAM_CHARGING_OR_DOCKED; case NEVER: default: diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java index 7516d2e6ab1b..e3d7902f34b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledByAdminControllerFactory.java @@ -22,6 +22,7 @@ import static com.android.settingslib.enterprise.ActionDisabledLearnMoreButtonLa import static com.android.settingslib.enterprise.ManagedDeviceActionDisabledByAdminController.DEFAULT_FOREGROUND_USER_CHECKER; import android.app.admin.DevicePolicyManager; +import android.app.supervision.SupervisionManager; import android.content.ComponentName; import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; @@ -59,12 +60,18 @@ public final class ActionDisabledByAdminControllerFactory { } private static boolean isSupervisedDevice(Context context) { - DevicePolicyManager devicePolicyManager = - context.getSystemService(DevicePolicyManager.class); - ComponentName supervisionComponent = - devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( - new UserHandle(UserHandle.myUserId())); - return supervisionComponent != null; + if (android.app.supervision.flags.Flags.deprecateDpmSupervisionApis()) { + SupervisionManager supervisionManager = + context.getSystemService(SupervisionManager.class); + return supervisionManager.isSupervisionEnabledForUser(UserHandle.myUserId()); + } else { + DevicePolicyManager devicePolicyManager = + context.getSystemService(DevicePolicyManager.class); + ComponentName supervisionComponent = + devicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent( + new UserHandle(UserHandle.myUserId())); + return supervisionComponent != null; + } } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 496c3e6c74cc..9aaefe47fda2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -37,7 +37,8 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = + MutableStateFlow(listOf(TestModeBuilder.MANUAL_DND)) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -65,8 +66,11 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value += mode } - fun addMode(id: String, @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, - active: Boolean = false) { + fun addMode( + id: String, + @AutomaticZenRule.Type type: Int = AutomaticZenRule.TYPE_UNKNOWN, + active: Boolean = false, + ) { mutableModesFlow.value += newMode(id, type, active) } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index abc163867248..64a2de5025de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -31,7 +31,6 @@ import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.Random; @@ -44,22 +43,7 @@ public class TestModeBuilder { private boolean mIsManualDnd; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); - - public static final ZenMode MANUAL_DND_ACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, true); - - public static final ZenMode MANUAL_DND_INACTIVE = manualDnd( - INTERRUPTION_FILTER_PRIORITY, false); - - @NonNull - public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter, - boolean isActive) { - return new TestModeBuilder() - .makeManualDnd() - .setInterruptionFilter(filter) - .setActive(isActive) - .build(); - } + public static final ZenMode MANUAL_DND = new TestModeBuilder().makeManualDnd().build(); public TestModeBuilder() { // Reasonable defaults 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 3b18aa310c91..4e821ca50dce 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 @@ -16,6 +16,7 @@ package com.android.settingslib.applications; +import static android.content.pm.Flags.FLAG_REMOVE_HIDDEN_MODULE_USAGE; import static android.content.pm.Flags.FLAG_PROVIDE_INFO_OF_APK_IN_APEX; import static android.os.UserHandle.MU_ENABLED; import static android.os.UserHandle.USER_SYSTEM; @@ -59,6 +60,8 @@ import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; import android.util.IconDrawableFactory; @@ -204,6 +207,7 @@ public class ApplicationsStateRoboTest { info.setPackageName(packageName); info.setApkInApexPackageNames(Collections.singletonList(apexPackageName)); // will treat any app with package name that contains "hidden" as hidden module + // TODO(b/382016780): to be removed after flag cleanup. info.setHidden(!TextUtils.isEmpty(packageName) && packageName.contains("hidden")); return info; } @@ -414,6 +418,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void onResume_shouldNotIncludeSystemHiddenModule() { mSession.onResume(); @@ -424,6 +429,18 @@ public class ApplicationsStateRoboTest { } @Test + @EnableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) + public void onResume_shouldIncludeSystemModule() { + mSession.onResume(); + + final List<ApplicationInfo> mApplications = mApplicationsState.mApplications; + assertThat(mApplications).hasSize(3); + assertThat(mApplications.get(0).packageName).isEqualTo("test.package.1"); + assertThat(mApplications.get(1).packageName).isEqualTo("test.hidden.module.2"); + assertThat(mApplications.get(2).packageName).isEqualTo("test.package.3"); + } + + @Test public void removeAndInstall_noWorkprofile_doResumeIfNeededLocked_shouldClearEntries() throws RemoteException { // scenario: only owner user @@ -832,6 +849,7 @@ public class ApplicationsStateRoboTest { mApplicationsState.mEntriesMap.clear(); ApplicationInfo appInfo = createApplicationInfo(PKG_1, /* uid= */ 0); mApplicationsState.mApplications.add(appInfo); + // TODO(b/382016780): to be removed after flag cleanup. mApplicationsState.mSystemModules.put(PKG_1, /* value= */ false); assertThat(mApplicationsState.getEntry(PKG_1, /* userId= */ 0).info.packageName) @@ -839,6 +857,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void isHiddenModule_hasApkInApexInfo_shouldSupportHiddenApexPackage() { mSetFlagsRule.enableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX); ApplicationsState.sInstance = null; @@ -853,6 +872,7 @@ public class ApplicationsStateRoboTest { } @Test + @DisableFlags({FLAG_REMOVE_HIDDEN_MODULE_USAGE}) public void isHiddenModule_noApkInApexInfo_onlySupportHiddenModule() { mSetFlagsRule.disableFlags(FLAG_PROVIDE_INFO_OF_APK_IN_APEX); ApplicationsState.sInstance = null; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index 21dde1fd9411..a215464f66c2 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -50,6 +50,9 @@ import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.audiopolicy.AudioProductStrategy; import android.os.Parcel; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.FeatureFlagUtils; import androidx.test.core.app.ApplicationProvider; @@ -72,6 +75,8 @@ import java.util.List; public class HearingAidDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final long HISYNCID1 = 10; private static final long HISYNCID2 = 11; @@ -736,6 +741,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_connected_callSetStrategies() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); @@ -750,6 +756,7 @@ public class HearingAidDeviceManagerTest { @Test public void onActiveDeviceChanged_disconnected_callSetStrategiesWithAutoValue() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(false); when(mHelper.getMatchedHearingDeviceAttributesForOutput(mCachedDevice1)).thenReturn( mHearingDeviceAttribute); when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); @@ -952,6 +959,38 @@ public class HearingAidDeviceManagerTest { ConnectionStatus.CONNECTED); } + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void onActiveDeviceChanged_activeHearingAidProfile_callSetInputDeviceForCalls() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); + when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true); + doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), + anyInt()); + + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); + + verify(mHelper).setPreferredInputDeviceForCalls( + eq(mCachedDevice1), eq(HearingAidAudioRoutingConstants.RoutingValue.AUTO)); + + } + + @Test + @RequiresFlagsEnabled( + com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_INPUT_ROUTING_CONTROL) + public void onActiveDeviceChanged_notActiveHearingAidProfile_callClearInputDeviceForCalls() { + when(mCachedDevice1.isConnectedHearingAidDevice()).thenReturn(true); + when(mCachedDevice1.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(false); + when(mDevice1.isMicrophonePreferredForCalls()).thenReturn(true); + doReturn(true).when(mHelper).setPreferredDeviceRoutingStrategies(anyList(), any(), + anyInt()); + + mHearingAidDeviceManager.onActiveDeviceChanged(mCachedDevice1); + + verify(mHelper).clearPreferredInputDeviceForCalls(); + } + private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) { return new HearingAidInfo.Builder() .setAshaDeviceSide(HearingAidInfo.DeviceSide.SIDE_LEFT) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index d08d91d18b27..6b30f159129e 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -87,7 +87,7 @@ public class ZenModeTest { @Test public void testBasicMethods_manualDnd() { - ZenMode manualMode = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualMode = TestModeBuilder.MANUAL_DND; assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID); assertThat(manualMode.isManualDnd()).isTrue(); @@ -271,7 +271,7 @@ public class ZenModeTest { @Test public void setInterruptionFilter_manualDnd_throws() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; assertThrows(IllegalStateException.class, () -> manualDnd.setInterruptionFilter(INTERRUPTION_FILTER_ALL)); @@ -280,24 +280,46 @@ public class ZenModeTest { @Test public void canEditPolicy_onlyFalseForSpecialDnd() { assertThat(TestModeBuilder.EXAMPLE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_ACTIVE.canEditPolicy()).isTrue(); - assertThat(TestModeBuilder.MANUAL_DND_INACTIVE.canEditPolicy()).isTrue(); - ZenMode dndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode inactiveDnd = new TestModeBuilder().makeManualDnd().setActive(false).build(); + assertThat(inactiveDnd.canEditPolicy()).isTrue(); + + ZenMode activeDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); + assertThat(activeDnd.canEditPolicy()).isTrue(); + + ZenMode dndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(dndWithAlarms.canEditPolicy()).isFalse(); - ZenMode dndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, true); + + ZenMode dndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(true) + .build(); assertThat(dndWithNone.canEditPolicy()).isFalse(); // Note: Backend will never return an inactive manual mode with custom filter. - ZenMode badDndWithAlarms = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, false); + ZenMode badDndWithAlarms = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(false) + .build(); assertThat(badDndWithAlarms.canEditPolicy()).isFalse(); - ZenMode badDndWithNone = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_NONE, false); + + ZenMode badDndWithNone = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_NONE) + .setActive(false) + .build(); assertThat(badDndWithNone.canEditPolicy()).isFalse(); } @Test public void canEditPolicy_whenTrue_allowsSettingPolicyAndEffects() { - ZenMode normalDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_PRIORITY, true); + ZenMode normalDnd = new TestModeBuilder().makeManualDnd().setActive(true).build(); assertThat(normalDnd.canEditPolicy()).isTrue(); @@ -313,7 +335,11 @@ public class ZenModeTest { @Test public void canEditPolicy_whenFalse_preventsSettingFilterPolicyOrEffects() { - ZenMode specialDnd = TestModeBuilder.manualDnd(INTERRUPTION_FILTER_ALARMS, true); + ZenMode specialDnd = new TestModeBuilder() + .makeManualDnd() + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .setActive(true) + .build(); assertThat(specialDnd.canEditPolicy()).isFalse(); assertThrows(IllegalStateException.class, @@ -324,7 +350,7 @@ public class ZenModeTest { @Test public void comparator_prioritizes() { - ZenMode manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE; + ZenMode manualDnd = TestModeBuilder.MANUAL_DND; ZenMode driving1 = new TestModeBuilder().setName("b1").setType(TYPE_DRIVING).build(); ZenMode driving2 = new TestModeBuilder().setName("b2").setType(TYPE_DRIVING).build(); ZenMode bedtime1 = new TestModeBuilder().setName("c1").setType(TYPE_BEDTIME).build(); @@ -403,7 +429,7 @@ public class ZenModeTest { @Test public void getIconKey_manualDnd_isDndIcon() { - ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND_INACTIVE.getIconKey(); + ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey(); assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 4125a81f9bbc..fc61b1e875f3 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -46,6 +46,7 @@ public class GlobalSettings { Settings.Global.APP_AUTO_RESTRICTION_ENABLED, Settings.Global.AUTO_TIME, Settings.Global.AUTO_TIME_ZONE, + Settings.Global.TIME_ZONE_NOTIFICATIONS, Settings.Global.POWER_SOUNDS_ENABLED, Settings.Global.DOCK_SOUNDS_ENABLED, Settings.Global.CHARGING_SOUNDS_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index dd28402d705f..7b4a2ca5de39 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -155,6 +155,7 @@ public class SecureSettings { Settings.Secure.SCREENSAVER_COMPONENTS, Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, Settings.Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 32d4580f67ec..c0e266fa269f 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -102,6 +102,7 @@ public class GlobalSettingsValidators { }); VALIDATORS.put(Global.AUTO_TIME, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.AUTO_TIME_ZONE, BOOLEAN_VALIDATOR); + VALIDATORS.put(Global.TIME_ZONE_NOTIFICATIONS, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.POWER_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.DOCK_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Global.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index b01f6229af16..b0309a8fa5a5 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -230,6 +230,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.SCREENSAVER_COMPONENTS, COMMA_SEPARATED_COMPONENT_LIST_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_DOCK, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SCREENSAVER_HOME_CONTROLS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index ef0bc3b100e0..c1c3e04d46fd 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -212,10 +212,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final String ERROR_IO_EXCEPTION = "io_exception"; private static final String ERROR_FAILED_TO_RESTORE_SOFTAP_CONFIG = "failed_to_restore_softap_config"; - private static final String ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES = - "failed_to_convert_network_policies"; - private static final String ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION = - "unknown_backup_serialization_version"; + private static final String ERROR_FAILED_TO_RESTORE_WIFI_CONFIG = + "failed_to_restore_wifi_config"; // Name of the temporary file we use during full backup/restore. This is @@ -1438,7 +1436,6 @@ public class SettingsBackupAgent extends BackupAgentHelper { try { out.writeInt(NETWORK_POLICIES_BACKUP_VERSION); out.writeInt(policies.length); - int numberOfPoliciesBackedUp = 0; for (NetworkPolicy policy : policies) { // We purposefully only backup policies that the user has // defined; any inferred policies might include @@ -1448,30 +1445,26 @@ public class SettingsBackupAgent extends BackupAgentHelper { out.writeByte(BackupUtils.NOT_NULL); out.writeInt(marshaledPolicy.length); out.write(marshaledPolicy); - if (areAgentMetricsEnabled) { - numberOfPoliciesBackedUp++; - } } else { out.writeByte(BackupUtils.NULL); } } - if (areAgentMetricsEnabled) { - numberOfSettingsPerKey.put(KEY_NETWORK_POLICIES, numberOfPoliciesBackedUp); - } } catch (IOException ioe) { Log.e(TAG, "Failed to convert NetworkPolicies to byte array " + ioe.getMessage()); baos.reset(); - mBackupRestoreEventLogger.logItemsBackupFailed( - KEY_NETWORK_POLICIES, - policies.length, - ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES); } } return baos.toByteArray(); } - private byte[] getNewWifiConfigData() { - return mWifiManager.retrieveBackupData(); + @VisibleForTesting + byte[] getNewWifiConfigData() { + byte[] data = mWifiManager.retrieveBackupData(); + if (areAgentMetricsEnabled) { + // We're unable to determine how many settings this includes, so we'll just log 1. + numberOfSettingsPerKey.put(KEY_WIFI_NEW_CONFIG, 1); + } + return data; } private byte[] getLocaleSettings() { @@ -1483,11 +1476,22 @@ public class SettingsBackupAgent extends BackupAgentHelper { return localeList.toLanguageTags().getBytes(); } - private void restoreNewWifiConfigData(byte[] bytes) { + @VisibleForTesting + void restoreNewWifiConfigData(byte[] bytes) { if (DEBUG_BACKUP) { Log.v(TAG, "Applying restored wifi data"); } - mWifiManager.restoreBackupData(bytes); + if (areAgentMetricsEnabled) { + try { + mWifiManager.restoreBackupData(bytes); + mBackupRestoreEventLogger.logItemsRestored(KEY_WIFI_NEW_CONFIG, /* count= */ 1); + } catch (Exception e) { + mBackupRestoreEventLogger.logItemsRestoreFailed( + KEY_WIFI_NEW_CONFIG, /* count= */ 1, ERROR_FAILED_TO_RESTORE_WIFI_CONFIG); + } + } else { + mWifiManager.restoreBackupData(bytes); + } } private void restoreNetworkPolicies(byte[] data) { @@ -1498,10 +1502,6 @@ public class SettingsBackupAgent extends BackupAgentHelper { try { int version = in.readInt(); if (version < 1 || version > NETWORK_POLICIES_BACKUP_VERSION) { - mBackupRestoreEventLogger.logItemsRestoreFailed( - KEY_NETWORK_POLICIES, - /* count= */ 1, - ERROR_UNKNOWN_BACKUP_SERIALIZATION_VERSION); throw new BackupUtils.BadVersionException( "Unknown Backup Serialization Version"); } @@ -1518,15 +1518,10 @@ public class SettingsBackupAgent extends BackupAgentHelper { } // Only set the policies if there was no error in the restore operation networkPolicyManager.setNetworkPolicies(policies); - mBackupRestoreEventLogger.logItemsRestored(KEY_NETWORK_POLICIES, policies.length); } catch (NullPointerException | IOException | BackupUtils.BadVersionException | DateTimeException e) { // NPE can be thrown when trying to instantiate a NetworkPolicy Log.e(TAG, "Failed to convert byte array to NetworkPolicies " + e.getMessage()); - mBackupRestoreEventLogger.logItemsRestoreFailed( - KEY_NETWORK_POLICIES, - /* count= */ 1, - ERROR_FAILED_TO_CONVERT_NETWORK_POLICIES); } } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 5ad4b8a6dffe..1c6d6816e9b4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -2550,6 +2550,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.Secure.SCREENSAVER_DEFAULT_COMPONENT, SecureSettingsProto.Screensaver.DEFAULT_COMPONENT); + dumpSetting(s, p, + Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED, + SecureSettingsProto.Screensaver.ACTIVATE_ON_POSTURED); p.end(screensaverToken); final long searchToken = p.start(SecureSettingsProto.SEARCH); diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java index 95dd0db40c0e..6e5b602c02c5 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupAgentTest.java @@ -16,6 +16,7 @@ package com.android.providers.settings; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG; import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG; import static junit.framework.Assert.assertEquals; @@ -28,6 +29,8 @@ import static org.junit.Assert.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import android.annotation.Nullable; @@ -69,6 +72,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -834,6 +838,74 @@ public class SettingsBackupAgentTest extends BaseSettingsProviderTest { assertNull(getLoggingResultForDatatype(KEY_SOFTAP_CONFIG, mAgentUnderTest)); } + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsEnabled_numberOfSettingsInKeyAreRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void getNewWifiConfigData_flagIsNotEnabled_numberOfSettingsInKeyAreNotRecorded() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.BACKUP); + when(mWifiManager.retrieveBackupData()).thenReturn(null); + + mAgentUnderTest.getNewWifiConfigData(); + + assertEquals(mAgentUnderTest.getNumberOfSettingsPerKey(KEY_WIFI_NEW_CONFIG), 0); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsSuccessful_successMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getSuccessCount(), 1); + } + + @Test + @EnableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void + restoreNewWifiConfigData_flagIsEnabled_restoreIsNotSuccessful_failureMetricsAreLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doThrow(new RuntimeException()).when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + DataTypeResult loggingResult = + getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest); + assertNotNull(loggingResult); + assertEquals(loggingResult.getFailCount(), 1); + } + + @Test + @DisableFlags(com.android.server.backup.Flags.FLAG_ENABLE_METRICS_SETTINGS_BACKUP_AGENTS) + public void restoreNewWifiConfigData_flagIsNotEnabled_metricsAreNotLogged() { + mAgentUnderTest.onCreate( + UserHandle.SYSTEM, BackupDestination.CLOUD, OperationType.RESTORE); + doNothing().when(mWifiManager).restoreBackupData(any()); + + mAgentUnderTest.restoreNewWifiConfigData(new byte[] {}); + + assertNull(getLoggingResultForDatatype(KEY_WIFI_NEW_CONFIG, mAgentUnderTest)); + } + private byte[] generateBackupData(Map<String, String> keyValueData) { int totalBytes = 0; for (String key : keyValueData.keySet()) { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 46bd88fcdc93..4448000324d8 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -169,6 +169,7 @@ <!-- Internal permissions granted to the shell. --> <uses-permission android:name="android.permission.FORCE_BACK" /> <uses-permission android:name="android.permission.BATTERY_STATS" /> + <uses-permission android:name="android.permission.ACCESS_FINE_POWER_MONITORS" /> <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="android.permission.REPORT_USAGE_STATS" /> <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" /> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 9adc95a01216..c3c5e874b907 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -92,7 +92,6 @@ filegroup { "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt", "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt", - "tests/src/**/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt", "tests/src/**/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt", "tests/src/**/systemui/animation/back/FlingOnBackAnimationCallbackTest.kt", "tests/src/**/systemui/education/domain/ui/view/ContextualEduDialogTest.kt", @@ -520,6 +519,7 @@ android_library { "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", "androidx.lifecycle_lifecycle-viewmodel-compose", + "kairos", ], libs: [ "keepanno-annotations", @@ -740,6 +740,7 @@ android_library { "PlatformMotionTesting", "SystemUICustomizationTestUtils", "androidx.compose.runtime_runtime", + "kairos", "kosmos", "testables", "androidx.test.rules", diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index b5eba08c8f87..72b3d08396c6 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -125,3 +125,13 @@ flag { description: "Update hearing device icon in floating menu according to the connection status." bug: "357882387" } + +flag { + name: "floating_menu_notify_targets_changed_on_strict_diff" + namespace: "accessibility" + description: "Only notify listeners that the list of accessibility targets has changed if the lists are not identical." + bug: "376473165" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7d5fd903c01b..153f89284587 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -84,6 +84,16 @@ flag { } flag { + name: "notification_ambient_suppression_after_inflation" + namespace: "systemui" + description: "Move the DND visual effects filter to the finalize stage of the pipeline when it is doze-dependent, but keep it in the pre-group stage when it is doze-independent." + bug: "373411431" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_over_expansion_clipping_fix" namespace: "systemui" description: "Fix NSSL clipping when over-expanding; fixes split shade bug." @@ -372,6 +382,13 @@ flag { } flag { + name: "status_bar_mobile_icon_kairos" + namespace: "systemui" + description: "Refactors the mobile connection icon in the status bar to use the Kairos library" + bug: "383172066" +} + +flag { name: "status_bar_monochrome_icons_fix" namespace: "systemui" description: "Fixes the status bar icon size when drawing InsetDrawables (ie. monochrome icons)" @@ -382,14 +399,6 @@ flag { } flag { - name: "status_bar_screen_sharing_chips" - namespace: "systemui" - description: "Show chips on the left side of the status bar when a user is screen sharing, " - "recording, or casting" - bug: "332662551" -} - -flag { name: "status_bar_show_audio_only_projection_chip" namespace: "systemui" description: "Show chip on the left side of the status bar when a user is only sharing *audio* " @@ -424,24 +433,6 @@ flag { } flag { - name: "status_bar_use_repos_for_call_chip" - namespace: "systemui" - description: "Use repositories as the source of truth for call notifications shown as a chip in" - "the status bar" - bug: "328584859" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { - name: "status_bar_call_chip_notification_icon" - namespace: "systemui" - description: "Use the small icon set on the notification for the status bar call chip" - bug: "354930838" -} - -flag { name: "status_bar_signal_policy_refactor" namespace: "systemui" description: "Use a settings observer for airplane mode and make StatusBarSignalPolicy startable" @@ -1344,6 +1335,13 @@ flag { } flag { + name: "output_switcher_redesign" + namespace: "systemui" + description: "Enables visual update for Media Output Switcher" + bug: "388296370" +} + +flag { namespace: "systemui" name: "enable_view_capture_tracing" description: "Enables view capture tracing in System UI." @@ -1806,16 +1804,6 @@ flag { } flag { - name: "disable_shade_expands_on_trackpad_two_finger_swipe" - namespace: "systemui" - description: "Disables expansion of the shade via two finger swipe on a trackpad" - bug: "356804470" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "keyboard_shortcut_helper_shortcut_customizer" namespace: "systemui" description: "An implementation of shortcut customizations through shortcut helper." @@ -1916,8 +1904,28 @@ flag { } flag { + name: "disable_shade_trackpad_two_finger_swipe" + namespace: "systemui" + description: "Disables expansion of the shade via two finger swipe on a trackpad" + bug: "356804470" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_magic_actions_treatment" namespace: "systemui" description: "Special UI treatment for magic actions" bug: "383567383" } + +flag { + name: "show_audio_sharing_slider_in_volume_panel" + namespace: "cross_device_experiences" + description: "Show two sliders in volume panel when audio sharing." + bug: "336183611" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt index b0c7ac09551a..c8d3430bf54b 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -232,19 +232,21 @@ constructor( private val lifecycleListener = object : Listener { override fun onTransitionAnimationStart() { - listeners.forEach { it.onTransitionAnimationStart() } + LinkedHashSet(listeners).forEach { it.onTransitionAnimationStart() } } override fun onTransitionAnimationEnd() { - listeners.forEach { it.onTransitionAnimationEnd() } + LinkedHashSet(listeners).forEach { it.onTransitionAnimationEnd() } } override fun onTransitionAnimationProgress(linearProgress: Float) { - listeners.forEach { it.onTransitionAnimationProgress(linearProgress) } + LinkedHashSet(listeners).forEach { + it.onTransitionAnimationProgress(linearProgress) + } } override fun onTransitionAnimationCancelled() { - listeners.forEach { it.onTransitionAnimationCancelled() } + LinkedHashSet(listeners).forEach { it.onTransitionAnimationCancelled() } } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt index 5f1f588bb2b5..a27bf8af1806 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope @@ -146,21 +147,66 @@ private data class NestedDraggableElement( private val orientation: Orientation, private val overscrollEffect: OverscrollEffect?, private val enabled: Boolean, -) : ModifierNodeElement<NestedDraggableNode>() { - override fun create(): NestedDraggableNode { - return NestedDraggableNode(draggable, orientation, overscrollEffect, enabled) +) : ModifierNodeElement<NestedDraggableRootNode>() { + override fun create(): NestedDraggableRootNode { + return NestedDraggableRootNode(draggable, orientation, overscrollEffect, enabled) } - override fun update(node: NestedDraggableNode) { + override fun update(node: NestedDraggableRootNode) { node.update(draggable, orientation, overscrollEffect, enabled) } } +/** + * A root node on top of [NestedDraggableNode] so that no [PointerInputModifierNode] is installed + * when this draggable is disabled. + */ +private class NestedDraggableRootNode( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, +) : DelegatingNode() { + private var delegateNode = + if (enabled) create(draggable, orientation, overscrollEffect) else null + + fun update( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + enabled: Boolean, + ) { + // Disabled. + if (!enabled) { + delegateNode?.let { undelegate(it) } + delegateNode = null + return + } + + // Disabled => Enabled. + val nullableDelegate = delegateNode + if (nullableDelegate == null) { + delegateNode = create(draggable, orientation, overscrollEffect) + return + } + + // Enabled => Enabled (update). + nullableDelegate.update(draggable, orientation, overscrollEffect) + } + + private fun create( + draggable: NestedDraggable, + orientation: Orientation, + overscrollEffect: OverscrollEffect?, + ): NestedDraggableNode { + return delegate(NestedDraggableNode(draggable, orientation, overscrollEffect)) + } +} + private class NestedDraggableNode( private var draggable: NestedDraggable, override var orientation: Orientation, private var overscrollEffect: OverscrollEffect?, - private var enabled: Boolean, ) : DelegatingNode(), PointerInputModifierNode, @@ -168,17 +214,11 @@ private class NestedDraggableNode( CompositionLocalConsumerModifierNode, OrientationAware { private val nestedScrollDispatcher = NestedScrollDispatcher() - private var trackDownPositionDelegate: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } - - private var detectDragsDelegate: SuspendingPointerInputModifierNode? = null - set(value) { - field?.let { undelegate(it) } - field = value?.also { delegate(it) } - } + private val trackWheelScroll = + delegate(SuspendingPointerInputModifierNode { trackWheelScroll() }) + private val trackDownPositionDelegate = + delegate(SuspendingPointerInputModifierNode { trackDownPosition() }) + private val detectDragsDelegate = delegate(SuspendingPointerInputModifierNode { detectDrags() }) /** The controller created by the nested scroll logic (and *not* the drag logic). */ private var nestedScrollController: NestedScrollController? = null @@ -189,6 +229,7 @@ private class NestedDraggableNode( * This is use to track the started position of a drag started on a nested scrollable. */ private var lastFirstDown: Offset? = null + private var lastEventWasScrollWheel: Boolean = false /** The pointers currently down, in order of which they were done and mapping to their type. */ private val pointersDown = linkedMapOf<PointerId, PointerType>() @@ -206,23 +247,25 @@ private class NestedDraggableNode( draggable: NestedDraggable, orientation: Orientation, overscrollEffect: OverscrollEffect?, - enabled: Boolean, ) { + if ( + draggable == this.draggable && + orientation == this.orientation && + overscrollEffect == this.overscrollEffect + ) { + return + } + this.draggable = draggable this.orientation = orientation this.overscrollEffect = overscrollEffect - this.enabled = enabled - trackDownPositionDelegate?.resetPointerInputHandler() - detectDragsDelegate?.resetPointerInputHandler() + trackWheelScroll.resetPointerInputHandler() + trackDownPositionDelegate.resetPointerInputHandler() + detectDragsDelegate.resetPointerInputHandler() + nestedScrollController?.ensureOnDragStoppedIsCalled() nestedScrollController = null - - if (!enabled && trackDownPositionDelegate != null) { - check(detectDragsDelegate != null) - trackDownPositionDelegate = null - detectDragsDelegate = null - } } override fun onPointerEvent( @@ -230,21 +273,15 @@ private class NestedDraggableNode( pass: PointerEventPass, bounds: IntSize, ) { - if (!enabled) return - - if (trackDownPositionDelegate == null) { - check(detectDragsDelegate == null) - trackDownPositionDelegate = SuspendingPointerInputModifierNode { trackDownPosition() } - detectDragsDelegate = SuspendingPointerInputModifierNode { detectDrags() } - } - - checkNotNull(trackDownPositionDelegate).onPointerEvent(pointerEvent, pass, bounds) - checkNotNull(detectDragsDelegate).onPointerEvent(pointerEvent, pass, bounds) + trackWheelScroll.onPointerEvent(pointerEvent, pass, bounds) + trackDownPositionDelegate.onPointerEvent(pointerEvent, pass, bounds) + detectDragsDelegate.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { - trackDownPositionDelegate?.onCancelPointerInput() - detectDragsDelegate?.onCancelPointerInput() + trackWheelScroll.onCancelPointerInput() + trackDownPositionDelegate.onCancelPointerInput() + detectDragsDelegate.onCancelPointerInput() } /* @@ -407,7 +444,7 @@ private class NestedDraggableNode( val left = available - consumed val postConsumed = nestedScrollDispatcher.dispatchPostScroll( - consumed = preConsumed + consumed, + consumed = consumed, available = left, source = NestedScrollSource.UserInput, ) @@ -445,10 +482,9 @@ private class NestedDraggableNode( val available = velocity - preConsumed val consumed = performFling(available) val left = available - consumed - return nestedScrollDispatcher.dispatchPostFling( - consumed = consumed + preConsumed, - available = left, - ) + val postConsumed = + nestedScrollDispatcher.dispatchPostFling(consumed = consumed, available = left) + return preConsumed + consumed + postConsumed } /* @@ -457,6 +493,13 @@ private class NestedDraggableNode( * =============================== */ + private suspend fun PointerInputScope.trackWheelScroll() { + awaitEachGesture { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + lastEventWasScrollWheel = event.type == PointerEventType.Scroll + } + } + private suspend fun PointerInputScope.trackDownPosition() { awaitEachGesture { try { @@ -501,8 +544,14 @@ private class NestedDraggableNode( } val sign = offset.sign - if (nestedScrollController == null && draggable.shouldConsumeNestedScroll(sign)) { - val startedPosition = checkNotNull(lastFirstDown) { "lastFirstDown is not set" } + if ( + nestedScrollController == null && + // TODO(b/388231324): Remove this. + !lastEventWasScrollWheel && + draggable.shouldConsumeNestedScroll(sign) && + lastFirstDown != null + ) { + val startedPosition = checkNotNull(lastFirstDown) // TODO(b/382665591): Ensure that there is at least one pointer down. val pointersDownCount = pointersDown.size.coerceAtLeast(1) diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml index 28f80d4af265..7c721b97ee47 100644 --- a/packages/SystemUI/compose/core/tests/AndroidManifest.xml +++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml @@ -15,6 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="com.android.compose.core.tests" > <application> @@ -23,7 +24,8 @@ <activity android:name="androidx.activity.ComponentActivity" android:theme="@android:style/Theme.DeviceDefault.DayNight" - android:exported="true" /> + android:exported="true" + tools:replace="android:theme" /> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt index 7f70e97411f4..19d28cc2d626 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/NestedDraggableTest.kt @@ -18,30 +18,42 @@ package com.android.compose.gesture import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.ScrollWheel +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Velocity import com.google.common.truth.Truth.assertThat import kotlin.math.ceil @@ -690,6 +702,7 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw } @Test + @Ignore("b/388507816: re-enable this when the crash in HitPath is fixed") fun pointersDown_clearedWhenDisabled() { val draggable = TestDraggable() var enabled by mutableStateOf(true) @@ -710,6 +723,233 @@ class NestedDraggableTest(override val orientation: Orientation) : OrientationAw rule.onRoot().performTouchInput { down(center) } } + @Test + // TODO(b/388231324): Remove this. + fun nestedScrollWithMouseWheelIsIgnored() { + val draggable = TestDraggable() + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation) + .scrollable(rememberScrollableState { 0f }, orientation) + ) + } + + rule.onRoot().performMouseInput { + enter(center) + scroll( + touchSlop + 1f, + when (orientation) { + Orientation.Horizontal -> ScrollWheel.Horizontal + Orientation.Vertical -> ScrollWheel.Vertical + }, + ) + } + + assertThat(draggable.onDragStartedCalled).isFalse() + } + + @Test + fun doesNotConsumeGesturesWhenDisabled() { + val buttonTag = "button" + rule.setContent { + Box { + var count by remember { mutableStateOf(0) } + Button(onClick = { count++ }, Modifier.testTag(buttonTag).align(Alignment.Center)) { + Text("Count: $count") + } + + Box( + Modifier.fillMaxSize() + .nestedDraggable(remember { TestDraggable() }, orientation, enabled = false) + ) + } + } + + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 0") + + // Click on the root at its center, where the button is located. Clicks should go through + // the draggable and reach the button given that it is disabled. + repeat(3) { rule.onRoot().performClick() } + rule.onNodeWithTag(buttonTag).assertTextEquals("Count: 3") + } + + @Test + fun nestedDragNotStartedWhenEnabledAfterDragStarted() { + val draggable = TestDraggable() + var enabled by mutableStateOf(false) + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedDraggable(draggable, orientation, enabled = enabled) + .scrollable(rememberScrollableState { 0f }, orientation) + ) + } + + rule.onRoot().performTouchInput { down(center) } + + enabled = true + rule.waitForIdle() + + rule.onRoot().performTouchInput { moveBy((touchSlop + 1f).toOffset()) } + + assertThat(draggable.onDragStartedCalled).isFalse() + } + + @Test + fun availableAndConsumedScrollDeltas() { + val totalScroll = 200f + val consumedByEffectPreScroll = 10f // 200f => 190f + val consumedByConnectionPreScroll = 20f // 190f => 170f + val consumedByScroll = 30f // 170f => 140f + val consumedByConnectionPostScroll = 40f // 140f => 100f + + // Available scroll values that we will check later. + var availableToEffectPreScroll = 0f + var availableToConnectionPreScroll = 0f + var availableToScroll = 0f + var availableToConnectionPostScroll = 0f + var availableToEffectPostScroll = 0f + + val effect = + TestOverscrollEffect( + orientation, + onPreScroll = { + availableToEffectPreScroll = it + consumedByEffectPreScroll + }, + onPostScroll = { + availableToEffectPostScroll = it + it + }, + ) + + val connection = + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + availableToConnectionPreScroll = available.toFloat() + return consumedByConnectionPreScroll.toOffset() + } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + assertThat(consumed.toFloat()).isEqualTo(consumedByScroll) + availableToConnectionPostScroll = available.toFloat() + return consumedByConnectionPostScroll.toOffset() + } + } + + val draggable = + TestDraggable( + onDrag = { + availableToScroll = it + consumedByScroll + } + ) + + val touchSlop = + rule.setContentWithTouchSlop { + Box( + Modifier.fillMaxSize() + .nestedScroll(connection) + .nestedDraggable(draggable, orientation, effect) + ) + } + + rule.onRoot().performTouchInput { + down(center) + moveBy((touchSlop + totalScroll).toOffset()) + } + + assertThat(availableToEffectPreScroll).isEqualTo(200f) + assertThat(availableToConnectionPreScroll).isEqualTo(190f) + assertThat(availableToScroll).isEqualTo(170f) + assertThat(availableToConnectionPostScroll).isEqualTo(140f) + assertThat(availableToEffectPostScroll).isEqualTo(100f) + } + + @Test + fun availableAndConsumedVelocities() { + val totalVelocity = 200f + val consumedByEffectPreFling = 10f // 200f => 190f + val consumedByConnectionPreFling = 20f // 190f => 170f + val consumedByFling = 30f // 170f => 140f + val consumedByConnectionPostFling = 40f // 140f => 100f + + // Available velocities that we will check later. + var availableToEffectPreFling = 0f + var availableToConnectionPreFling = 0f + var availableToFling = 0f + var availableToConnectionPostFling = 0f + var availableToEffectPostFling = 0f + + val effect = + TestOverscrollEffect( + orientation, + onPreFling = { + availableToEffectPreFling = it + consumedByEffectPreFling + }, + onPostFling = { + availableToEffectPostFling = it + it + }, + onPostScroll = { 0f }, + ) + + val connection = + object : NestedScrollConnection { + override suspend fun onPreFling(available: Velocity): Velocity { + availableToConnectionPreFling = available.toFloat() + return consumedByConnectionPreFling.toVelocity() + } + + override suspend fun onPostFling( + consumed: Velocity, + available: Velocity, + ): Velocity { + assertThat(consumed.toFloat()).isEqualTo(consumedByFling) + availableToConnectionPostFling = available.toFloat() + return consumedByConnectionPostFling.toVelocity() + } + } + + val draggable = + TestDraggable( + onDragStopped = { velocity, _ -> + availableToFling = velocity + consumedByFling + }, + onDrag = { 0f }, + ) + + rule.setContent { + Box( + Modifier.fillMaxSize() + .nestedScroll(connection) + .nestedDraggable(draggable, orientation, effect) + ) + } + + rule.onRoot().performTouchInput { + when (orientation) { + Orientation.Horizontal -> swipeWithVelocity(topLeft, topRight, totalVelocity) + Orientation.Vertical -> swipeWithVelocity(topLeft, bottomLeft, totalVelocity) + } + } + + assertThat(availableToEffectPreFling).isWithin(1f).of(200f) + assertThat(availableToConnectionPreFling).isWithin(1f).of(190f) + assertThat(availableToFling).isWithin(1f).of(170f) + assertThat(availableToConnectionPostFling).isWithin(1f).of(140f) + assertThat(availableToEffectPostFling).isWithin(1f).of(100f) + } + private fun ComposeContentTestRule.setContentWithTouchSlop( content: @Composable () -> Unit ): Float { diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt index 8bf9c21639f4..0659f9198730 100644 --- a/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt +++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/gesture/TestOverscrollEffect.kt @@ -24,6 +24,8 @@ import androidx.compose.ui.unit.Velocity class TestOverscrollEffect( override val orientation: Orientation, + private val onPreScroll: (Float) -> Float = { 0f }, + private val onPreFling: suspend (Float) -> Float = { 0f }, private val onPostFling: suspend (Float) -> Float = { it }, private val onPostScroll: (Float) -> Float, ) : OverscrollEffect, OrientationAware { @@ -36,19 +38,23 @@ class TestOverscrollEffect( source: NestedScrollSource, performScroll: (Offset) -> Offset, ): Offset { - val consumedByScroll = performScroll(delta) - val available = delta - consumedByScroll - val consumedByEffect = onPostScroll(available.toFloat()).toOffset() - return consumedByScroll + consumedByEffect + val consumedByPreScroll = onPreScroll(delta.toFloat()).toOffset() + val availableToScroll = delta - consumedByPreScroll + val consumedByScroll = performScroll(availableToScroll) + val availableToPostScroll = availableToScroll - consumedByScroll + val consumedByPostScroll = onPostScroll(availableToPostScroll.toFloat()).toOffset() + return consumedByPreScroll + consumedByScroll + consumedByPostScroll } override suspend fun applyToFling( velocity: Velocity, performFling: suspend (Velocity) -> Velocity, ) { - val consumedByFling = performFling(velocity) - val available = velocity - consumedByFling - onPostFling(available.toFloat()) + val consumedByPreFling = onPreFling(velocity.toFloat()).toVelocity() + val availableToFling = velocity - consumedByPreFling + val consumedByFling = performFling(availableToFling) + val availableToPostFling = availableToFling - consumedByFling + onPostFling(availableToPostFling.toFloat()) applyToFlingDone = true } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 9c53afecad11..a2a91fcd5d52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -78,6 +78,18 @@ object TransitionDuration { const val EDIT_MODE_TO_HUB_GRID_END_MS = EDIT_MODE_TO_HUB_GRID_DELAY_MS + EDIT_MODE_TO_HUB_CONTENT_MS const val HUB_TO_EDIT_MODE_CONTENT_MS = 250 + const val TO_GLANCEABLE_HUB_DURATION_MS = 1000 +} + +val sceneTransitionsV2 = transitions { + to(CommunalScenes.Communal) { + spec = tween(durationMillis = TransitionDuration.TO_GLANCEABLE_HUB_DURATION_MS) + fade(AllElements) + } + to(CommunalScenes.Blank) { + spec = tween(durationMillis = TO_GONE_DURATION.toInt(DurationUnit.MILLISECONDS)) + fade(AllElements) + } } val sceneTransitions = transitions { @@ -157,7 +169,7 @@ fun CommunalContainer( MutableSceneTransitionLayoutState( initialScene = currentSceneKey, canChangeScene = { _ -> viewModel.canChangeScene() }, - transitions = sceneTransitions, + transitions = if (viewModel.v2FlagEnabled()) sceneTransitionsV2 else sceneTransitions, ) } val isUiBlurred by viewModel.isUiBlurred.collectAsStateWithLifecycle() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 6d24fc16df23..aa8b4ae9000d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -48,8 +48,8 @@ import com.android.systemui.shade.ui.composable.Shade val SceneContainerTransitions = transitions { interruptionHandler = SceneContainerInterruptionHandler - // Overscroll progress starts linearly with some resistance (3f) and slowly approaches 0.2f - defaultSwipeSpec = spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f) + defaultMotionSpatialSpec = + spring(stiffness = 300f, dampingRatio = 0.8f, visibilityThreshold = 0.5f) // Scene transitions diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt index 93c10b6224ab..6ca9c7934979 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.kt @@ -17,17 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal fun TransitionBuilder.dreamToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade in all communal hub elements. + fade(AllElements) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt index ce7a85b19fb4..e30e7d3ee34c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt @@ -30,7 +30,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt index 826a2550cca3..de9a78c497f8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToCommunalTransition.kt @@ -17,21 +17,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.systemui.communal.ui.compose.AllElements -import com.android.systemui.communal.ui.compose.Communal -import com.android.systemui.scene.shared.model.Scenes fun TransitionBuilder.lockscreenToCommunalTransition() { spec = tween(durationMillis = 1000) - // Translate lockscreen to the start direction. - translate(Scenes.Lockscreen.rootElementKey, Edge.Start) - - // Translate communal hub grid from the end direction. - translate(Communal.Elements.Grid, Edge.End) - - // Fade all communal hub elements. - timestampRange(startMillis = 167, endMillis = 334) { fade(AllElements) } + // Fade all communal hub elements in. + fade(AllElements) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt index 1f7a7380bbc6..1a243ca48157 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToSplitShadeTransition.kt @@ -29,7 +29,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt index 24f285e81da2..a9af95bdcb8a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromNotificationsShadeToQuickSettingsShadeTransition.kt @@ -27,7 +27,7 @@ fun TransitionBuilder.notificationsShadeToQuickSettingsShadeTransition( durationScale: Double = 1.0 ) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index 3d62151baf2f..ddea5854d67e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -31,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt index e78bc6afcc4f..e477a41ac608 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt @@ -28,7 +28,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt index bfae4897dc68..4db4934cf271 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt @@ -32,7 +32,7 @@ import kotlin.time.Duration.Companion.milliseconds fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 6bb579d18bf9..633328a836e3 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -16,67 +16,29 @@ package com.android.compose.animation.scene +import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState.Companion.DistanceUnspecified -import com.android.compose.nestedscroll.OnStopScope -import com.android.compose.nestedscroll.PriorityNestedScrollConnection -import com.android.compose.nestedscroll.ScrollController +import com.android.compose.animation.scene.effect.GestureEffect +import com.android.compose.gesture.NestedDraggable import com.android.compose.ui.util.SpaceVectorConverter import kotlin.math.absoluteValue -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -internal interface DraggableHandler { - /** - * Start a drag with the given [pointersDown] and [overSlop]. - * - * The returned [DragController] should be used to continue or stop the drag. - */ - fun onDragStarted(pointersDown: PointersInfo.PointersDown?, overSlop: Float): DragController -} - -/** - * The [DragController] provides control over the transition between two scenes through the [onDrag] - * and [onStop] methods. - */ -internal interface DragController { - /** - * Drag the current scene by [delta] pixels. - * - * @param delta The distance to drag the scene in pixels. - * @return the consumed [delta] - */ - fun onDrag(delta: Float): Float - - /** - * Stop the current drag with the given [velocity]. - * - * @param velocity The velocity of the drag when it stopped. - * @param canChangeContent Whether the content can be changed as a result of this drag. - * @return the consumed [velocity] when the animation complete - */ - suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float - - /** - * Cancels the current drag. - * - * @param canChangeContent Whether the content can be changed as a result of this drag. - */ - fun onCancel(canChangeContent: Boolean) -} - -internal class DraggableHandlerImpl( +internal class DraggableHandler( internal val layoutImpl: SceneTransitionLayoutImpl, internal val orientation: Orientation, -) : DraggableHandler { + private val gestureEffectProvider: (ContentKey) -> GestureEffect, +) : NestedDraggable { /** The [DraggableHandler] can only have one active [DragController] at a time. */ private var dragController: DragControllerImpl? = null @@ -97,20 +59,36 @@ internal class DraggableHandlerImpl( internal val positionalThreshold get() = with(layoutImpl.density) { 56.dp.toPx() } + /** The [OverscrollEffect] that should consume any overscroll on this draggable. */ + internal val overscrollEffect: OverscrollEffect = DelegatingOverscrollEffect() + + override fun shouldStartDrag(change: PointerInputChange): Boolean { + return layoutImpl.swipeDetector.detectSwipe(change) + } + + override fun shouldConsumeNestedScroll(sign: Float): Boolean { + return this.enabled() + } + override fun onDragStarted( - pointersDown: PointersInfo.PointersDown?, - overSlop: Float, - ): DragController { - check(overSlop != 0f) - val swipes = computeSwipes(pointersDown) + position: Offset, + sign: Float, + pointersDown: Int, + pointerType: PointerType?, + ): NestedDraggable.Controller { + check(sign != 0f) + val swipes = computeSwipes(position, pointersDown, pointerType) val fromContent = layoutImpl.contentForUserActions() swipes.updateSwipesResults(fromContent) + val upOrLeft = swipes.upOrLeftResult + val downOrRight = swipes.downOrRightResult val result = - (if (overSlop < 0f) swipes.upOrLeftResult else swipes.downOrRightResult) - // As we were unable to locate a valid target scene, the initial SwipeAnimation - // cannot be defined. Consequently, a simple NoOp Controller will be returned. - ?: return NoOpDragController + when { + sign < 0 -> upOrLeft ?: downOrRight + sign >= 0f -> downOrRight ?: upOrLeft + else -> null + } ?: return NoOpDragController val swipeAnimation = createSwipeAnimation(swipes, result) return updateDragController(swipes, swipeAnimation) @@ -148,20 +126,109 @@ internal class DraggableHandlerImpl( ) } - private fun computeSwipes(pointersDown: PointersInfo.PointersDown?): Swipes { - val fromSource = pointersDown?.let { resolveSwipeSource(it.startedPosition) } + private fun computeSwipes( + position: Offset, + pointersDown: Int, + pointerType: PointerType?, + ): Swipes { + val fromSource = resolveSwipeSource(position) return Swipes( - upOrLeft = resolveSwipe(orientation, isUpOrLeft = true, pointersDown, fromSource), - downOrRight = resolveSwipe(orientation, isUpOrLeft = false, pointersDown, fromSource), + upOrLeft = + resolveSwipe(orientation, isUpOrLeft = true, fromSource, pointersDown, pointerType), + downOrRight = + resolveSwipe(orientation, isUpOrLeft = false, fromSource, pointersDown, pointerType), ) } + + /** + * An implementation of [OverscrollEffect] that delegates to the correct content effect + * depending on the current scene/overlays and transition. + */ + private inner class DelegatingOverscrollEffect : + OverscrollEffect, SpaceVectorConverter by SpaceVectorConverter(orientation) { + private var currentContent: ContentKey? = null + private var currentDelegate: GestureEffect? = null + set(value) { + field?.let { delegate -> + if (delegate.isInProgress) { + layoutImpl.animationScope.launch { delegate.ensureApplyToFlingIsCalled() } + } + } + + field = value + } + + override val isInProgress: Boolean + get() = currentDelegate?.isInProgress ?: false + + override fun applyToScroll( + delta: Offset, + source: NestedScrollSource, + performScroll: (Offset) -> Offset, + ): Offset { + val available = delta.toFloat() + if (available == 0f) { + return performScroll(delta) + } + + ensureDelegateIsNotNull(available) + val delegate = checkNotNull(currentDelegate) + return if (delegate.node.node.isAttached) { + delegate.applyToScroll(delta, source, performScroll) + } else { + performScroll(delta) + } + } + + override suspend fun applyToFling( + velocity: Velocity, + performFling: suspend (Velocity) -> Velocity, + ) { + val available = velocity.toFloat() + if (available != 0f && isDrivingTransition) { + ensureDelegateIsNotNull(available) + } + + // Note: we set currentDelegate and currentContent to null before calling performFling, + // which can suspend and take a lot of time. + val delegate = currentDelegate + currentDelegate = null + currentContent = null + + if (delegate != null && delegate.node.node.isAttached) { + delegate.applyToFling(velocity, performFling) + } else { + performFling(velocity) + } + } + + private fun ensureDelegateIsNotNull(direction: Float) { + require(direction != 0f) + if (isInProgress) { + return + } + + val content = + if (isDrivingTransition) { + checkNotNull(dragController).swipeAnimation.contentByDirection(direction) + } else { + layoutImpl.contentForUserActions().key + } + + if (content != currentContent) { + currentContent = content + currentDelegate = gestureEffectProvider(content) + } + } + } } private fun resolveSwipe( orientation: Orientation, isUpOrLeft: Boolean, - pointersDown: PointersInfo.PointersDown?, fromSource: SwipeSource.Resolved?, + pointersDown: Int, + pointerType: PointerType?, ): Swipe.Resolved { return Swipe.Resolved( direction = @@ -180,28 +247,22 @@ private fun resolveSwipe( SwipeDirection.Resolved.Down } }, - // If the number of pointers is not specified, 1 is assumed. - pointerCount = pointersDown?.count ?: 1, - // Resolves the pointer type only if all pointers are of the same type. - pointersType = pointersDown?.countByType?.keys?.singleOrNull(), + pointerCount = pointersDown, + pointerType = pointerType, fromSource = fromSource, ) } /** @param swipes The [Swipes] associated to the current gesture. */ private class DragControllerImpl( - private val draggableHandler: DraggableHandlerImpl, + private val draggableHandler: DraggableHandler, val swipes: Swipes, var swipeAnimation: SwipeAnimation<*>, -) : DragController, SpaceVectorConverter by SpaceVectorConverter(draggableHandler.orientation) { +) : + NestedDraggable.Controller, + SpaceVectorConverter by SpaceVectorConverter(draggableHandler.orientation) { val layoutState = draggableHandler.layoutImpl.state - val overscrollableContent: OverscrollableContent = - when (draggableHandler.orientation) { - Orientation.Vertical -> draggableHandler.layoutImpl.verticalOverscrollableContent - Orientation.Horizontal -> draggableHandler.layoutImpl.horizontalOverscrollableContent - } - /** * Whether this handle is active. If this returns false, calling [onDrag] and [onStop] will do * nothing. @@ -236,57 +297,25 @@ private class DragControllerImpl( if (delta == 0f || !isDrivingTransition || initialAnimation.isAnimatingOffset()) { return 0f } + // swipeAnimation can change during the gesture, we want to always use the initial reference // during the whole drag gesture. - return dragWithOverscroll(delta, animation = initialAnimation) - } - - private fun <T : ContentKey> dragWithOverscroll( - delta: Float, - animation: SwipeAnimation<T>, - ): Float { - require(delta != 0f) { "delta should not be 0" } - var overscrollEffect = overscrollableContent.currentOverscrollEffect - - // If we're already overscrolling, continue with the current effect for a smooth finish. - if (overscrollEffect == null || !overscrollEffect.isInProgress) { - // Otherwise, determine the target content (toContent or fromContent) for the new - // overscroll effect based on the gesture's direction. - val content = animation.contentByDirection(delta) - overscrollEffect = overscrollableContent.applyOverscrollEffectOn(content) - } - - // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags. - if (!overscrollEffect.node.node.isAttached) { - return drag(delta, animation) - } - - return overscrollEffect - .applyToScroll( - delta = delta.toOffset(), - source = NestedScrollSource.UserInput, - performScroll = { - val preScrollAvailable = it.toFloat() - drag(preScrollAvailable, animation).toOffset() - }, - ) - .toFloat() + return drag(delta, animation = initialAnimation) } private fun <T : ContentKey> drag(delta: Float, animation: SwipeAnimation<T>): Float { - if (delta == 0f) return 0f - val distance = animation.distance() val previousOffset = animation.dragOffset val desiredOffset = previousOffset + delta - val desiredProgress = animation.computeProgress(desiredOffset) // Note: the distance could be negative if fromContent is above or to the left of toContent. val newOffset = when { - distance == DistanceUnspecified || - animation.contentTransition.isWithinProgressRange(desiredProgress) -> - desiredOffset + distance == DistanceUnspecified -> { + // Consume everything so that we don't overscroll, this will be coerced later + // when the distance is defined. + delta + } distance > 0f -> desiredOffset.fastCoerceIn(0f, distance) else -> desiredOffset.fastCoerceIn(distance, 0f) } @@ -295,23 +324,19 @@ private class DragControllerImpl( return newOffset - previousOffset } - override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { - // To ensure that any ongoing animation completes gracefully and avoids an undefined state, - // we execute the actual `onStop` logic in a non-cancellable context. This prevents the - // coroutine from being cancelled prematurely, which could interrupt the animation. - // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags. - return withContext(NonCancellable) { onStop(velocity, canChangeContent, swipeAnimation) } + override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float { + return onStop(velocity, swipeAnimation, awaitFling) } private suspend fun <T : ContentKey> onStop( velocity: Float, - canChangeContent: Boolean, // Important: Make sure that this has the same name as [this.swipeAnimation] so that all the // code here references the current animation when [onDragStopped] is called, otherwise the // callbacks (like onAnimationCompleted()) might incorrectly finish a new transition that // replaced this one. swipeAnimation: SwipeAnimation<T>, + awaitFling: suspend () -> Unit, ): Float { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition || swipeAnimation.isAnimatingOffset()) { @@ -319,65 +344,31 @@ private class DragControllerImpl( } val fromContent = swipeAnimation.fromContent + // If we are halfway between two contents, we check what the target will be based on + // the velocity and offset of the transition, then we launch the animation. + + val toContent = swipeAnimation.toContent + + // Compute the destination content (and therefore offset) to settle in. + val offset = swipeAnimation.dragOffset + val distance = swipeAnimation.distance() val targetContent = - if (canChangeContent) { - // If we are halfway between two contents, we check what the target will be based on - // the velocity and offset of the transition, then we launch the animation. - - val toContent = swipeAnimation.toContent - - // Compute the destination content (and therefore offset) to settle in. - val offset = swipeAnimation.dragOffset - val distance = swipeAnimation.distance() - if ( - distance != DistanceUnspecified && - shouldCommitSwipe( - offset = offset, - distance = distance, - velocity = velocity, - wasCommitted = swipeAnimation.currentContent == toContent, - requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe, - ) - ) { - toContent - } else { - fromContent - } + if ( + distance != DistanceUnspecified && + shouldCommitSwipe( + offset = offset, + distance = distance, + velocity = velocity, + wasCommitted = swipeAnimation.currentContent == toContent, + requiresFullDistanceSwipe = swipeAnimation.requiresFullDistanceSwipe, + ) + ) { + toContent } else { - // We are doing an overscroll preview animation between scenes. - check(fromContent == swipeAnimation.currentContent) { - "canChangeContent is false but currentContent != fromContent" - } fromContent } - val overscrollEffect = overscrollableContent.applyOverscrollEffectOn(targetContent) - - // TODO(b/378470603) Remove this check once NestedDraggable is used to handle drags. - if (!overscrollEffect.node.node.isAttached) { - return swipeAnimation.animateOffset(velocity, targetContent) - } - - val overscrollCompletable = CompletableDeferred<Unit>() - try { - overscrollEffect.applyToFling( - velocity = velocity.toVelocity(), - performFling = { - val velocityLeft = it.toFloat() - swipeAnimation - .animateOffset( - velocityLeft, - targetContent, - overscrollCompletable = overscrollCompletable, - ) - .toVelocity() - }, - ) - } finally { - overscrollCompletable.complete(Unit) - } - - return velocity + return swipeAnimation.animateOffset(velocity, targetContent, awaitFling = awaitFling) } /** @@ -422,12 +413,6 @@ private class DragControllerImpl( isCloserToTarget() } } - - override fun onCancel(canChangeContent: Boolean) { - swipeAnimation.contentTransition.coroutineScope.launch { - onStop(velocity = 0f, canChangeContent = canChangeContent) - } - } } /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */ @@ -445,6 +430,58 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol } /** + * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. + * Prioritizes actions with matching [Swipe.Resolved.fromSource]. + * + * @param swipe The swipe to match against. + * @return The best matching [UserActionResult], or `null` if no match is found. + */ + private fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { + if (!areSwipesAllowed()) { + return null + } + + var bestPoints = Int.MIN_VALUE + var bestMatch: UserActionResult? = null + userActions.forEach { (actionSwipe, actionResult) -> + if ( + actionSwipe !is Swipe.Resolved || + // The direction must match. + actionSwipe.direction != swipe.direction || + // The number of pointers down must match. + actionSwipe.pointerCount != swipe.pointerCount || + // The action requires a specific fromSource. + (actionSwipe.fromSource != null && + actionSwipe.fromSource != swipe.fromSource) || + // The action requires a specific pointerType. + (actionSwipe.pointerType != null && + actionSwipe.pointerType != swipe.pointerType) + ) { + // This action is not eligible. + return@forEach + } + + val sameFromSource = actionSwipe.fromSource == swipe.fromSource + val samePointerType = actionSwipe.pointerType == swipe.pointerType + // Prioritize actions with a perfect match. + if (sameFromSource && samePointerType) { + return actionResult + } + + var points = 0 + if (sameFromSource) points++ + if (samePointerType) points++ + + // Otherwise, keep track of the best eligible action. + if (points > bestPoints) { + bestPoints = points + bestMatch = actionResult + } + } + return bestMatch + } + + /** * Update the swipes results. * * Usually we don't want to update them while doing a drag, because this could change the target @@ -460,82 +497,6 @@ internal class Swipes(val upOrLeft: Swipe.Resolved, val downOrRight: Swipe.Resol } } -internal class NestedScrollHandlerImpl( - private val draggableHandler: DraggableHandlerImpl, - private val pointersInfoOwner: PointersInfoOwner, -) { - val connection: PriorityNestedScrollConnection = nestedScrollConnection() - - private fun nestedScrollConnection(): PriorityNestedScrollConnection { - var lastPointersDown: PointersInfo.PointersDown? = null - - return PriorityNestedScrollConnection( - orientation = draggableHandler.orientation, - canStartPreScroll = { _, _, _ -> false }, - canStartPostScroll = { offsetAvailable, _, _ -> - if (offsetAvailable == 0f) return@PriorityNestedScrollConnection false - - lastPointersDown = - when (val info = pointersInfoOwner.pointersInfo()) { - PointersInfo.MouseWheel -> { - // Do not support mouse wheel interactions - return@PriorityNestedScrollConnection false - } - - is PointersInfo.PointersDown -> info - null -> null - } - - draggableHandler.layoutImpl - .contentForUserActions() - .shouldEnableSwipes(draggableHandler.orientation) - }, - onStart = { firstScroll -> - scrollController( - dragController = - draggableHandler.onDragStarted( - pointersDown = lastPointersDown, - overSlop = firstScroll, - ), - pointersInfoOwner = pointersInfoOwner, - ) - }, - ) - } -} - -private fun scrollController( - dragController: DragController, - pointersInfoOwner: PointersInfoOwner, -): ScrollController { - return object : ScrollController { - override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { - if (pointersInfoOwner.pointersInfo() == PointersInfo.MouseWheel) { - // Do not support mouse wheel interactions - return 0f - } - - return dragController.onDrag(delta = deltaScroll) - } - - override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { - return dragController.onStop(velocity = initialVelocity, canChangeContent = true) - } - - override fun onCancel() { - dragController.onCancel(canChangeContent = true) - } - - /** - * We need to maintain scroll priority even if the scene transition can no longer consume - * the scroll gesture to allow us to return to the previous scene. - */ - override fun canCancelScroll(available: Float, consumed: Float) = false - - override fun canStopOnPreFling() = true - } -} - /** * The number of pixels below which there won't be a visible difference in the transition and from * which the animation can stop. @@ -544,12 +505,8 @@ private fun scrollController( // account instead. internal const val OffsetVisibilityThreshold = 0.5f -private object NoOpDragController : DragController { +private object NoOpDragController : NestedDraggable.Controller { override fun onDrag(delta: Float) = 0f - override suspend fun onStop(velocity: Float, canChangeContent: Boolean) = 0f - - override fun onCancel(canChangeContent: Boolean) { - /* do nothing */ - } + override suspend fun onDragStopped(velocity: Float, awaitFling: suspend () -> Unit): Float = 0f } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt deleted file mode 100644 index f5f01d4d1a35..000000000000 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ /dev/null @@ -1,678 +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.compose.animation.scene - -import androidx.annotation.VisibleForTesting -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation -import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation -import androidx.compose.runtime.Stable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.pointer.AwaitPointerEventScope -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerEventType -import androidx.compose.ui.input.pointer.PointerId -import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.input.pointer.PointerType -import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode -import androidx.compose.ui.input.pointer.changedToDown -import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed -import androidx.compose.ui.input.pointer.positionChange -import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed -import androidx.compose.ui.input.pointer.util.VelocityTracker -import androidx.compose.ui.input.pointer.util.addPointerInputChange -import androidx.compose.ui.node.CompositionLocalConsumerModifierNode -import androidx.compose.ui.node.DelegatingNode -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.node.PointerInputModifierNode -import androidx.compose.ui.node.currentValueOf -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.Velocity -import androidx.compose.ui.util.fastAll -import androidx.compose.ui.util.fastAny -import androidx.compose.ui.util.fastFilter -import androidx.compose.ui.util.fastFirstOrNull -import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastSumBy -import com.android.compose.ui.util.SpaceVectorConverter -import kotlin.coroutines.cancellation.CancellationException -import kotlin.math.sign -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -/** - * Make an element draggable in the given [orientation]. - * - * The main difference with [multiPointerDraggable] and - * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number - * of pointers that are down when the drag is started. If you don't need this information, you - * should use `draggable` instead. - * - * Note that the current implementation is trivial: we wait for the touch slope on the *first* down - * pointer, then we count the number of distinct pointers that are down right before calling - * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not - * dragged) and a second pointer is down and dragged. This is an implementation detail that might - * change in the future. - */ -@VisibleForTesting -@Stable -internal fun Modifier.multiPointerDraggable( - orientation: Orientation, - onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, - onFirstPointerDown: () -> Unit = {}, - swipeDetector: SwipeDetector = DefaultSwipeDetector, - dispatcher: NestedScrollDispatcher, -): Modifier = - this.then( - MultiPointerDraggableElement( - orientation, - onDragStarted, - onFirstPointerDown, - swipeDetector, - dispatcher, - ) - ) - -private data class MultiPointerDraggableElement( - private val orientation: Orientation, - private val onDragStarted: - (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, - private val onFirstPointerDown: () -> Unit, - private val swipeDetector: SwipeDetector, - private val dispatcher: NestedScrollDispatcher, -) : ModifierNodeElement<MultiPointerDraggableNode>() { - override fun create(): MultiPointerDraggableNode = - MultiPointerDraggableNode( - orientation = orientation, - onDragStarted = onDragStarted, - onFirstPointerDown = onFirstPointerDown, - swipeDetector = swipeDetector, - dispatcher = dispatcher, - ) - - override fun update(node: MultiPointerDraggableNode) { - node.orientation = orientation - node.onDragStarted = onDragStarted - node.onFirstPointerDown = onFirstPointerDown - node.swipeDetector = swipeDetector - } -} - -internal class MultiPointerDraggableNode( - orientation: Orientation, - var onDragStarted: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, - var onFirstPointerDown: () -> Unit, - swipeDetector: SwipeDetector = DefaultSwipeDetector, - private val dispatcher: NestedScrollDispatcher, -) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode { - private val pointerTracker = delegate(SuspendingPointerInputModifierNode { pointerTracker() }) - private val pointerInput = delegate(SuspendingPointerInputModifierNode { pointerInput() }) - private val velocityTracker = VelocityTracker() - - var swipeDetector: SwipeDetector = swipeDetector - set(value) { - if (value != field) { - field = value - pointerInput.resetPointerInputHandler() - } - } - - private var converter = SpaceVectorConverter(orientation) - - fun Offset.toFloat(): Float = with(converter) { this@toFloat.toFloat() } - - fun Velocity.toFloat(): Float = with(converter) { this@toFloat.toFloat() } - - fun Float.toOffset(): Offset = with(converter) { this@toOffset.toOffset() } - - fun Float.toVelocity(): Velocity = with(converter) { this@toVelocity.toVelocity() } - - var orientation: Orientation = orientation - set(value) { - // Reset the pointer input whenever orientation changed. - if (value != field) { - field = value - converter = SpaceVectorConverter(value) - pointerInput.resetPointerInputHandler() - } - } - - override fun onCancelPointerInput() { - pointerTracker.onCancelPointerInput() - pointerInput.onCancelPointerInput() - } - - override fun onPointerEvent( - pointerEvent: PointerEvent, - pass: PointerEventPass, - bounds: IntSize, - ) { - // The order is important here: the tracker is always called first. - pointerTracker.onPointerEvent(pointerEvent, pass, bounds) - pointerInput.onPointerEvent(pointerEvent, pass, bounds) - } - - private var lastPointerEvent: PointerEvent? = null - private var startedPosition: Offset? = null - private var countPointersDown: Int = 0 - - internal fun pointersInfo(): PointersInfo? { - // This may be null, i.e. when the user uses TalkBack - val lastPointerEvent = lastPointerEvent ?: return null - - if (lastPointerEvent.type == PointerEventType.Scroll) return PointersInfo.MouseWheel - - val startedPosition = startedPosition ?: return null - - return PointersInfo.PointersDown( - startedPosition = startedPosition, - count = countPointersDown, - countByType = - buildMap { - lastPointerEvent.changes.fastForEach { change -> - if (!change.pressed) return@fastForEach - val newValue = (get(change.type) ?: 0) + 1 - put(change.type, newValue) - } - }, - ) - } - - private suspend fun PointerInputScope.pointerTracker() { - val currentContext = currentCoroutineContext() - awaitPointerEventScope { - var velocityPointerId: PointerId? = null - // Intercepts pointer inputs and exposes [PointersInfo], via - // [requireAncestorPointersInfoOwner], to our descendants. - while (currentContext.isActive) { - // During the Initial pass, we receive the event after our ancestors. - val pointerEvent = awaitPointerEvent(PointerEventPass.Initial) - - // Ignore cursor has entered the input region. - // This will only be sent after the cursor is hovering when in the input region. - if (pointerEvent.type == PointerEventType.Enter) continue - - val changes = pointerEvent.changes - lastPointerEvent = pointerEvent - countPointersDown = changes.countDown() - - when { - // There are no more pointers down. - countPointersDown == 0 -> { - startedPosition = null - - // In case of multiple events with 0 pointers down (not pressed) we may have - // already removed the velocityPointer - val lastPointerUp = changes.fastFilter { it.id == velocityPointerId } - check(lastPointerUp.isEmpty() || lastPointerUp.size == 1) { - "There are ${lastPointerUp.size} pointers up: $lastPointerUp" - } - if (lastPointerUp.size == 1) { - velocityTracker.addPointerInputChange(lastPointerUp.first()) - } - } - - // The first pointer down, startedPosition was not set. - startedPosition == null -> { - // Mouse wheel could start with multiple pointer down - val firstPointerDown = changes.first() - velocityPointerId = firstPointerDown.id - velocityTracker.resetTracking() - velocityTracker.addPointerInputChange(firstPointerDown) - startedPosition = firstPointerDown.position - onFirstPointerDown() - } - - // Changes with at least one pointer - else -> { - val pointerChange = changes.first() - - // Assuming that the list of changes doesn't have two changes with the same - // id (PointerId), we can check: - // - If the first change has `id` equals to `velocityPointerId` (this should - // always be true unless the pointer has been removed). - // - If it does, we've found our change event (assuming there aren't any - // others changes with the same id in this PointerEvent - not checked). - // - If it doesn't, we can check that the change with that id isn't in first - // place (which should never happen - this will crash). - check( - pointerChange.id == velocityPointerId || - !changes.fastAny { it.id == velocityPointerId } - ) { - "$velocityPointerId is present, but not the first: $changes" - } - - // If the previous pointer has been removed, we use the first available - // change to keep tracking the velocity. - velocityPointerId = - if (pointerChange.pressed) { - pointerChange.id - } else { - changes.first { it.pressed }.id - } - - velocityTracker.addPointerInputChange(pointerChange) - } - } - } - } - } - - private suspend fun PointerInputScope.pointerInput() { - val currentContext = currentCoroutineContext() - awaitPointerEventScope { - while (currentContext.isActive) { - try { - detectDragGestures( - orientation = orientation, - onDragStart = { pointersDown, overSlop -> - onDragStarted(pointersDown, overSlop) - }, - onDrag = { controller, amount -> - dispatchScrollEvents( - availableOnPreScroll = amount, - onScroll = { controller.onDrag(it) }, - source = NestedScrollSource.UserInput, - ) - }, - onDragEnd = { controller -> - startFlingGesture( - initialVelocity = - currentValueOf(LocalViewConfiguration) - .maximumFlingVelocity - .let { - val maxVelocity = Velocity(it, it) - velocityTracker.calculateVelocity(maxVelocity) - } - .toFloat(), - onFling = { controller.onStop(it, canChangeContent = true) }, - ) - }, - onDragCancel = { controller -> - startFlingGesture( - initialVelocity = 0f, - onFling = { controller.onStop(it, canChangeContent = true) }, - ) - }, - swipeDetector = swipeDetector, - ) - } catch (exception: CancellationException) { - // If the coroutine scope is active, we can just restart the drag cycle. - if (!currentContext.isActive) { - throw exception - } - } - } - } - } - - /** - * Start a fling gesture in another CoroutineScope, this is to ensure that even when the pointer - * input scope is reset we will continue any coroutine scope that we started from these methods - * while the pointer input scope was active. - * - * Note: Inspired by [androidx.compose.foundation.gestures.ScrollableNode.onDragStopped] - */ - private fun startFlingGesture( - initialVelocity: Float, - onFling: suspend (velocity: Float) -> Float, - ) { - // Note: [AwaitPointerEventScope] is annotated as @RestrictsSuspension, we need another - // CoroutineScope to run the fling gestures. - // We do not need to cancel this [Job], the source will take care of emitting an - // [onPostFling] before starting a new gesture. - dispatcher.coroutineScope.launch { - dispatchFlingEvents(availableOnPreFling = initialVelocity, onFling = onFling) - } - } - - /** - * Use the nested scroll system to fire scroll events. This allows us to consume events from our - * ancestors during the pre-scroll and post-scroll phases. - * - * @param availableOnPreScroll amount available before the scroll, this can be partially - * consumed by our ancestors. - * @param onScroll function that returns the amount consumed during a scroll given the amount - * available after the [NestedScrollConnection.onPreScroll]. - * @param source the source of the scroll event - * @return Total offset consumed. - */ - private inline fun dispatchScrollEvents( - availableOnPreScroll: Float, - onScroll: (delta: Float) -> Float, - source: NestedScrollSource, - ): Float { - // PreScroll phase - val consumedByPreScroll = - dispatcher - .dispatchPreScroll(available = availableOnPreScroll.toOffset(), source = source) - .toFloat() - - // Scroll phase - val availableOnScroll = availableOnPreScroll - consumedByPreScroll - val consumedBySelfScroll = onScroll(availableOnScroll) - - // PostScroll phase - val availableOnPostScroll = availableOnScroll - consumedBySelfScroll - val consumedByPostScroll = - dispatcher - .dispatchPostScroll( - consumed = consumedBySelfScroll.toOffset(), - available = availableOnPostScroll.toOffset(), - source = source, - ) - .toFloat() - - return consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll - } - - /** - * Use the nested scroll system to fire fling events. This allows us to consume events from our - * ancestors during the pre-fling and post-fling phases. - * - * @param availableOnPreFling velocity available before the fling, this can be partially - * consumed by our ancestors. - * @param onFling function that returns the velocity consumed during the fling given the - * velocity available after the [NestedScrollConnection.onPreFling]. - * @return Total velocity consumed. - */ - private suspend inline fun dispatchFlingEvents( - availableOnPreFling: Float, - onFling: suspend (velocity: Float) -> Float, - ): Float { - // PreFling phase - val consumedByPreFling = - dispatcher.dispatchPreFling(available = availableOnPreFling.toVelocity()).toFloat() - - // Fling phase - val availableOnFling = availableOnPreFling - consumedByPreFling - val consumedBySelfFling = onFling(availableOnFling) - - // PostFling phase - val availableOnPostFling = availableOnFling - consumedBySelfFling - val consumedByPostFling = - dispatcher - .dispatchPostFling( - consumed = consumedBySelfFling.toVelocity(), - available = availableOnPostFling.toVelocity(), - ) - .toFloat() - - return consumedByPreFling + consumedBySelfFling + consumedByPostFling - } - - /** - * Detect drag gestures in the given [orientation]. - * - * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and - * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for passing - * the number of pointers down to [onDragStart]. - */ - private suspend fun AwaitPointerEventScope.detectDragGestures( - orientation: Orientation, - onDragStart: (pointersDown: PointersInfo.PointersDown, overSlop: Float) -> DragController, - onDrag: (controller: DragController, dragAmount: Float) -> Unit, - onDragEnd: (controller: DragController) -> Unit, - onDragCancel: (controller: DragController) -> Unit, - swipeDetector: SwipeDetector, - ) { - val consumablePointer = - awaitConsumableEvent { - // We are searching for an event that can be used as the starting point for the - // drag gesture. Our options are: - // - Initial: These events should never be consumed by the MultiPointerDraggable - // since our ancestors can consume the gesture, but we would eliminate this - // possibility for our descendants. - // - Main: These events are consumed during the drag gesture, and they are a - // good place to start if the previous event has not been consumed. - // - Final: If the previous event has been consumed, we can wait for the Main - // pass to finish. If none of our ancestors were interested in the event, we - // can wait for an unconsumed event in the Final pass. - val previousConsumed = currentEvent.changes.fastAny { it.isConsumed } - if (previousConsumed) PointerEventPass.Final else PointerEventPass.Main - } - .changes - .first() - - var overSlop = 0f - val onSlopReached = { change: PointerInputChange, over: Float -> - if (swipeDetector.detectSwipe(change)) { - change.consume() - overSlop = over - } - } - - // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it - // is public. - val drag = - when (orientation) { - Orientation.Horizontal -> - awaitHorizontalTouchSlopOrCancellation(consumablePointer.id, onSlopReached) - Orientation.Vertical -> - awaitVerticalTouchSlopOrCancellation(consumablePointer.id, onSlopReached) - } ?: return - - val lastPointersDown = - checkNotNull(pointersInfo()) { - "We should have pointers down, last event: $currentEvent" - } - as PointersInfo.PointersDown - // Make sure that overSlop is not 0f. This can happen when the user drags by exactly - // the touch slop. However, the overSlop we pass to onDragStarted() is used to - // compute the direction we are dragging in, so overSlop should never be 0f. - if (overSlop == 0f) { - // If the user drags in the opposite direction, the delta becomes zero because - // we return to the original point. Therefore, we should use the previous event - // to calculate the direction. - val delta = (drag.position - drag.previousPosition).toFloat() - check(delta != 0f) { - buildString { - append("delta is equal to 0 ") - append("touchSlop ${currentValueOf(LocalViewConfiguration).touchSlop} ") - append("consumablePointer.position ${consumablePointer.position} ") - append("drag.position ${drag.position} ") - append("drag.previousPosition ${drag.previousPosition}") - } - } - overSlop = delta.sign - } - - val controller = onDragStart(lastPointersDown, overSlop) - val successful: Boolean - try { - onDrag(controller, overSlop) - - successful = - drag( - initialPointerId = drag.id, - hasDragged = { it.positionChangeIgnoreConsumed().toFloat() != 0f }, - onDrag = { - onDrag(controller, it.positionChange().toFloat()) - it.consume() - }, - onIgnoredEvent = { - // We are still dragging an object, but this event is not of interest to the - // caller. - // This event will not trigger the onDrag event, but we will consume the - // event to prevent another pointerInput from interrupting the current - // gesture just because the event was ignored. - it.consume() - }, - ) - } catch (t: Throwable) { - onDragCancel(controller) - throw t - } - - if (successful) { - onDragEnd(controller) - } else { - onDragCancel(controller) - } - } - - private suspend fun AwaitPointerEventScope.awaitConsumableEvent( - pass: () -> PointerEventPass - ): PointerEvent { - fun canBeConsumed(changes: List<PointerInputChange>): Boolean { - // At least one pointer down AND - return changes.fastAny { it.pressed } && - // All pointers must be either: - changes.fastAll { - // A) unconsumed AND recently pressed - it.changedToDown() || - // B) unconsumed AND in a new position (on the current axis) - it.positionChange().toFloat() != 0f - } - } - - var event: PointerEvent - do { - event = awaitPointerEvent(pass = pass()) - } while (!canBeConsumed(event.changes)) - - // We found a consumable event in the Main pass - return event - } - - /** - * Continues to read drag events until all pointers are up or the drag event is canceled. The - * initial pointer to use for driving the drag is [initialPointerId]. [hasDragged] passes the - * result whether a change was detected from the drag function or not. - * - * Whenever the pointer moves, if [hasDragged] returns true, [onDrag] is called; otherwise, - * [onIgnoredEvent] is called. - * - * @return true when gesture ended with all pointers up and false when the gesture was canceled. - * - * Note: Inspired by DragGestureDetector.kt - */ - private suspend inline fun AwaitPointerEventScope.drag( - initialPointerId: PointerId, - hasDragged: (PointerInputChange) -> Boolean, - onDrag: (PointerInputChange) -> Unit, - onIgnoredEvent: (PointerInputChange) -> Unit, - ): Boolean { - val pointer = currentEvent.changes.fastFirstOrNull { it.id == initialPointerId } - val isPointerUp = pointer?.pressed != true - if (isPointerUp) { - return false // The pointer has already been lifted, so the gesture is canceled - } - var pointerId = initialPointerId - while (true) { - val change = awaitDragOrUp(pointerId, hasDragged, onIgnoredEvent) ?: return false - - if (change.isConsumed) { - return false - } - - if (change.changedToUpIgnoreConsumed()) { - return true - } - - onDrag(change) - pointerId = change.id - } - } - - /** - * Waits for a single drag in one axis, final pointer up, or all pointers are up. When - * [initialPointerId] has lifted, another pointer that is down is chosen to be the finger - * governing the drag. When the final pointer is lifted, that [PointerInputChange] is returned. - * When a drag is detected, that [PointerInputChange] is returned. A drag is only detected when - * [hasDragged] returns `true`. Events that should not be captured are passed to - * [onIgnoredEvent]. - * - * `null` is returned if there was an error in the pointer input stream and the pointer that was - * down was dropped before the 'up' was received. - * - * Note: Inspired by DragGestureDetector.kt - */ - private suspend inline fun AwaitPointerEventScope.awaitDragOrUp( - initialPointerId: PointerId, - hasDragged: (PointerInputChange) -> Boolean, - onIgnoredEvent: (PointerInputChange) -> Unit, - ): PointerInputChange? { - var pointerId = initialPointerId - while (true) { - val event = awaitPointerEvent() - val dragEvent = event.changes.fastFirstOrNull { it.id == pointerId } ?: return null - if (dragEvent.changedToUpIgnoreConsumed()) { - val otherDown = event.changes.fastFirstOrNull { it.pressed } - if (otherDown == null) { - // This is the last "up" - return dragEvent - } else { - pointerId = otherDown.id - } - } else if (hasDragged(dragEvent)) { - return dragEvent - } else { - onIgnoredEvent(dragEvent) - } - } - } - - private fun List<PointerInputChange>.countDown() = fastSumBy { if (it.pressed) 1 else 0 } -} - -internal fun interface PointersInfoOwner { - /** - * Provides information about the pointers interacting with this composable. - * - * @return A [PointersInfo] object containing details about the pointers, including the starting - * position and the number of pointers down, or `null` if there are no pointers down. - */ - fun pointersInfo(): PointersInfo? -} - -internal sealed interface PointersInfo { - /** - * Holds information about pointer interactions within a composable. - * - * This class stores details such as the starting position of a gesture, the number of pointers - * down, and whether the last pointer event was a mouse wheel scroll. - * - * @param startedPosition The starting position of the gesture. This is the position where the - * first pointer touched the screen, not necessarily the point where dragging begins. This may - * be different from the initial touch position if a child composable intercepts the gesture - * before this one. - * @param count The number of pointers currently down. - * @param countByType Provide a map of pointer types to the count of pointers of that type - * currently down/pressed. - */ - data class PointersDown( - val startedPosition: Offset, - val count: Int, - val countByType: Map<PointerType, Int>, - ) : PointersInfo { - init { - check(count > 0) { "We should have at least 1 pointer down, $count instead" } - } - } - - /** Indicates whether the last pointer event was a mouse wheel scroll. */ - data object MouseWheel : PointersInfo -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index de428a7d3548..4e389471d1d0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -457,7 +457,7 @@ data class Swipe private constructor( val direction: SwipeDirection, val pointerCount: Int = 1, - val pointersType: PointerType? = null, + val pointerType: PointerType? = null, val fromSource: SwipeSource? = null, ) : UserAction() { companion object { @@ -470,46 +470,46 @@ private constructor( fun Left( pointerCount: Int = 1, - pointersType: PointerType? = null, + pointerType: PointerType? = null, fromSource: SwipeSource? = null, - ) = Swipe(SwipeDirection.Left, pointerCount, pointersType, fromSource) + ) = Swipe(SwipeDirection.Left, pointerCount, pointerType, fromSource) fun Up( pointerCount: Int = 1, - pointersType: PointerType? = null, + pointerType: PointerType? = null, fromSource: SwipeSource? = null, - ) = Swipe(SwipeDirection.Up, pointerCount, pointersType, fromSource) + ) = Swipe(SwipeDirection.Up, pointerCount, pointerType, fromSource) fun Right( pointerCount: Int = 1, - pointersType: PointerType? = null, + pointerType: PointerType? = null, fromSource: SwipeSource? = null, - ) = Swipe(SwipeDirection.Right, pointerCount, pointersType, fromSource) + ) = Swipe(SwipeDirection.Right, pointerCount, pointerType, fromSource) fun Down( pointerCount: Int = 1, - pointersType: PointerType? = null, + pointerType: PointerType? = null, fromSource: SwipeSource? = null, - ) = Swipe(SwipeDirection.Down, pointerCount, pointersType, fromSource) + ) = Swipe(SwipeDirection.Down, pointerCount, pointerType, fromSource) fun Start( pointerCount: Int = 1, - pointersType: PointerType? = null, + pointerType: PointerType? = null, fromSource: SwipeSource? = null, - ) = Swipe(SwipeDirection.Start, pointerCount, pointersType, fromSource) + ) = Swipe(SwipeDirection.Start, pointerCount, pointerType, fromSource) fun End( pointerCount: Int = 1, - pointersType: PointerType? = null, + pointerType: PointerType? = null, fromSource: SwipeSource? = null, - ) = Swipe(SwipeDirection.End, pointerCount, pointersType, fromSource) + ) = Swipe(SwipeDirection.End, pointerCount, pointerType, fromSource) } override fun resolve(layoutDirection: LayoutDirection): UserAction.Resolved { return Resolved( direction = direction.resolve(layoutDirection), pointerCount = pointerCount, - pointersType = pointersType, + pointerType = pointerType, fromSource = fromSource?.resolve(layoutDirection), ) } @@ -519,7 +519,7 @@ private constructor( val direction: SwipeDirection.Resolved, val pointerCount: Int, val fromSource: SwipeSource.Resolved?, - val pointersType: PointerType?, + val pointerType: PointerType?, ) : UserAction.Resolved() } @@ -724,6 +724,7 @@ internal fun SceneTransitionLayoutForTesting( density = density, layoutDirection = layoutDirection, swipeSourceDetector = swipeSourceDetector, + swipeDetector = swipeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = builder, animationScope = animationScope, @@ -767,8 +768,9 @@ internal fun SceneTransitionLayoutForTesting( layoutImpl.density = density layoutImpl.layoutDirection = layoutDirection layoutImpl.swipeSourceDetector = swipeSourceDetector + layoutImpl.swipeDetector = swipeDetector layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold } - layoutImpl.Content(modifier, swipeDetector) + layoutImpl.Content(modifier) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index e5bdc92b5762..b4c449d0566c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -31,6 +31,9 @@ import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ApproachLayoutModifierNode import androidx.compose.ui.layout.ApproachMeasureScope import androidx.compose.ui.layout.LookaheadScope @@ -49,10 +52,8 @@ import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.Overlay import com.android.compose.animation.scene.content.Scene import com.android.compose.animation.scene.content.state.TransitionState -import com.android.compose.animation.scene.effect.GestureEffect import com.android.compose.ui.util.lerp import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch /** The type for the content of movable elements. */ internal typealias MovableElementContent = @Composable (@Composable () -> Unit) -> Unit @@ -75,6 +76,7 @@ internal class SceneTransitionLayoutImpl( internal var density: Density, internal var layoutDirection: LayoutDirection, internal var swipeSourceDetector: SwipeSourceDetector, + internal var swipeDetector: SwipeDetector, internal var transitionInterceptionThreshold: Float, builder: SceneTransitionLayoutScope.() -> Unit, @@ -149,18 +151,6 @@ internal class SceneTransitionLayoutImpl( _movableContents = it } - internal var horizontalOverscrollableContent = - OverscrollableContent( - animationScope = animationScope, - overscrollEffect = { content(it).scope.horizontalOverscrollGestureEffect }, - ) - - internal var verticalOverscrollableContent = - OverscrollableContent( - animationScope = animationScope, - overscrollEffect = { content(it).scope.verticalOverscrollGestureEffect }, - ) - /** * The different values of a shared value keyed by a a [ValueKey] and the different elements and * contents it is associated to. @@ -175,8 +165,8 @@ internal class SceneTransitionLayoutImpl( } // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed. - internal val horizontalDraggableHandler: DraggableHandlerImpl - internal val verticalDraggableHandler: DraggableHandlerImpl + internal val horizontalDraggableHandler: DraggableHandler + internal val verticalDraggableHandler: DraggableHandler internal val elementStateScope = ElementStateScopeImpl(this) internal val propertyTransformationScope = PropertyTransformationScopeImpl(this) @@ -190,16 +180,33 @@ internal class SceneTransitionLayoutImpl( internal var lastSize: IntSize = IntSize.Zero + /** + * An empty [NestedScrollDispatcher] and [NestedScrollConnection]. These are composed above our + * [SwipeToSceneElement] modifiers, so that the dispatcher will be used by the nested draggables + * to launch fling events, making sure that they are not cancelled unless this whole layout is + * removed from composition. + */ + private val nestedScrollDispatcher = NestedScrollDispatcher() + private val nestedScrollConnection = object : NestedScrollConnection {} + init { updateContents(builder, layoutDirection) // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the // current scene (required for SwipeTransition). horizontalDraggableHandler = - DraggableHandlerImpl(layoutImpl = this, orientation = Orientation.Horizontal) + DraggableHandler( + layoutImpl = this, + orientation = Orientation.Horizontal, + gestureEffectProvider = { content(it).scope.horizontalOverscrollGestureEffect }, + ) verticalDraggableHandler = - DraggableHandlerImpl(layoutImpl = this, orientation = Orientation.Vertical) + DraggableHandler( + layoutImpl = this, + orientation = Orientation.Vertical, + gestureEffectProvider = { content(it).scope.verticalOverscrollGestureEffect }, + ) // Make sure that the state is created on the same thread (most probably the main thread) // than this STLImpl. @@ -379,14 +386,15 @@ internal class SceneTransitionLayoutImpl( } @Composable - internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) { + internal fun Content(modifier: Modifier) { Box( modifier + .nestedScroll(nestedScrollConnection, nestedScrollDispatcher) // Handle horizontal and vertical swipes on this layout. // Note: order here is important and will give a slight priority to the vertical // swipes. - .swipeToScene(horizontalDraggableHandler, swipeDetector) - .swipeToScene(verticalDraggableHandler, swipeDetector) + .swipeToScene(horizontalDraggableHandler) + .swipeToScene(verticalDraggableHandler) .then(LayoutElement(layoutImpl = this)) ) { LookaheadScope { @@ -580,23 +588,3 @@ private class LayoutNode(var layoutImpl: SceneTransitionLayoutImpl) : return layout(width, height) { placeable.place(0, 0) } } } - -internal class OverscrollableContent( - private val animationScope: CoroutineScope, - private val overscrollEffect: (ContentKey) -> GestureEffect, -) { - private var currentContent: ContentKey? = null - var currentOverscrollEffect: GestureEffect? = null - - fun applyOverscrollEffectOn(contentKey: ContentKey): GestureEffect { - if (currentContent == contentKey) return currentOverscrollEffect!! - - currentOverscrollEffect?.apply { animationScope.launch { ensureApplyToFlingIsCalled() } } - - // We are wrapping the overscroll effect. - val overscrollEffect = overscrollEffect(contentKey) - currentContent = contentKey - currentOverscrollEffect = overscrollEffect - return overscrollEffect - } -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index ff8efc28aa21..d50304d433f9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -34,7 +34,7 @@ import com.android.internal.jank.Cuj.CujType /** The transitions configuration of a [SceneTransitionLayout]. */ class SceneTransitions internal constructor( - internal val defaultSwipeSpec: SpringSpec<Float>, + internal val defaultMotionSpatialSpec: SpringSpec<Float>, internal val transitionSpecs: List<TransitionSpecImpl>, internal val interruptionHandler: InterruptionHandler, ) { @@ -132,7 +132,7 @@ internal constructor( val Empty = SceneTransitions( - defaultSwipeSpec = DefaultSwipeSpec, + defaultMotionSpatialSpec = DefaultSwipeSpec, transitionSpecs = emptyList(), interruptionHandler = DefaultInterruptionHandler, ) @@ -194,9 +194,9 @@ internal interface TransformationSpec { * The [SpringSpec] used to animate the associated transition progress when the transition was * started by a swipe and is now animating back to a scene because the user lifted their finger. * - * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used. + * If `null`, then the [SceneTransitions.defaultMotionSpatialSpec] will be used. */ - val swipeSpec: SpringSpec<Float>? + val motionSpatialSpec: AnimationSpec<Float>? /** * The distance it takes for this transition to animate from 0% to 100% when it is driven by a @@ -213,7 +213,7 @@ internal interface TransformationSpec { internal val Empty = TransformationSpecImpl( progressSpec = snap(), - swipeSpec = null, + motionSpatialSpec = null, distance = null, transformationMatchers = emptyList(), ) @@ -246,7 +246,7 @@ internal class TransitionSpecImpl( val reverse = transformationSpec.invoke(transition) TransformationSpecImpl( progressSpec = reverse.progressSpec, - swipeSpec = reverse.swipeSpec, + motionSpatialSpec = reverse.motionSpatialSpec, distance = reverse.distance, transformationMatchers = reverse.transformationMatchers.map { @@ -276,7 +276,7 @@ internal class TransitionSpecImpl( */ internal class TransformationSpecImpl( override val progressSpec: AnimationSpec<Float>, - override val swipeSpec: SpringSpec<Float>?, + override val motionSpatialSpec: SpringSpec<Float>?, override val distance: UserActionDistance?, override val transformationMatchers: List<TransformationMatcher>, ) : TransformationSpec { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt index ba92f9bea07d..b1d6d1efc264 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.Companion.DistanceUnspecified import kotlin.math.absoluteValue @@ -76,7 +77,16 @@ internal fun createSwipeAnimation( return DistanceUnspecified } - val distance = if (isUpOrLeft) -absoluteDistance else absoluteDistance + // Compute the signed distance and make sure that the offset is always coerced in the right + // range. + val distance = + if (isUpOrLeft) { + animation.dragOffset = animation.dragOffset.fastCoerceIn(-absoluteDistance, 0f) + -absoluteDistance + } else { + animation.dragOffset = animation.dragOffset.fastCoerceIn(0f, absoluteDistance) + absoluteDistance + } lastDistance = distance return distance } @@ -294,12 +304,10 @@ internal class SwipeAnimation<T : ContentKey>( initialVelocity: Float, targetContent: T, spec: AnimationSpec<Float>? = null, - overscrollCompletable: CompletableDeferred<Unit>? = null, + awaitFling: (suspend () -> Unit)? = null, ): Float { check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" } - val initialProgress = progress - val targetContent = if (targetContent != currentContent && !canChangeContent(targetContent)) { currentContent @@ -307,18 +315,16 @@ internal class SwipeAnimation<T : ContentKey>( targetContent } - // Skip the animation if we have already reached the target content and the overscroll does - // not animate anything. - val hasReachedTargetContent = - (targetContent == toContent && initialProgress >= 1f) || - (targetContent == fromContent && initialProgress <= 0f) - val skipAnimation = - hasReachedTargetContent && !contentTransition.isWithinProgressRange(initialProgress) - val distance = distance() - check(distance != DistanceUnspecified) { "distance is equal to $DistanceUnspecified" } - - val targetOffset = if (targetContent == fromContent) 0f else distance + val targetOffset = + if (targetContent == fromContent) { + 0f + } else { + check(distance != DistanceUnspecified) { + "distance is equal to $DistanceUnspecified" + } + distance + } // If the effective current content changed, it should be reflected right now in the // current state, even before the settle animation is ongoing. That way all the @@ -350,33 +356,17 @@ internal class SwipeAnimation<T : ContentKey>( check(isAnimatingOffset()) - // Note: we still create the animatable and set it on offsetAnimation even when - // skipAnimation is true, just so that isUserInputOngoing and isAnimatingOffset() are - // unchanged even despite this small skip-optimization (which is just an implementation - // detail). - if (skipAnimation) { - // Unblock the job. - offsetAnimationRunnable.complete { - // Wait for overscroll to finish so that the transition is removed from the STLState - // only after the overscroll is done, to avoid dropping frame right when the user - // lifts their finger and overscroll is animated to 0. - overscrollCompletable?.await() - } - return 0f - } - - val swipeSpec = + val motionSpatialSpec = spec - ?: contentTransition.transformationSpec.swipeSpec - ?: layoutState.transitions.defaultSwipeSpec + ?: contentTransition.transformationSpec.motionSpatialSpec + ?: layoutState.transitions.defaultMotionSpatialSpec val velocityConsumed = CompletableDeferred<Float>() - offsetAnimationRunnable.complete { val result = animatable.animateTo( targetValue = targetOffset, - animationSpec = swipeSpec, + animationSpec = motionSpatialSpec, initialVelocity = initialVelocity, ) @@ -385,9 +375,9 @@ internal class SwipeAnimation<T : ContentKey>( velocityConsumed.complete(initialVelocity - result.endState.velocity) // Wait for overscroll to finish so that the transition is removed from the STLState - // only after the overscroll is done, to avoid dropping frame right when the user - // lifts their finger and overscroll is animated to 0. - overscrollCompletable?.await() + // only after the overscroll is done, to avoid dropping frame right when the user lifts + // their finger and overscroll is animated to 0. + awaitFling?.invoke() } return velocityConsumed.await() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 3f6bce724b1b..19f707df91e0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -19,179 +19,35 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher -import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.node.DelegatingNode -import androidx.compose.ui.node.ModifierNodeElement -import androidx.compose.ui.node.PointerInputModifierNode -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.content.Content +import com.android.compose.gesture.nestedDraggable /** * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ @Stable -internal fun Modifier.swipeToScene( - draggableHandler: DraggableHandlerImpl, - swipeDetector: SwipeDetector, -): Modifier { - return if (draggableHandler.enabled()) { - this.then(SwipeToSceneElement(draggableHandler, swipeDetector)) - } else { - this - } +internal fun Modifier.swipeToScene(draggableHandler: DraggableHandler): Modifier { + return this.nestedDraggable( + draggable = draggableHandler, + orientation = draggableHandler.orientation, + overscrollEffect = draggableHandler.overscrollEffect, + enabled = draggableHandler.enabled(), + ) } -private fun DraggableHandlerImpl.enabled(): Boolean { +internal fun DraggableHandler.enabled(): Boolean { return isDrivingTransition || contentForSwipes().shouldEnableSwipes(orientation) } -private fun DraggableHandlerImpl.contentForSwipes(): Content { +private fun DraggableHandler.contentForSwipes(): Content { return layoutImpl.contentForUserActions() } /** Whether swipe should be enabled in the given [orientation]. */ -internal fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { +private fun Content.shouldEnableSwipes(orientation: Orientation): Boolean { if (userActions.isEmpty() || !areSwipesAllowed()) { return false } return userActions.keys.any { it is Swipe.Resolved && it.direction.orientation == orientation } } - -/** - * Finds the best matching [UserActionResult] for the given [swipe] within this [Content]. - * Prioritizes actions with matching [Swipe.Resolved.fromSource]. - * - * @param swipe The swipe to match against. - * @return The best matching [UserActionResult], or `null` if no match is found. - */ -internal fun Content.findActionResultBestMatch(swipe: Swipe.Resolved): UserActionResult? { - if (!areSwipesAllowed()) { - return null - } - - var bestPoints = Int.MIN_VALUE - var bestMatch: UserActionResult? = null - userActions.forEach { (actionSwipe, actionResult) -> - if ( - actionSwipe !is Swipe.Resolved || - // The direction must match. - actionSwipe.direction != swipe.direction || - // The number of pointers down must match. - actionSwipe.pointerCount != swipe.pointerCount || - // The action requires a specific fromSource. - (actionSwipe.fromSource != null && actionSwipe.fromSource != swipe.fromSource) || - // The action requires a specific pointerType. - (actionSwipe.pointersType != null && actionSwipe.pointersType != swipe.pointersType) - ) { - // This action is not eligible. - return@forEach - } - - val sameFromSource = actionSwipe.fromSource == swipe.fromSource - val samePointerType = actionSwipe.pointersType == swipe.pointersType - // Prioritize actions with a perfect match. - if (sameFromSource && samePointerType) { - return actionResult - } - - var points = 0 - if (sameFromSource) points++ - if (samePointerType) points++ - - // Otherwise, keep track of the best eligible action. - if (points > bestPoints) { - bestPoints = points - bestMatch = actionResult - } - } - return bestMatch -} - -private data class SwipeToSceneElement( - val draggableHandler: DraggableHandlerImpl, - val swipeDetector: SwipeDetector, -) : ModifierNodeElement<SwipeToSceneRootNode>() { - override fun create(): SwipeToSceneRootNode = - SwipeToSceneRootNode(draggableHandler, swipeDetector) - - override fun update(node: SwipeToSceneRootNode) { - node.update(draggableHandler, swipeDetector) - } -} - -private class SwipeToSceneRootNode( - draggableHandler: DraggableHandlerImpl, - swipeDetector: SwipeDetector, -) : DelegatingNode() { - private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) - - fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) { - if (draggableHandler == delegateNode.draggableHandler) { - // Simple update, just update the swipe detector directly and keep the node. - delegateNode.swipeDetector = swipeDetector - } else { - // The draggableHandler changed, force recreate the underlying SwipeToSceneNode. - undelegate(delegateNode) - delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) - } - } -} - -private class SwipeToSceneNode( - val draggableHandler: DraggableHandlerImpl, - swipeDetector: SwipeDetector, -) : DelegatingNode(), PointerInputModifierNode { - private val dispatcher = NestedScrollDispatcher() - private val multiPointerDraggableNode = - delegate( - MultiPointerDraggableNode( - orientation = draggableHandler.orientation, - onDragStarted = draggableHandler::onDragStarted, - onFirstPointerDown = ::onFirstPointerDown, - swipeDetector = swipeDetector, - dispatcher = dispatcher, - ) - ) - - var swipeDetector: SwipeDetector - get() = multiPointerDraggableNode.swipeDetector - set(value) { - multiPointerDraggableNode.swipeDetector = value - } - - private val nestedScrollHandlerImpl = - NestedScrollHandlerImpl( - draggableHandler = draggableHandler, - pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() }, - ) - - init { - delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher)) - } - - private fun onFirstPointerDown() { - // When we drag our finger across the screen, the NestedScrollConnection keeps track of all - // the scroll events until we lift our finger. However, in some cases, the connection might - // not receive the "up" event. This can lead to an incorrect initial state for the gesture. - // To prevent this issue, we can call the reset() method when the first finger touches the - // screen. This ensures that the NestedScrollConnection starts from a correct state. - nestedScrollHandlerImpl.connection.reset() - } - - override fun onDetach() { - // Make sure we reset the scroll connection when this modifier is removed from composition - nestedScrollHandlerImpl.connection.reset() - } - - override fun onPointerEvent( - pointerEvent: PointerEvent, - pass: PointerEventPass, - bounds: IntSize, - ) = multiPointerDraggableNode.onPointerEvent(pointerEvent, pass, bounds) - - override fun onCancelPointerInput() = multiPointerDraggableNode.onCancelPointerInput() -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 998054ef6c9e..776d553ee49c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -40,7 +40,7 @@ interface SceneTransitionsBuilder { * The default [AnimationSpec] used when after the user lifts their finger after starting a * swipe to transition, to animate back into one of the 2 scenes we are transitioning to. */ - var defaultSwipeSpec: SpringSpec<Float> + var defaultMotionSpatialSpec: SpringSpec<Float> /** * The [InterruptionHandler] used when transitions are interrupted. Defaults to @@ -145,9 +145,9 @@ interface TransitionBuilder : BaseTransitionBuilder { * The [SpringSpec] used to animate the associated transition progress when the transition was * started by a swipe and is now animating back to a scene because the user lifted their finger. * - * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used. + * If `null`, then the [SceneTransitionsBuilder.defaultMotionSpatialSpec] will be used. */ - var swipeSpec: SpringSpec<Float>? + var motionSpatialSpec: SpringSpec<Float>? /** The CUJ associated to this transitions. */ @CujType var cuj: Int? diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 7ca521513714..9a9b05eb3c1d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -41,11 +41,15 @@ import com.android.internal.jank.Cuj.CujType internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { val impl = SceneTransitionsBuilderImpl().apply(builder) - return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs, impl.interruptionHandler) + return SceneTransitions( + defaultMotionSpatialSpec = impl.defaultMotionSpatialSpec, + transitionSpecs = impl.transitionSpecs, + interruptionHandler = impl.interruptionHandler, + ) } private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { - override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec + override var defaultMotionSpatialSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler val transitionSpecs = mutableListOf<TransitionSpecImpl>() @@ -105,7 +109,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { val impl = TransitionBuilderImpl(transition).apply(builder) return TransformationSpecImpl( progressSpec = impl.spec, - swipeSpec = impl.swipeSpec, + motionSpatialSpec = impl.motionSpatialSpec, distance = impl.distance, transformationMatchers = impl.transformationMatchers, ) @@ -209,7 +213,7 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { internal class TransitionBuilderImpl(override val transition: TransitionState.Transition) : BaseTransitionBuilderImpl(), TransitionBuilder { override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow) - override var swipeSpec: SpringSpec<Float>? = null + override var motionSpatialSpec: SpringSpec<Float>? = null override var distance: UserActionDistance? = null override var cuj: Int? = null private val durationMillis: Int by lazy { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt index 712af56ee1bc..f772f1a0b4a6 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt @@ -373,15 +373,6 @@ sealed interface TransitionState { } } - /** - * Checks if the given [progress] value is within the valid range for this transition. - * - * The valid range is between 0f and 1f, inclusive. - */ - internal fun isWithinProgressRange(progress: Float): Boolean { - return progress >= 0f && progress <= 1f - } - internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float { if (replacedTransition != null) { return replacedTransition.interruptionProgress(layoutImpl) @@ -390,10 +381,10 @@ sealed interface TransitionState { fun create(): Animatable<Float, AnimationVector1D> { val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) layoutImpl.animationScope.launch { - val swipeSpec = layoutImpl.state.transitions.defaultSwipeSpec + val motionSpatialSpec = layoutImpl.state.transitions.defaultMotionSpatialSpec val progressSpec = spring( - stiffness = swipeSpec.stiffness, + stiffness = motionSpatialSpec.stiffness, dampingRatio = Spring.DampingRatioNoBouncy, visibilityThreshold = ProgressVisibilityThreshold, ) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt index 7c4dbf153013..00cd0ca564b1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt @@ -104,7 +104,7 @@ fun TransitionBuilder.verticalContainerReveal( val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f) // The spring animating the progress when releasing the finger. - swipeSpec = + motionSpatialSpec = spring( stiffness = Spring.StiffnessMediumLow, dampingRatio = Spring.DampingRatioNoBouncy, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 5a35d11c0b29..4a0c330be4f8 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -38,9 +38,11 @@ import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.Transition import com.android.compose.animation.scene.subjects.assertThat +import com.android.compose.gesture.NestedDraggable import com.android.compose.test.MonotonicClockTestScope import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat +import kotlin.math.sign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async @@ -51,18 +53,6 @@ import org.junit.runner.RunWith private const val SCREEN_SIZE = 100f private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) -private fun pointersDown( - startedPosition: Offset = Offset.Zero, - pointersDown: Int = 1, - pointersDownByType: Map<PointerType, Int> = mapOf(PointerType.Touch to pointersDown), -): PointersInfo.PointersDown { - return PointersInfo.PointersDown( - startedPosition = startedPosition, - count = pointersDown, - countByType = pointersDownByType, - ) -} - @RunWith(AndroidJUnit4::class) class DraggableHandlerTest { private class TestGestureScope(val testScope: MonotonicClockTestScope) { @@ -123,6 +113,7 @@ class DraggableHandlerTest { density = Density(1f), layoutDirection = LayoutDirection.Ltr, swipeSourceDetector = DefaultEdgeDetector, + swipeDetector = DefaultSwipeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenesBuilder, @@ -134,16 +125,6 @@ class DraggableHandlerTest { val draggableHandler = layoutImpl.verticalDraggableHandler val horizontalDraggableHandler = layoutImpl.horizontalDraggableHandler - - var pointerInfoOwner: () -> PointersInfo = { pointersDown() } - - fun nestedScrollConnection() = - NestedScrollHandlerImpl( - draggableHandler = draggableHandler, - pointersInfoOwner = { pointerInfoOwner() }, - ) - .connection - val velocityThreshold = draggableHandler.velocityThreshold fun down(fractionOfScreen: Float) = @@ -211,68 +192,72 @@ class DraggableHandlerTest { } fun onDragStarted( - pointersInfo: PointersInfo.PointersDown = pointersDown(), overSlop: Float, + position: Offset = Offset.Zero, + pointersDown: Int = 1, + pointerType: PointerType? = PointerType.Touch, expectedConsumedOverSlop: Float = overSlop, - ): DragController { - // overSlop should be 0f only if the drag gesture starts with startDragImmediately - if (overSlop == 0f) error("Consider using onDragStartedImmediately()") + ): NestedDraggable.Controller { return onDragStarted( draggableHandler = draggableHandler, - pointersInfo = pointersInfo, overSlop = overSlop, + position = position, + pointersDown = pointersDown, + pointerType = pointerType, expectedConsumedOverSlop = expectedConsumedOverSlop, ) } fun onDragStarted( - draggableHandler: DraggableHandler, - pointersInfo: PointersInfo.PointersDown = pointersDown(), - overSlop: Float = 0f, + draggableHandler: NestedDraggable, + overSlop: Float, + position: Offset = Offset.Zero, + pointersDown: Int = 1, + pointerType: PointerType? = PointerType.Touch, expectedConsumedOverSlop: Float = overSlop, - ): DragController { + ): NestedDraggable.Controller { + // overSlop should be 0f only if the drag gesture starts with startDragImmediately. + if (overSlop == 0f) error("Consider using onDragStartedImmediately()") + val dragController = - draggableHandler.onDragStarted(pointersDown = pointersInfo, overSlop = overSlop) + draggableHandler.onDragStarted(position, overSlop.sign, pointersDown, pointerType) - // MultiPointerDraggable will always call onDelta with the initial overSlop right after + // MultiPointerDraggable will always call onDelta with the initial overSlop right after. dragController.onDragDelta(pixels = overSlop, expectedConsumedOverSlop) return dragController } - fun DragController.onDragDelta(pixels: Float, expectedConsumed: Float = pixels) { + fun NestedDraggable.Controller.onDragDelta( + pixels: Float, + expectedConsumed: Float = pixels, + ) { val consumed = onDrag(delta = pixels) assertThat(consumed).isEqualTo(expectedConsumed) } - suspend fun DragController.onDragStoppedAnimateNow( + suspend fun NestedDraggable.Controller.onDragStoppedAnimateNow( velocity: Float, - canChangeScene: Boolean = true, onAnimationStart: () -> Unit, onAnimationEnd: (Float) -> Unit, ) { - val velocityConsumed = onDragStoppedAnimateLater(velocity, canChangeScene) + val velocityConsumed = onDragStoppedAnimateLater(velocity) onAnimationStart() onAnimationEnd(velocityConsumed.await()) } - suspend fun DragController.onDragStoppedAnimateNow( + suspend fun NestedDraggable.Controller.onDragStoppedAnimateNow( velocity: Float, - canChangeScene: Boolean = true, onAnimationStart: () -> Unit, ) = onDragStoppedAnimateNow( velocity = velocity, - canChangeScene = canChangeScene, onAnimationStart = onAnimationStart, onAnimationEnd = {}, ) - fun DragController.onDragStoppedAnimateLater( - velocity: Float, - canChangeScene: Boolean = true, - ): Deferred<Float> { - val velocityConsumed = testScope.async { onStop(velocity, canChangeScene) } + fun NestedDraggable.Controller.onDragStoppedAnimateLater(velocity: Float): Deferred<Float> { + val velocityConsumed = testScope.async { onDragStopped(velocity, awaitFling = {}) } testScope.testScheduler.runCurrent() return velocityConsumed } @@ -421,12 +406,13 @@ class DraggableHandlerTest { } @Test - fun onDragIntoNoAction_stayIdle() = runGestureTest { + fun onDragIntoNoAction_overscrolls() = runGestureTest { navigateToSceneC() - // We are on SceneC which has no action in Down direction + // We are on SceneC which has no action in Down direction, we still start a transition so + // that we can overscroll on that scene. onDragStarted(overSlop = 10f, expectedConsumedOverSlop = 0f) - assertIdle(currentScene = SceneC) + assertTransition(fromScene = SceneC, toScene = SceneB, progress = 0f) } @Test @@ -435,8 +421,7 @@ class DraggableHandlerTest { mutableUserActionsA = mapOf(Swipe.Up to UserActionResult(SceneB), Swipe.Down to SceneC) val dragController = onDragStarted( - pointersInfo = - pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f)), + position = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE * 0.5f), overSlop = up(fractionOfScreen = 0.2f), ) assertTransition( @@ -460,7 +445,7 @@ class DraggableHandlerTest { // Start dragging from the bottom onDragStarted( - pointersInfo = pointersDown(startedPosition = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE)), + position = Offset(SCREEN_SIZE * 0.5f, SCREEN_SIZE), overSlop = up(fractionOfScreen = 0.1f), ) assertTransition( @@ -561,8 +546,7 @@ class DraggableHandlerTest { navigateToSceneC() // Swipe up from the middle to transition to scene B. - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - onDragStarted(pointersInfo = middle, overSlop = up(0.1f)) + onDragStarted(position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), overSlop = up(0.1f)) assertTransition(fromScene = SceneC, toScene = SceneB, isUserInputOngoing = true) // Freeze the transition. @@ -613,61 +597,13 @@ class DraggableHandlerTest { } @Test - fun nestedScrollUseFromSourceInfo() = runGestureTest { - // Start at scene C. - navigateToSceneC() - val nestedScroll = nestedScrollConnection() - - // Drag from the **top** of the screen - pointerInfoOwner = { pointersDown() } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - // userAction: Swipe.Up to SceneB - toScene = SceneB, - progress = 0.1f, - ) - - // Reset to SceneC - nestedScroll.preFling(Velocity.Zero) - advanceUntilIdle() - - // Drag from the **bottom** of the screen - pointerInfoOwner = { pointersDown(startedPosition = Offset(0f, SCREEN_SIZE)) } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition( - currentScene = SceneC, - fromScene = SceneC, - // userAction: Swipe.Up(fromSource = Edge.Bottom) to SceneA - toScene = SceneA, - progress = 0.1f, - ) - } - - @Test - fun ignoreMouseWheel() = runGestureTest { - // Start at scene C. - navigateToSceneC() - val nestedScroll = nestedScrollConnection() - - // Use mouse wheel - pointerInfoOwner = { PointersInfo.MouseWheel } - assertIdle(currentScene = SceneC) - - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertIdle(currentScene = SceneC) - } - - @Test fun transitionIsImmediatelyUpdatedWhenReleasingFinger() = runGestureTest { // Swipe up from the middle to transition to scene B. - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.1f)) + val dragController = + onDragStarted( + position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), + overSlop = up(0.1f), + ) assertTransition(fromScene = SceneA, toScene = SceneB, isUserInputOngoing = true) dragController.onDragStoppedAnimateLater(velocity = 0f) @@ -677,8 +613,11 @@ class DraggableHandlerTest { @Test fun emptyOverscrollAbortsSettleAnimationAndExposeTheConsumedVelocity() = runGestureTest { // Swipe up to scene B at progress = 200%. - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.99f)) + val dragController = + onDragStarted( + position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), + overSlop = up(0.99f), + ) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.99f) // Release the finger. @@ -695,34 +634,18 @@ class DraggableHandlerTest { } @Test - fun scrollKeepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() = runGestureTest { - val nestedScroll = nestedScrollConnection() - - // Overscroll is disabled, it will scroll up to 100% - nestedScroll.scroll(available = upOffset(fractionOfScreen = 2f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) - - // We need to maintain scroll priority even if the scene transition can no longer consume - // the scroll gesture. - nestedScroll.scroll(available = upOffset(fractionOfScreen = 0.1f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f) - - // A scroll gesture in the opposite direction allows us to return to the previous scene. - nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.5f)) - assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.5f) - } - - @Test fun overscroll_releaseBetween0And100Percent_up() = runGestureTest { // Make scene B overscrollable. layoutState.transitions = transitions { - defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) + defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) from(SceneA, to = SceneB) {} } - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - - val dragController = onDragStarted(pointersInfo = middle, overSlop = up(0.5f)) + val dragController = + onDragStarted( + position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), + overSlop = up(0.5f), + ) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneB) @@ -745,13 +668,15 @@ class DraggableHandlerTest { fun overscroll_releaseBetween0And100Percent_down() = runGestureTest { // Make scene C overscrollable. layoutState.transitions = transitions { - defaultSwipeSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) + defaultMotionSpatialSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy) from(SceneA, to = SceneC) {} } - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - - val dragController = onDragStarted(pointersInfo = middle, overSlop = down(0.5f)) + val dragController = + onDragStarted( + position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), + overSlop = down(0.5f), + ) val transition = assertThat(transitionState).isSceneTransition() assertThat(transition).hasFromScene(SceneA) assertThat(transition).hasToScene(SceneC) @@ -777,11 +702,9 @@ class DraggableHandlerTest { from(SceneA, to = SceneB) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } } - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted( - pointersInfo = middle, + position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), overSlop = up(1.5f), expectedConsumedOverSlop = up(1f), ) @@ -811,11 +734,9 @@ class DraggableHandlerTest { from(SceneA, to = SceneC) { spec = spring(dampingRatio = Spring.DampingRatioNoBouncy) } } - val middle = pointersDown(startedPosition = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f)) - val dragController = onDragStarted( - pointersInfo = middle, + position = Offset(SCREEN_SIZE / 2f, SCREEN_SIZE / 2f), overSlop = down(1.5f), expectedConsumedOverSlop = down(1f), ) @@ -950,33 +871,4 @@ class DraggableHandlerTest { assertThat(layoutState.transitionState).hasCurrentScene(SceneA) assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB) } - - @Test - fun replaceOverlayNestedScroll() = runGestureTest { - layoutState.showOverlay(OverlayA, animationScope = testScope) - advanceUntilIdle() - - // Initial state. - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayA) - - // Swipe down to replace overlay A by overlay B. - - val nestedScroll = nestedScrollConnection() - nestedScroll.scroll(downOffset(0.1f)) - val transition = assertThat(layoutState.transitionState).isReplaceOverlayTransition() - assertThat(transition).hasCurrentScene(SceneA) - assertThat(transition).hasFromOverlay(OverlayA) - assertThat(transition).hasToOverlay(OverlayB) - assertThat(transition).hasCurrentOverlays(OverlayA) - assertThat(transition).hasProgress(0.1f) - - nestedScroll.preFling(Velocity(0f, velocityThreshold)) - advanceUntilIdle() - // Commit the gesture. The overlays are instantly swapped in the set of current overlays. - assertThat(layoutState.transitionState).isIdle() - assertThat(layoutState.transitionState).hasCurrentScene(SceneA) - assertThat(layoutState.transitionState).hasCurrentOverlays(OverlayB) - } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 53495be7b02a..005146997813 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -618,7 +618,7 @@ class ElementTest { fun layoutGetsCurrentTransitionStateFromComposition() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { @@ -1126,7 +1126,7 @@ class ElementTest { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } @@ -1331,7 +1331,7 @@ class ElementTest { val fooSize = 100.dp val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { spec = tween(duration, easing = LinearEasing) } @@ -1439,7 +1439,7 @@ class ElementTest { @Test fun targetStateIsSetEvenWhenNotPlaced() { // Start directly at A => B but with progress < 0f to overscroll on A. - val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } lateinit var layoutImpl: SceneTransitionLayoutImpl val scope = @@ -1473,7 +1473,7 @@ class ElementTest { fun lastAlphaIsNotSetByOutdatedLayer() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }, ) @@ -1537,7 +1537,7 @@ class ElementTest { fun fadingElementsDontAppearInstantly() { val state = rule.runOnUiThread { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } }, ) @@ -1583,7 +1583,7 @@ class ElementTest { @Test fun lastPlacementValuesAreClearedOnNestedElements() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.NestedFooBar() { @@ -1658,7 +1658,7 @@ class ElementTest { fun currentTransitionSceneIsUsedToComputeElementValues() { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneB, to = SceneC) { @@ -1709,7 +1709,7 @@ class ElementTest { @Test fun interruptionDeltasAreProperlyCleaned() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.Foo(offset: Dp) { @@ -1780,7 +1780,7 @@ class ElementTest { fun transparentElementIsNotImpactingInterruption() { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( SceneA, transitions { from(SceneA, to = SceneB) { @@ -1856,7 +1856,7 @@ class ElementTest { @Test fun replacedTransitionDoesNotTriggerInterruption() { - val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + val state = rule.runOnIdle { MutableSceneTransitionLayoutState(SceneA) } @Composable fun ContentScope.Foo(modifier: Modifier = Modifier) { @@ -2027,7 +2027,7 @@ class ElementTest { ): SceneTransitionLayoutImpl { val state = rule.runOnIdle { - MutableSceneTransitionLayoutStateImpl( + MutableSceneTransitionLayoutState( from, transitions { from(from, to = to, preview = preview, builder = transition) }, ) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt deleted file mode 100644 index 4153350fce60..000000000000 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ /dev/null @@ -1,866 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.compose.animation.scene - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.rememberScrollableState -import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.AwaitPointerEventScope -import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.test.TouchInjectionScope -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onRoot -import androidx.compose.ui.test.performTouchInput -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Velocity -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.compose.modifiers.thenIf -import com.google.common.truth.Truth.assertThat -import kotlin.properties.Delegates -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.isActive -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class MultiPointerDraggableTest { - @get:Rule val rule = createComposeRule() - - private val emptyConnection = object : NestedScrollConnection {} - private val defaultDispatcher = NestedScrollDispatcher() - - private fun Modifier.nestedScrollDispatcher() = nestedScroll(emptyConnection, defaultDispatcher) - - private class SimpleDragController( - val onDrag: (delta: Float) -> Unit, - val onStop: (velocity: Float) -> Unit, - ) : DragController { - override fun onDrag(delta: Float): Float { - onDrag.invoke(delta) - return delta - } - - override suspend fun onStop(velocity: Float, canChangeContent: Boolean): Float { - onStop.invoke(velocity) - return velocity - } - - override fun onCancel(canChangeContent: Boolean) { - error("MultiPointerDraggable never calls onCancel()") - } - } - - @Test - fun cancellingPointerCallsOnDragStopped() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var enabled by mutableStateOf(false) - var started = false - var dragged = false - var stopped = false - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .thenIf(enabled) { - Modifier.multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - started = true - SimpleDragController( - onDrag = { dragged = true }, - onStop = { stopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) - } - ) - } - - fun startDraggingDown() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun releaseFinger() { - rule.onRoot().performTouchInput { up() } - } - - // Swiping down does nothing because enabled is false. - startDraggingDown() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - releaseFinger() - - // Enable the draggable and swipe down. This should both call onDragStarted() and - // onDragDelta(). - enabled = true - rule.waitForIdle() - startDraggingDown() - assertThat(started).isTrue() - assertThat(dragged).isTrue() - assertThat(stopped).isFalse() - - // Disable the pointer input. This should call onDragStopped() even if didn't release the - // finger yet. - enabled = false - rule.waitForIdle() - assertThat(started).isTrue() - assertThat(dragged).isTrue() - assertThat(stopped).isTrue() - } - - @Test - fun shouldNotStartDragEventsWith0PointersDown() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var started = false - var dragged = false - var stopped = false - var consumedByDescendant = false - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - started = true - SimpleDragController( - onDrag = { dragged = true }, - onStop = { stopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) - .pointerInput(Unit) { - coroutineScope { - awaitPointerEventScope { - while (isActive) { - val change = awaitPointerEvent().changes.first() - if (consumedByDescendant) { - change.consume() - } - } - } - } - } - ) - } - - // The first part of the gesture is consumed by our descendant - consumedByDescendant = true - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - - // The events were consumed by our descendant, we should not start a drag gesture. - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - // The next events could be consumed by us - consumedByDescendant = false - rule.onRoot().performTouchInput { - // The pointer is moved to a new position without reporting it - updatePointerBy(0, Offset(0f, touchSlop)) - - // The pointer report an "up" (0 pointers down) with a new position - up() - } - - // The "up" event should not be used to start a drag gesture - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - } - - @Test - fun handleDisappearingScrollableDuringAGesture() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var started = false - var dragged = false - var stopped = false - var consumedByScroll = false - var hasScrollable by mutableStateOf(true) - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - started = true - SimpleDragController( - onDrag = { dragged = true }, - onStop = { stopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) { - if (hasScrollable) { - Box( - Modifier.scrollable( - // Consume all the vertical scroll gestures - rememberScrollableState( - consumeScrollDelta = { - consumedByScroll = true - it - } - ), - Orientation.Vertical, - ) - .fillMaxSize() - ) - } - } - } - - fun startDraggingDown() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun continueDraggingDown() { - rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } - } - - fun releaseFinger() { - rule.onRoot().performTouchInput { up() } - } - - // Swipe down. This should intercepted by the scrollable modifier. - startDraggingDown() - assertThat(consumedByScroll).isTrue() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - // Reset the scroll state for the test - consumedByScroll = false - - // Suddenly remove the scrollable container - hasScrollable = false - rule.waitForIdle() - - // Swipe down. This will be intercepted by multiPointerDraggable, it will wait touchSlop - // before consuming it. - continueDraggingDown() - assertThat(consumedByScroll).isFalse() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - // Swipe down. This should both call onDragStarted() and onDragDelta(). - continueDraggingDown() - assertThat(consumedByScroll).isFalse() - assertThat(started).isTrue() - assertThat(dragged).isTrue() - assertThat(stopped).isFalse() - - rule.waitForIdle() - releaseFinger() - assertThat(stopped).isTrue() - } - - @Test - fun multiPointerWaitAConsumableEventInMainPass() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var started = false - var dragged = false - var stopped = false - - var childConsumesOnPass: PointerEventPass? = null - - suspend fun AwaitPointerEventScope.childPointerInputScope() { - awaitPointerEvent(PointerEventPass.Initial).also { initial -> - // Check unconsumed: it should be always true - assertThat(initial.changes.any { it.isConsumed }).isFalse() - - if (childConsumesOnPass == PointerEventPass.Initial) { - initial.changes.first().consume() - } - } - - awaitPointerEvent(PointerEventPass.Main).also { main -> - // Check unconsumed - if (childConsumesOnPass != PointerEventPass.Initial) { - assertThat(main.changes.any { it.isConsumed }).isFalse() - } - - if (childConsumesOnPass == PointerEventPass.Main) { - main.changes.first().consume() - } - } - } - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - started = true - SimpleDragController( - onDrag = { dragged = true }, - onStop = { stopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) { - Box( - Modifier.pointerInput(Unit) { - coroutineScope { - awaitPointerEventScope { - while (isActive) { - childPointerInputScope() - } - } - } - } - .fillMaxSize() - ) - } - } - - fun startDraggingDown() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun continueDraggingDown() { - rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } - } - - childConsumesOnPass = PointerEventPass.Initial - - startDraggingDown() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - continueDraggingDown() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - childConsumesOnPass = PointerEventPass.Main - - continueDraggingDown() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - continueDraggingDown() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - childConsumesOnPass = null - - // Swipe down. This will be intercepted by multiPointerDraggable, it will wait touchSlop - // before consuming it. - continueDraggingDown() - assertThat(started).isFalse() - assertThat(dragged).isFalse() - assertThat(stopped).isFalse() - - // Swipe down. This should both call onDragStarted() and onDragDelta(). - continueDraggingDown() - assertThat(started).isTrue() - assertThat(dragged).isTrue() - assertThat(stopped).isFalse() - - childConsumesOnPass = PointerEventPass.Main - - continueDraggingDown() - assertThat(stopped).isTrue() - - // Complete the gesture - rule.onRoot().performTouchInput { up() } - } - - @Test - fun multiPointerDuringAnotherGestureWaitAConsumableEventAfterMainPass() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var verticalStarted = false - var verticalDragged = false - var verticalStopped = false - var horizontalStarted = false - var horizontalDragged = false - var horizontalStopped = false - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - verticalStarted = true - SimpleDragController( - onDrag = { verticalDragged = true }, - onStop = { verticalStopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) - .multiPointerDraggable( - orientation = Orientation.Horizontal, - onDragStarted = { _, _ -> - horizontalStarted = true - SimpleDragController( - onDrag = { horizontalDragged = true }, - onStop = { horizontalStopped = true }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) - } - - fun startDraggingDown() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun startDraggingRight() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(touchSlop, 0f)) - } - } - - fun stopDragging() { - rule.onRoot().performTouchInput { up() } - } - - fun continueDown() { - rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } - } - - fun continueRight() { - rule.onRoot().performTouchInput { moveBy(Offset(touchSlop, 0f)) } - } - - startDraggingDown() - assertThat(verticalStarted).isTrue() - assertThat(verticalDragged).isTrue() - assertThat(verticalStopped).isFalse() - - // Ignore right swipe, do not interrupt the dragging gesture. - continueRight() - assertThat(horizontalStarted).isFalse() - assertThat(horizontalDragged).isFalse() - assertThat(horizontalStopped).isFalse() - assertThat(verticalStopped).isFalse() - - stopDragging() - assertThat(verticalStopped).isTrue() - - verticalStarted = false - verticalDragged = false - verticalStopped = false - - startDraggingRight() - assertThat(horizontalStarted).isTrue() - assertThat(horizontalDragged).isTrue() - assertThat(horizontalStopped).isFalse() - - // Ignore down swipe, do not interrupt the dragging gesture. - continueDown() - assertThat(verticalStarted).isFalse() - assertThat(verticalDragged).isFalse() - assertThat(verticalStopped).isFalse() - assertThat(horizontalStopped).isFalse() - - stopDragging() - assertThat(horizontalStopped).isTrue() - } - - @Test - fun multiPointerSwipeDetectorInteraction() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var started = false - - var capturedChange: PointerInputChange? = null - var swipeConsume = false - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - swipeDetector = - object : SwipeDetector { - override fun detectSwipe(change: PointerInputChange): Boolean { - capturedChange = change - return swipeConsume - } - }, - onDragStarted = { _, _ -> - started = true - SimpleDragController( - onDrag = { /* do nothing */ }, - onStop = { /* do nothing */ }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) {} - } - - fun startDraggingDown() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun dragDown() { - rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } - } - - startDraggingDown() - assertThat(capturedChange).isNotNull() - capturedChange = null - assertThat(started).isFalse() - - swipeConsume = true - // Drag in same direction - dragDown() - assertThat(capturedChange).isNotNull() - capturedChange = null - - dragDown() - assertThat(capturedChange).isNull() - - assertThat(started).isTrue() - } - - @Test - fun multiPointerSwipeDetectorInteractionZeroOffsetFromStartPosition() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var started = false - - var capturedChange: PointerInputChange? = null - var swipeConsume = false - - var touchSlop = 0f - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - swipeDetector = - object : SwipeDetector { - override fun detectSwipe(change: PointerInputChange): Boolean { - capturedChange = change - return swipeConsume - } - }, - onDragStarted = { _, _ -> - started = true - SimpleDragController( - onDrag = { /* do nothing */ }, - onStop = { /* do nothing */ }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) {} - } - - fun startDraggingDown() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun dragUp() { - rule.onRoot().performTouchInput { moveBy(Offset(0f, -touchSlop)) } - } - - startDraggingDown() - assertThat(capturedChange).isNotNull() - capturedChange = null - assertThat(started).isFalse() - - swipeConsume = true - // Drag in the opposite direction - dragUp() - assertThat(capturedChange).isNotNull() - capturedChange = null - - dragUp() - assertThat(capturedChange).isNull() - - assertThat(started).isTrue() - } - - @Test - fun multiPointerNestedScrollDispatcher() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - var touchSlop = 0f - - var consumedOnPreScroll = 0f - - var availableOnPreScroll = Float.MIN_VALUE - var availableOnPostScroll = Float.MIN_VALUE - var availableOnPreFling = Float.MIN_VALUE - var availableOnPostFling = Float.MIN_VALUE - - var consumedOnDrag = 0f - var consumedOnDragStop = 0f - - val connection = - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - availableOnPreScroll = available.y - return Offset(0f, consumedOnPreScroll) - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset { - availableOnPostScroll = available.y - return Offset.Zero - } - - override suspend fun onPreFling(available: Velocity): Velocity { - availableOnPreFling = available.y - return Velocity.Zero - } - - override suspend fun onPostFling( - consumed: Velocity, - available: Velocity, - ): Velocity { - availableOnPostFling = available.y - return Velocity.Zero - } - } - - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - Box( - Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) - .nestedScroll(connection) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - SimpleDragController( - onDrag = { consumedOnDrag = it }, - onStop = { consumedOnDragStop = it }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) - } - - fun startDrag() { - rule.onRoot().performTouchInput { - down(middle) - moveBy(Offset(0f, touchSlop)) - } - } - - fun continueDrag() { - rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } - } - - fun stopDrag() { - rule.onRoot().performTouchInput { up() } - } - - startDrag() - - continueDrag() - assertThat(availableOnPreScroll).isEqualTo(touchSlop) - assertThat(consumedOnDrag).isEqualTo(touchSlop) - assertThat(availableOnPostScroll).isEqualTo(0f) - - // Parent node consumes half of the gesture - consumedOnPreScroll = touchSlop / 2f - continueDrag() - assertThat(availableOnPreScroll).isEqualTo(touchSlop) - assertThat(consumedOnDrag).isEqualTo(touchSlop / 2f) - assertThat(availableOnPostScroll).isEqualTo(0f) - - // Parent node consumes the gesture - consumedOnPreScroll = touchSlop - continueDrag() - assertThat(availableOnPreScroll).isEqualTo(touchSlop) - assertThat(consumedOnDrag).isEqualTo(0f) - assertThat(availableOnPostScroll).isEqualTo(0f) - - // Parent node can intercept the velocity on stop - stopDrag() - assertThat(availableOnPreFling).isEqualTo(consumedOnDragStop) - assertThat(availableOnPostFling).isEqualTo(0f) - } - - @Test - fun multiPointerOnStopVelocity() { - val size = 200f - val middle = Offset(size / 2f, size / 2f) - - var stopped = false - var lastVelocity = -1f - var touchSlop = 0f - var density: Density by Delegates.notNull() - rule.setContent { - touchSlop = LocalViewConfiguration.current.touchSlop - density = LocalDensity.current - Box( - Modifier.size(with(density) { Size(size, size).toDpSize() }) - .nestedScrollDispatcher() - .multiPointerDraggable( - orientation = Orientation.Vertical, - onDragStarted = { _, _ -> - SimpleDragController( - onDrag = { /* do nothing */ }, - onStop = { - stopped = true - lastVelocity = it - }, - ) - }, - dispatcher = defaultDispatcher, - ) - ) - } - - var eventMillis: Long by Delegates.notNull() - rule.onRoot().performTouchInput { eventMillis = eventPeriodMillis } - - fun swipeGesture(block: TouchInjectionScope.() -> Unit) { - stopped = false - rule.onRoot().performTouchInput { - down(middle) - block() - up() - } - assertThat(stopped).isEqualTo(true) - } - - val shortDistance = touchSlop / 2f - swipeGesture { - moveBy(delta = Offset(0f, shortDistance), delayMillis = eventMillis) - moveBy(delta = Offset(0f, shortDistance), delayMillis = eventMillis) - } - assertThat(lastVelocity).isGreaterThan(0f) - assertThat(lastVelocity).isWithin(1f).of((shortDistance / eventMillis) * 1000f) - - val longDistance = touchSlop * 4f - swipeGesture { - moveBy(delta = Offset(0f, longDistance), delayMillis = eventMillis) - moveBy(delta = Offset(0f, longDistance), delayMillis = eventMillis) - } - assertThat(lastVelocity).isGreaterThan(0f) - assertThat(lastVelocity).isWithin(1f).of((longDistance / eventMillis) * 1000f) - - rule.onRoot().performTouchInput { - down(pointerId = 0, position = middle) - down(pointerId = 1, position = middle) - moveBy(pointerId = 0, delta = Offset(0f, longDistance), delayMillis = eventMillis) - moveBy(pointerId = 0, delta = Offset(0f, longDistance), delayMillis = eventMillis) - // The velocity should be: - // (longDistance / eventMillis) pixels/ms - - // 1 pointer left, the second one - up(pointerId = 0) - - // After a few events the velocity should be: - // (shortDistance / eventMillis) pixels/ms - repeat(10) { - moveBy(pointerId = 1, delta = Offset(0f, shortDistance), delayMillis = eventMillis) - } - up(pointerId = 1) - } - assertThat(lastVelocity).isGreaterThan(0f) - assertThat(lastVelocity).isWithin(1f).of((shortDistance / eventMillis) * 1000f) - } -} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 9135fdd15b3a..e03608466dae 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ScrollWheel +import androidx.compose.ui.test.TouchInjectionScope import androidx.compose.ui.test.assertPositionInRootIsEqualTo import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.junit4.createComposeRule @@ -55,6 +56,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestOverlays.OverlayA +import com.android.compose.animation.scene.TestOverlays.OverlayB import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC @@ -125,7 +128,7 @@ class SwipeToSceneTest { mapOf( Swipe.Down to SceneA, Swipe.Down(pointerCount = 2) to SceneB, - Swipe.Down(pointersType = PointerType.Mouse) to SceneD, + Swipe.Down(pointerType = PointerType.Mouse) to SceneD, Swipe.Down(fromSource = Edge.Top) to SceneB, Swipe.Right(fromSource = Edge.Left) to SceneB, ) @@ -936,4 +939,205 @@ class SwipeToSceneTest { assertThat(state.transitionState).isIdle() assertThat(state.transitionState).hasCurrentScene(SceneC) } + + @Test + fun swipeToSceneNodeIsKeptWhenDisabled() { + var hasHorizontalActions by mutableStateOf(false) + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene( + SceneA, + userActions = + buildList { + add(Swipe.Down to SceneB) + + if (hasHorizontalActions) { + add(Swipe.Left to SceneC) + } + } + .toMap(), + ) { + Box(Modifier.fillMaxSize()) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + // Swipe down to start a transition to B. + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + + assertThat(state.transitionState).isSceneTransition() + + // Add new horizontal user actions. This should not stop the current transition, even if a + // new horizontal Modifier.swipeToScene() handler is introduced where the vertical one was. + hasHorizontalActions = true + rule.waitForIdle() + assertThat(state.transitionState).isSceneTransition() + } + + @Test + fun nestedScroll_useFromSourceInfo() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene( + SceneA, + userActions = + mapOf(Swipe.Down to SceneB, Swipe.Down(fromSource = Edge.Top) to SceneC), + ) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + scene(SceneC) { Box(Modifier.fillMaxSize()) } + } + } + + // Swiping down from the middle of the screen leads to B. + rule.onRoot().performTouchInput { + down(center) + moveBy(Offset(0f, touchSlop + 1f)) + } + + var transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + + // Release finger and wait to settle back to A. + rule.onRoot().performTouchInput { up() } + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + + // Swiping down from the top of the screen leads to B. + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + 1f)) + } + + transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneC) + } + + @Test + fun nestedScroll_ignoreMouseWheel() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + rule.onRoot().performMouseInput { + scroll(-touchSlop - 1f, scrollWheel = ScrollWheel.Vertical) + } + assertThat(state.transitionState).isIdle() + } + + @Test + fun nestedScroll_keepPriorityEvenIfWeCanNoLongerScrollOnThatDirection() { + val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA, userActions = mapOf(Swipe.Down to SceneB)) { + // Use a fullscreen nested scrollable to use the nested scroll connection. + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + scene(SceneB) { Box(Modifier.fillMaxSize()) } + } + } + + fun TouchInjectionScope.height() = bottom + fun TouchInjectionScope.halfHeight() = height() / 2f + + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + halfHeight())) + } + val transition = assertThat(state.transitionState).isSceneTransition() + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) + + // The progress should never go above 100%. + rule.onRoot().performTouchInput { moveBy(Offset(0f, height())) } + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + // Because the overscroll effect of scene B is not attached, swiping in the opposite + // direction will directly decrease the progress. + rule.onRoot().performTouchInput { moveBy(Offset(0f, -halfHeight())) } + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) + } + + @Test + fun nestedScroll_replaceOverlay() { + val state = + rule.runOnUiThread { + MutableSceneTransitionLayoutState(SceneA, initialOverlays = setOf(OverlayA)) + } + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + SceneTransitionLayout(state) { + scene(SceneA) { Box(Modifier.fillMaxSize()) } + overlay( + OverlayA, + mapOf(Swipe.Down to UserActionResult.ReplaceByOverlay(OverlayB)), + ) { + Box( + Modifier.fillMaxSize() + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical) + ) + } + overlay(OverlayB) { Box(Modifier.fillMaxSize()) } + } + } + + // Swipe down 100% to replace A by B. + rule.onRoot().performTouchInput { + down(center.copy(y = 0f)) + moveBy(Offset(0f, touchSlop + bottom)) + } + + val transition = assertThat(state.transitionState).isReplaceOverlayTransition() + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasFromOverlay(OverlayA) + assertThat(transition).hasToOverlay(OverlayB) + assertThat(transition).hasCurrentOverlays(OverlayA) + assertThat(transition).hasProgress(1f, tolerance = 0.01f) + + // Commit the gesture. The overlays are instantly swapped in the set of current overlays. + rule.onRoot().performTouchInput { up() } + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasCurrentOverlays(OverlayB) + + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) + assertThat(state.transitionState).hasCurrentOverlays(OverlayB) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index cb87fe849a81..aada4a50c89c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -276,22 +276,22 @@ class TransitionDslTest { val defaultSpec = spring<Float>(stiffness = 1f) val specFromAToC = spring<Float>(stiffness = 2f) val transitions = transitions { - defaultSwipeSpec = defaultSpec + defaultMotionSpatialSpec = defaultSpec from(SceneA, to = SceneB) { // Default swipe spec. } - from(SceneA, to = SceneC) { swipeSpec = specFromAToC } + from(SceneA, to = SceneC) { motionSpatialSpec = specFromAToC } } - assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec) + assertThat(transitions.defaultMotionSpatialSpec).isSameInstanceAs(defaultSpec) // A => B does not have a custom spec. assertThat( transitions .transitionSpec(from = SceneA, to = SceneB, key = null) .transformationSpec(aToB()) - .swipeSpec + .motionSpatialSpec ) .isNull() @@ -300,7 +300,7 @@ class TransitionDslTest { transitions .transitionSpec(from = SceneA, to = SceneC, key = null) .transformationSpec(transition(from = SceneA, to = SceneC)) - .swipeSpec + .motionSpatialSpec ) .isSameInstanceAs(specFromAToC) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index aed3a2df0436..e69fa994931d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -50,7 +50,6 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController DEFAULT_CLOCK_ID, clockCtx.resources.getString(R.string.clock_default_name), clockCtx.resources.getString(R.string.clock_default_description), - isReactiveToTone = true, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 266591028efb..6edf94939010 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -65,7 +65,7 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(); + final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, mockSecureSettings, mHearingAidDeviceManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index 241da5fbc444..15afd2559d9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -71,7 +71,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { private AccessibilityManager mAccessibilityManager; @Mock private HearingAidDeviceManager mHearingAidDeviceManager; - private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext); private RecyclerView mStubListView; private MenuView mMenuView; private MenuViewLayer mMenuViewLayer; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index 715c40a31632..56a97bb34172 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -89,7 +89,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index cb7c205742fc..737170f6a665 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -18,17 +18,25 @@ package com.android.systemui.accessibility.floatingmenu; import static android.app.UiModeManager.MODE_NIGHT_YES; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.UiModeManager; +import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.TestableLooper; import android.view.WindowManager; @@ -37,6 +45,8 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.accessibility.common.ShortcutConstants; +import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.settingslib.bluetooth.HearingAidDeviceManager; import com.android.systemui.Flags; import com.android.systemui.Prefs; @@ -54,6 +64,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; +import java.util.List; + /** Tests for {@link MenuView}. */ @RunWith(AndroidJUnit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -63,17 +76,24 @@ public class MenuViewTest extends SysuiTestCase { private int mNightMode; private UiModeManager mUiModeManager; private MenuView mMenuView; + private MenuView mMenuViewSpy; private String mLastPosition; private MenuViewAppearance mStubMenuViewAppearance; + private MenuViewModel mMenuViewModel; + private final List<String> mShortcutTargets = new ArrayList<>(); @Rule public MockitoRule mockito = MockitoJUnit.rule(); @Mock private AccessibilityManager mAccessibilityManager; + @Mock private HearingAidDeviceManager mHearingAidDeviceManager; + @Mock + private MenuView.OnTargetFeaturesChangeListener mOnTargetFeaturesChangeListener; + private SysuiTestableContext mSpyContext; @Before @@ -91,22 +111,38 @@ public class MenuViewTest extends SysuiTestCase { mSpyContext = spy(mContext); doNothing().when(mSpyContext).startActivity(any()); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); - final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - secureSettings, mHearingAidDeviceManager); + mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); + mShortcutTargets.add(MAGNIFICATION_CONTROLLER_NAME); + doReturn(mShortcutTargets) + .when(mAccessibilityManager) + .getAccessibilityShortcutTargets(anyInt()); + + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); + mMenuViewModel = + new MenuViewModel( + mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); mStubMenuViewAppearance = new MenuViewAppearance(mSpyContext, stubWindowManager); - mMenuView = spy(new MenuView(mSpyContext, stubMenuViewModel, mStubMenuViewAppearance, - secureSettings)); + mMenuView = + new MenuView(mSpyContext, mMenuViewModel, mStubMenuViewAppearance, secureSettings); + mMenuView.setOnTargetFeaturesChangeListener(mOnTargetFeaturesChangeListener); mLastPosition = Prefs.getString(mSpyContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); + + mMenuViewSpy = + spy( + new MenuView( + mSpyContext, + mMenuViewModel, + mStubMenuViewAppearance, + secureSettings)); } @Test public void onConfigurationChanged_updateViewModel() { - mMenuView.onConfigurationChanged(/* newConfig= */ null); + mMenuViewSpy.onConfigurationChanged(/* newConfig= */ null); - verify(mMenuView).loadLayoutResources(); + verify(mMenuViewSpy).loadLayoutResources(); } @Test @@ -179,6 +215,75 @@ public class MenuViewTest extends SysuiTestCase { assertThat(radiiAnimator.isStarted()).isTrue(); } + @Test + @DisableFlags(Flags.FLAG_FLOATING_MENU_NOTIFY_TARGETS_CHANGED_ON_STRICT_DIFF) + public void onTargetFeaturesChanged_listenerCalled_flagDisabled() { + // Call show() to start observing the target features change listener. + mMenuView.show(); + + // The target features change listener should be called when the observer is added. + verify(mOnTargetFeaturesChangeListener, times(1)).onChange(any()); + + // When the target features list changes, the listener should be called. + mMenuViewModel.onTargetFeaturesChanged( + List.of( + new TestAccessibilityTarget(mContext, 123), + new TestAccessibilityTarget(mContext, 456))); + verify(mOnTargetFeaturesChangeListener, times(2)).onChange(any()); + + // Double check that when the target features list changes, the listener should be called. + List<AccessibilityTarget> newFeaturesList = + List.of( + new TestAccessibilityTarget(mContext, 123), + new TestAccessibilityTarget(mContext, 789), + new TestAccessibilityTarget(mContext, 456)); + mMenuViewModel.onTargetFeaturesChanged(newFeaturesList); + verify(mOnTargetFeaturesChangeListener, times(3)).onChange(any()); + + // When the target features list doesn't change, the listener will still be called. + mMenuViewModel.onTargetFeaturesChanged(newFeaturesList); + verify(mOnTargetFeaturesChangeListener, times(4)).onChange(any()); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_NOTIFY_TARGETS_CHANGED_ON_STRICT_DIFF) + public void onTargetFeaturesChanged_listenerCalled_flagEnabled() { + // Call show() to start observing the target features change listener. + mMenuView.show(); + + // The target features change listener should be called when the observer is added. + verify(mOnTargetFeaturesChangeListener, times(1)).onChange(any()); + + // When the target features list changes, the listener should be called. + mMenuViewModel.onTargetFeaturesChanged( + List.of( + new TestAccessibilityTarget(mContext, 123), + new TestAccessibilityTarget(mContext, 456))); + verify(mOnTargetFeaturesChangeListener, times(2)).onChange(any()); + + // Double check that when the target features list changes, the listener should be called. + List<AccessibilityTarget> newFeaturesList = + List.of( + new TestAccessibilityTarget(mContext, 123), + new TestAccessibilityTarget(mContext, 789), + new TestAccessibilityTarget(mContext, 456)); + mMenuViewModel.onTargetFeaturesChanged(newFeaturesList); + verify(mOnTargetFeaturesChangeListener, times(3)).onChange(any()); + + // When the target features list doesn't change, the listener should not be called again. + mMenuViewModel.onTargetFeaturesChanged(newFeaturesList); + verify(mOnTargetFeaturesChangeListener, times(3)).onChange(any()); + + // When the target features list changes order (but the UIDs of the targets don't change), + // the listener should be called. + mMenuViewModel.onTargetFeaturesChanged( + List.of( + new TestAccessibilityTarget(mContext, 789), + new TestAccessibilityTarget(mContext, 123), + new TestAccessibilityTarget(mContext, 456))); + verify(mOnTargetFeaturesChangeListener, times(4)).onChange(any()); + } + private InstantInsetLayerDrawable getMenuViewInsetLayer() { return (InstantInsetLayerDrawable) mMenuView.getBackground(); } @@ -196,6 +301,23 @@ public class MenuViewTest extends SysuiTestCase { return radiiAnimator; } + /** Simplified AccessibilityTarget for testing MenuView. */ + private static class TestAccessibilityTarget extends AccessibilityTarget { + TestAccessibilityTarget(Context context, int uid) { + // Set fields unused by tests to defaults that allow test compilation. + super( + context, + ShortcutConstants.UserShortcutType.SOFTWARE, + 0, + false, + MAGNIFICATION_COMPONENT_NAME.flattenToString(), + uid, + null, + null, + null); + } + } + @After public void tearDown() throws Exception { mUiModeManager.setNightMode(mNightMode); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index 43d0d69c428f..e4539b75f317 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -66,7 +66,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.bluetooth.VolumeControlProfile; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bluetooth.qsdialog.DeviceItem; @@ -227,7 +226,6 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS) public void showDialog_noLiveCaption_noRelatedToolsInConfig_relatedToolLayoutGone() { mContext.getOrCreateTestableResources().addOverride( R.array.config_quickSettingsHearingDevicesRelatedToolName, new String[]{}); @@ -239,7 +237,6 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS) public void showDialog_hasLiveCaption_noRelatedToolsInConfig_showOneRelatedTool() { when(mPackageManager.queryIntentActivities( eq(LIVE_CAPTION_INTENT), anyInt())).thenReturn( @@ -254,7 +251,6 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS) public void showDialog_hasLiveCaption_oneRelatedToolInConfig_showTwoRelatedTools() throws PackageManager.NameNotFoundException { when(mPackageManager.queryIntentActivities(eq(LIVE_CAPTION_INTENT), anyInt())) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java index 8399fa85bfb1..aafb21209468 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/utils/TestUtils.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.ComponentName; +import android.content.Context; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -76,8 +77,10 @@ public class TestUtils { * Returns a mock secure settings configured to return information needed for tests. * Currently, this only includes button targets. */ - public static SecureSettings mockSecureSettings() { + public static SecureSettings mockSecureSettings(Context context) { SecureSettings secureSettings = mock(SecureSettings.class); + when(secureSettings.getRealUserHandle(UserHandle.USER_CURRENT)) + .thenReturn(context.getUserId()); final String targets = getShortcutTargets( Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt index a11dace0505c..4c329dcf2f2b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt @@ -18,12 +18,20 @@ package com.android.systemui.bluetooth.qsdialog import android.bluetooth.BluetoothLeBroadcast import android.bluetooth.BluetoothLeBroadcastMetadata +import android.content.ContentResolver +import android.content.applicationContext import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager +import com.android.settingslib.bluetooth.VolumeControlProfile +import com.android.settingslib.volume.shared.AudioSharingLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -38,10 +46,16 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -50,8 +64,11 @@ import org.mockito.kotlin.any class AudioSharingInteractorTest : SysuiTestCase() { @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() + @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast + @Mock private lateinit var bluetoothLeBroadcastMetadata: BluetoothLeBroadcastMetadata + @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback> private lateinit var underTest: AudioSharingInteractor @@ -157,13 +174,15 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) + runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() job.cancel() @@ -174,15 +193,14 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() - verify(localBluetoothLeBroadcast) - .registerServiceCallBack(any(), callbackCaptor.capture()) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) runCurrent() assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse() @@ -194,13 +212,15 @@ class AudioSharingInteractorTest : SysuiTestCase() { fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() = with(kosmos) { testScope.runTest { - bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false) bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile( localBluetoothLeBroadcast ) val job = launch { underTest.handleAudioSourceWhenReady() } runCurrent() + bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true) + runCurrent() verify(localBluetoothLeBroadcast) .registerServiceCallBack(any(), callbackCaptor.capture()) runCurrent() @@ -211,4 +231,100 @@ class AudioSharingInteractorTest : SysuiTestCase() { job.cancel() } } + + @Test + fun testHandleAudioSourceWhenReady_skipInitialValue_noAudioSharing_sourceNotAdded() = + with(kosmos) { + testScope.runTest { + val (broadcast, repository) = setupRepositoryImpl() + val interactor = + object : + AudioSharingInteractorImpl( + applicationContext, + localBluetoothManager, + repository, + testDispatcher, + ) { + override suspend fun audioSharingAvailable() = true + } + val job = launch { interactor.handleAudioSourceWhenReady() } + runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + // Verify source is not added + verify(repository, never()).addSource() + job.cancel() + } + } + + @Test + fun testHandleAudioSourceWhenReady_skipInitialValue_newAudioSharing_sourceAdded() = + with(kosmos) { + testScope.runTest { + val (broadcast, repository) = setupRepositoryImpl() + val interactor = + object : + AudioSharingInteractorImpl( + applicationContext, + localBluetoothManager, + repository, + testDispatcher, + ) { + override suspend fun audioSharingAvailable() = true + } + val job = launch { interactor.handleAudioSourceWhenReady() } + runCurrent() + // Verify callback registered for onBroadcastStartedOrStopped + verify(broadcast).registerServiceCallBack(any(), callbackCaptor.capture()) + // Audio sharing started, trigger onBroadcastStarted + whenever(broadcast.isEnabled(null)).thenReturn(true) + callbackCaptor.value.onBroadcastStarted(0, 0) + runCurrent() + // Verify callback registered for onBroadcastMetadataChanged + verify(broadcast, times(2)).registerServiceCallBack(any(), callbackCaptor.capture()) + runCurrent() + // Trigger onBroadcastMetadataChanged (ready to add source) + callbackCaptor.value.onBroadcastMetadataChanged(0, bluetoothLeBroadcastMetadata) + runCurrent() + // Verify source added + verify(repository).addSource() + job.cancel() + } + } + + private fun setupRepositoryImpl(): Pair<LocalBluetoothLeBroadcast, AudioSharingRepositoryImpl> { + with(kosmos) { + val broadcast = + mock<LocalBluetoothLeBroadcast> { + on { isProfileReady } doReturn true + on { isEnabled(null) } doReturn false + } + val assistant = + mock<LocalBluetoothLeBroadcastAssistant> { on { isProfileReady } doReturn true } + val volumeControl = mock<VolumeControlProfile> { on { isProfileReady } doReturn true } + val profileManager = + mock<LocalBluetoothProfileManager> { + on { leAudioBroadcastProfile } doReturn broadcast + on { leAudioBroadcastAssistantProfile } doReturn assistant + on { volumeControlProfile } doReturn volumeControl + } + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(localBluetoothManager.eventManager).thenReturn(mock<BluetoothEventManager> {}) + + val repository = + AudioSharingRepositoryImpl( + localBluetoothManager, + com.android.settingslib.volume.data.repository.AudioSharingRepositoryImpl( + mock<ContentResolver> {}, + localBluetoothManager, + testScope.backgroundScope, + testScope.testScheduler, + mock<AudioSharingLogger> {}, + ), + testDispatcher, + ) + return Pair(broadcast, spy(repository)) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt index acfe9dd45f75..f0746064f67f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt @@ -111,6 +111,28 @@ class AudioSharingRepositoryTest : SysuiTestCase() { } @Test + fun testStopAudioSharing() = + with(kosmos) { + testScope.runTest { + whenever(localBluetoothManager.profileManager).thenReturn(profileManager) + whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile) + audioSharingRepository.setAudioSharingAvailable(true) + underTest.stopAudioSharing() + verify(leAudioBroadcastProfile).stopLatestBroadcast() + } + } + + @Test + fun testStopAudioSharing_flagOff_doNothing() = + with(kosmos) { + testScope.runTest { + audioSharingRepository.setAudioSharingAvailable(false) + underTest.stopAudioSharing() + verify(leAudioBroadcastProfile, never()).stopLatestBroadcast() + } + } + + @Test fun testAddSource_flagOff_doesNothing() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt index 44f9720cb9e4..ad0337e5ce86 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt @@ -15,14 +15,15 @@ */ package com.android.systemui.bluetooth.qsdialog -import androidx.test.ext.junit.runners.AndroidJUnit4 import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -48,6 +49,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { private lateinit var notConnectedDeviceItem: DeviceItem private lateinit var connectedMediaDeviceItem: DeviceItem private lateinit var connectedOtherDeviceItem: DeviceItem + private lateinit var audioSharingDeviceItem: DeviceItem @Mock private lateinit var dialog: SystemUIDialog @Before @@ -59,7 +61,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) notConnectedDeviceItem = DeviceItem( @@ -68,7 +70,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) connectedMediaDeviceItem = DeviceItem( @@ -77,7 +79,7 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, ) connectedOtherDeviceItem = DeviceItem( @@ -86,7 +88,16 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = null, - background = null + background = null, + ) + audioSharingDeviceItem = + DeviceItem( + type = DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + cachedBluetoothDevice = kosmos.cachedBluetoothDevice, + deviceName = DEVICE_NAME, + connectionSummary = DEVICE_CONNECTION_SUMMARY, + iconWithDescription = null, + background = null, ) actionInteractorImpl = kosmos.deviceItemActionInteractorImpl } @@ -135,6 +146,29 @@ class DeviceItemActionInteractorTest : SysuiTestCase() { } } + @Test + fun onActionIconClick_onIntent() { + with(kosmos) { + testScope.runTest { + var onIntentCalledOnAddress = "" + whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) + actionInteractorImpl.onActionIconClick(connectedMediaDeviceItem) { + onIntentCalledOnAddress = connectedMediaDeviceItem.cachedBluetoothDevice.address + } + assertThat(onIntentCalledOnAddress).isEqualTo(DEVICE_ADDRESS) + } + } + } + + @Test(expected = IllegalArgumentException::class) + fun onActionIconClick_audioSharingDeviceType_throwException() { + with(kosmos) { + testScope.runTest { + actionInteractorImpl.onActionIconClick(audioSharingDeviceItem) {} + } + } + } + private companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt index 791f1f2e1f26..6fdeb2b8ebb0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardImageLoaderTest.kt @@ -17,14 +17,17 @@ package com.android.systemui.clipboardoverlay import android.content.ContentResolver import android.content.Context +import android.content.pm.UserInfo import android.net.Uri +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_CLIPBOARD_OVERLAY_MULTIUSER import com.android.systemui.SysuiTestCase -import com.android.systemui.util.mockito.whenever +import com.android.systemui.settings.FakeUserTracker import java.io.IOException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert.assertNull @@ -36,44 +39,90 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever @SmallTest -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class ClipboardImageLoaderTest : SysuiTestCase() { @Mock private lateinit var mockContext: Context @Mock private lateinit var mockContentResolver: ContentResolver + @Mock private lateinit var mockSecondaryContentResolver: ContentResolver private lateinit var clipboardImageLoader: ClipboardImageLoader + private var fakeUserTracker: FakeUserTracker = + FakeUserTracker(userContentResolverProvider = { mockContentResolver }) + + private val userInfos = listOf(UserInfo(0, "system", 0), UserInfo(50, "secondary", 0)) @Before fun setup() { MockitoAnnotations.initMocks(this) + + fakeUserTracker.set(userInfos, 0) } @Test @Throws(IOException::class) + @DisableFlags(FLAG_CLIPBOARD_OVERLAY_MULTIUSER) + fun test_imageLoadSuccess_legacy() = runTest { + val testDispatcher = StandardTestDispatcher(this.testScheduler) + fakeUserTracker = + FakeUserTracker(userContentResolverProvider = { mockSecondaryContentResolver }) + fakeUserTracker.set(userInfos, 1) + + clipboardImageLoader = + ClipboardImageLoader( + mockContext, + fakeUserTracker, + testDispatcher, + CoroutineScope(testDispatcher), + ) + val testUri = Uri.parse("testUri") + whenever<ContentResolver?>(mockContext.contentResolver) + .thenReturn(mockSecondaryContentResolver) + whenever(mockContext.resources).thenReturn(context.resources) + + clipboardImageLoader.load(testUri) + + verify(mockSecondaryContentResolver).loadThumbnail(eq(testUri), any(), any()) + } + + @Test + @Throws(IOException::class) + @EnableFlags(FLAG_CLIPBOARD_OVERLAY_MULTIUSER) fun test_imageLoadSuccess() = runTest { val testDispatcher = StandardTestDispatcher(this.testScheduler) + fakeUserTracker = + FakeUserTracker(userContentResolverProvider = { mockSecondaryContentResolver }) + fakeUserTracker.set(userInfos, 1) + clipboardImageLoader = - ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher)) + ClipboardImageLoader( + mockContext, + fakeUserTracker, + testDispatcher, + CoroutineScope(testDispatcher), + ) val testUri = Uri.parse("testUri") - whenever(mockContext.contentResolver).thenReturn(mockContentResolver) whenever(mockContext.resources).thenReturn(context.resources) clipboardImageLoader.load(testUri) - verify(mockContentResolver).loadThumbnail(eq(testUri), any(), any()) + verify(mockSecondaryContentResolver).loadThumbnail(eq(testUri), any(), any()) } - @OptIn(ExperimentalCoroutinesApi::class) @Test @Throws(IOException::class) fun test_imageLoadFailure() = runTest { val testDispatcher = StandardTestDispatcher(this.testScheduler) clipboardImageLoader = - ClipboardImageLoader(mockContext, testDispatcher, CoroutineScope(testDispatcher)) + ClipboardImageLoader( + mockContext, + fakeUserTracker, + testDispatcher, + CoroutineScope(testDispatcher), + ) val testUri = Uri.parse("testUri") whenever(mockContext.contentResolver).thenReturn(mockContentResolver) whenever(mockContext.resources).thenReturn(context.resources) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 20d66155e5ca..6c955bf1818d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -256,6 +256,22 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { @EnableSceneContainer @Test + fun playSuccessHaptic_onFaceAuthSuccess_whenBypassDisabled_sceneContainer() = + testScope.runTest { + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + + enrollFace() + kosmos.configureKeyguardBypass(isBypassAvailable = false) + runCurrent() + configureDeviceEntryFromBiometricSource(isFaceUnlock = true, bypassEnabled = false) + kosmos.fakeDeviceEntryFaceAuthRepository.isAuthenticated.value = true + + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() = testScope.runTest { kosmos.configureKeyguardBypass(isBypassAvailable = false) @@ -299,6 +315,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { private fun configureDeviceEntryFromBiometricSource( isFpUnlock: Boolean = false, isFaceUnlock: Boolean = false, + bypassEnabled: Boolean = true, ) { // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState if (isFpUnlock) { @@ -314,11 +331,14 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { ) // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING - kosmos.sceneInteractor.setTransitionState( - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(Scenes.Lockscreen) + // if the successful face authentication will bypass keyguard + if (bypassEnabled) { + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) ) - ) + } } underTest = kosmos.deviceEntryHapticsInteractor } diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt index 74e8257f4f08..5e023a203267 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt @@ -292,8 +292,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -306,8 +305,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @@ -321,8 +319,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -335,8 +332,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @@ -347,8 +343,7 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) assertThat(model?.lastShortcutTriggeredTime).isNull() - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ true, gestureType) assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant()) } @@ -358,15 +353,14 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge testScope.runTest { setUpForDeviceConnection() tutorialSchedulerRepository.setScheduledTutorialLaunchTime( - DeviceType.TOUCHPAD, + getTargetDevice(gestureType), eduClock.instant(), ) val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount eduClock.offset(initialDelayElapsedDuration) - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue + 1) } @@ -376,33 +370,92 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge testScope.runTest { setUpForDeviceConnection() tutorialSchedulerRepository.setScheduledTutorialLaunchTime( - DeviceType.TOUCHPAD, + getTargetDevice(gestureType), eduClock.instant(), ) val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount // No offset to the clock to simulate update before initial delay - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } @Test - fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchTime() = + fun dataUnchangedOnIncrementSignalCountWithoutOobeLaunchOrNotifyTime() = testScope.runTest { - // No update to OOBE launch time to simulate no OOBE is launched yet + // No update to OOBE launch/notify time to simulate no OOBE is launched yet setUpForDeviceConnection() val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) val originalValue = model!!.signalCount - val listener = getOverviewProxyListener() - listener.updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) assertThat(model?.signalCount).isEqualTo(originalValue) } + @Test + fun dataUpdatedOnIncrementSignalCountAfterNotifyTimeDelayWithoutLaunchTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + + @Test + fun dataUnchangedOnIncrementSignalCountBeforeLaunchTimeDelayWithNotifyTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + eduClock.offset(initialDelayElapsedDuration) + + tutorialSchedulerRepository.setScheduledTutorialLaunchTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + // No offset to the clock to simulate update before initial delay of launch time + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue) + } + + @Test + fun dataUpdatedOnIncrementSignalCountAfterLaunchTimeDelayWithNotifyTime() = + testScope.runTest { + setUpForDeviceConnection() + tutorialSchedulerRepository.setNotifiedTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + eduClock.offset(initialDelayElapsedDuration) + + tutorialSchedulerRepository.setScheduledTutorialLaunchTime( + getTargetDevice(gestureType), + eduClock.instant(), + ) + val model by collectLastValue(repository.readGestureEduModelFlow(gestureType)) + val originalValue = model!!.signalCount + eduClock.offset(initialDelayElapsedDuration) + updateContextualEduStats(/* isTrackpadGesture= */ false, gestureType) + + assertThat(model?.signalCount).isEqualTo(originalValue + 1) + } + private suspend fun setUpForInitialDelayElapse() { tutorialSchedulerRepository.setScheduledTutorialLaunchTime( DeviceType.TOUCHPAD, @@ -465,12 +518,18 @@ class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: Ge keyboardRepository.setIsAnyKeyboardConnected(true) } - private fun getOverviewProxyListener(): OverviewProxyListener { + private fun updateContextualEduStats(isTrackpadGesture: Boolean, gestureType: GestureType) { val listenerCaptor = argumentCaptor<OverviewProxyListener>() verify(overviewProxyService).addCallback(listenerCaptor.capture()) - return listenerCaptor.firstValue + listenerCaptor.firstValue.updateContextualEduStats(isTrackpadGesture, gestureType) } + private fun getTargetDevice(gestureType: GestureType) = + when (gestureType) { + ALL_APPS -> DeviceType.KEYBOARD + else -> DeviceType.TOUCHPAD + } + companion object { private val USER_INFOS = listOf(UserInfo(101, "Second User", 0)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 692b9c67f322..692b9c67f322 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 7c88d76f28bd..183e4d6f624b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -24,6 +24,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME import android.os.SystemClock import android.view.KeyEvent import android.view.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.ACTION_UP import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON @@ -540,11 +541,7 @@ object TestShortcuts { simpleShortcutCategory(System, "System apps", "Take a note"), simpleShortcutCategory(System, "System controls", "Take screenshot"), simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch to full screen", - ), + simpleShortcutCategory(MultiTasking, "Split screen", "Switch to full screen"), simpleShortcutCategory( MultiTasking, "Split screen", @@ -704,7 +701,7 @@ object TestShortcuts { android.view.KeyEvent( /* downTime = */ SystemClock.uptimeMillis(), /* eventTime = */ SystemClock.uptimeMillis(), - /* action = */ ACTION_DOWN, + /* action = */ ACTION_UP, /* code = */ KEYCODE_A, /* repeat = */ 0, /* metaState = */ 0, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index 755c218f6789..d9d34f5ace7b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -92,13 +92,14 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) - assertThat(uiState).isEqualTo( - AddShortcutDialog( - shortcutLabel = "Standard shortcut", - defaultCustomShortcutModifierKey = - ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + assertThat(uiState) + .isEqualTo( + AddShortcutDialog( + shortcutLabel = "Standard shortcut", + defaultCustomShortcutModifierKey = + ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + ) ) - ) } } @@ -137,8 +138,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } @@ -161,8 +161,7 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - assertThat((uiState as AddShortcutDialog).errorMessage) - .isEmpty() + assertThat((uiState as AddShortcutDialog).errorMessage).isEmpty() } } @@ -244,32 +243,34 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { } @Test - fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() { + fun onShortcutKeyCombinationSelected_handlesKeyEvents_whereActionKeyIsAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) assertThat(isHandled).isTrue() } } @Test - fun onKeyPressed_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { + fun onShortcutKeyCombinationSelected_doesNotHandleKeyEvents_whenActionKeyIsNotAlsoPressed() { testScope.runTest { viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - val isHandled = viewModel.onKeyPressed(keyDownEventWithoutActionKeyPressed) + val isHandled = + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithoutActionKeyPressed) assertThat(isHandled).isFalse() } } @Test - fun onKeyPressed_convertsKeyEventsAndUpdatesUiStatesPressedKey() { + fun onShortcutKeyCombinationSelected_convertsKeyEventsAndUpdatesUiStatesPressedKey() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -282,8 +283,8 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { testScope.runTest { val uiState by collectLastValue(viewModel.shortcutCustomizationUiState) viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) // Note that Action Key is excluded as it's already displayed on the UI assertThat((uiState as AddShortcutDialog).pressedKeys) @@ -292,16 +293,15 @@ class ShortcutCustomizationViewModelTest : SysuiTestCase() { // Close the dialog and show it again viewModel.onDialogDismissed() viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest) - assertThat((uiState as AddShortcutDialog).pressedKeys) - .isEmpty() + assertThat((uiState as AddShortcutDialog).pressedKeys).isEmpty() } } private suspend fun openAddShortcutDialogAndSetShortcut() { viewModel.onShortcutCustomizationRequested(allAppsShortcutAddRequest) - viewModel.onKeyPressed(keyDownEventWithActionKeyPressed) - viewModel.onKeyPressed(keyUpEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyDownEventWithActionKeyPressed) + viewModel.onShortcutKeyCombinationSelected(keyUpEventWithActionKeyPressed) viewModel.onSetShortcut() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index fde9b8ce6a50..bf49186a7f01 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -28,7 +28,7 @@ import android.provider.Settings.Secure.ZEN_DURATION_PROMPT import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.EnableZenModeDialog -import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription @@ -187,7 +187,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -2) collectLastValue(underTest.lockScreenState) runCurrent() @@ -233,7 +233,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_FOREVER) collectLastValue(underTest.lockScreenState) runCurrent() @@ -278,7 +277,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingNotFOREVERorPROMPT_dndWithDuration() = testScope.runTest { val currentModes by collectLastValue(zenModeRepository.modes) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, -900) runCurrent() @@ -323,7 +321,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { fun onTriggered_dndModeIsOff_settingIsPROMPT_showDialog() = testScope.runTest { val expandable: Expandable = mock() - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) secureSettingsRepository.setInt(Settings.Secure.ZEN_DURATION, ZEN_DURATION_PROMPT) whenever(enableZenModeDialog.createDialog()).thenReturn(mock()) collectLastValue(underTest.lockScreenState) @@ -405,10 +402,6 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { testScope.runTest { val lockScreenState by collectLastValue(underTest.lockScreenState) - val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE - zenModeRepository.addMode(manualDnd) - runCurrent() - assertThat(lockScreenState) .isEqualTo( KeyguardQuickAffordanceConfig.LockScreenState.Visible( @@ -420,7 +413,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) ) - zenModeRepository.activateMode(manualDnd) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(lockScreenState) 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 b3417b9de36d..c44f27ef348b 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 @@ -46,8 +46,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes @@ -76,7 +74,6 @@ class KeyguardInteractorTest : SysuiTestCase() { private val configRepository by lazy { kosmos.fakeConfigurationRepository } private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository } private val shadeRepository by lazy { kosmos.shadeRepository } - private val powerInteractor by lazy { kosmos.powerInteractor } private val keyguardRepository by lazy { kosmos.keyguardRepository } private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } @@ -444,7 +441,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() advanceTimeBy(1000L) assertThat(isAbleToDream).isEqualTo(false) @@ -460,9 +456,6 @@ class KeyguardInteractorTest : SysuiTestCase() { repository.setDozeTransitionModel( DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) ) - powerInteractor.setAwakeForTest() - runCurrent() - // After some delay, still false advanceTimeBy(300L) assertThat(isAbleToDream).isEqualTo(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt index b0698555941c..98e3c68e6e33 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractorTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode 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 @@ -406,4 +407,48 @@ class KeyguardWakeDirectlyToGoneInteractorTest : SysuiTestCase() { // It should not have any effect. assertEquals(listOf(false, true, false, true), canWake) } + + @Test + @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) + fun testCanWakeDirectlyToGone_falseAsSoonAsTransitionsAwayFromGone() = + testScope.runTest { + val canWake by collectValues(underTest.canWakeDirectlyToGone) + + assertEquals( + listOf( + false // Defaults to false. + ), + canWake, + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope, + ) + + assertEquals( + listOf( + false, + true, // Because we're GONE. + ), + canWake, + ) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope = testScope, + throughTransitionState = TransitionState.RUNNING, + ) + + assertEquals( + listOf( + false, + true, + false, // False as soon as we start a transition away from GONE. + ), + canWake, + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt index feaf06aca29a..ade7614ae853 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt @@ -16,10 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues +import com.android.systemui.flags.BrokenWithSceneContainer import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -72,6 +75,28 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsOpen() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.1f), + step(0.2f), + step(0.3f), + step(1f), + ), + testScope, + ) + + values.forEach { assertThat(it).isEqualTo(1f) } + } + + @Test fun blurRadiusGoesToMaximumWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -88,6 +113,25 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt index d909c5ab5f1b..914094fa39df 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt @@ -16,9 +16,11 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -153,7 +155,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusIsMaxWhenShadeIsExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -170,7 +172,7 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza } @Test - @BrokenWithSceneContainer(330311871) + @BrokenWithSceneContainer(388068805) fun blurRadiusGoesFromMinToMaxWhenShadeIsNotExpanded() = testScope.runTest { val values by collectValues(underTest.windowBlurRadius) @@ -185,6 +187,44 @@ class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameteriza ) } + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notificationBlur_isNonZero_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationBlurRadius) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + endValue = kosmos.blurConfig.maxBlurRadiusPx / 3.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + + @Test + @EnableFlags(FLAG_BOUNCER_UI_REVAMP) + @BrokenWithSceneContainer(388068805) + fun notifications_areFullyVisible_whenShadeIsExpanded() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + kosmos.bouncerWindowBlurTestUtil.shadeExpanded(true) + runCurrent() + + kosmos.bouncerWindowBlurTestUtil.assertTransitionToBlurRadius( + transitionProgress = listOf(0f, 0f, 0.1f, 0.2f, 0.3f, 1f), + startValue = 1.0f, + endValue = 1.0f, + transitionFactory = ::step, + actualValuesProvider = { values }, + checkInterpolatedValues = false, + ) + } + private fun step( value: Float, state: TransitionState = TransitionState.RUNNING, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt index a8e390c25a4d..46d98f979655 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt @@ -23,11 +23,16 @@ import com.android.systemui.classifier.fakeFalsingManager import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest +import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.ui.viewmodel.toolbar.editModeButtonViewModelFactory import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @SmallTest @@ -36,6 +41,15 @@ class EditModeButtonViewModelTest : SysuiTestCase() { val underTest = kosmos.editModeButtonViewModelFactory.create() + @Before + fun setUp() { + with(kosmos) { + whenever(activityStarter.postQSRunnableDismissingKeyguard(any())).doAnswer { + (it.getArgument(0) as Runnable).run() + } + } + } + @Test fun falsingFalseTap_editModeDoesntStart() = kosmos.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java index 7d41a20e628f..307b87a74dad 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java @@ -27,8 +27,6 @@ import static org.mockito.Mockito.when; import android.content.Intent; import android.os.Handler; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.service.quicksettings.Tile; import android.testing.TestableLooper; @@ -38,7 +36,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker; import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager; @@ -124,18 +121,6 @@ public class HearingDevicesTileTest extends SysuiTestCase { } @Test - @EnableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) - public void isAvailable_flagEnabled_true() { - assertThat(mTile.isAvailable()).isTrue(); - } - - @Test - @DisableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) - public void isAvailable_flagDisabled_false() { - assertThat(mTile.isAvailable()).isFalse(); - } - - @Test public void longClick_expectedAction() { mTile.longClick(null); mTestableLooper.processAllMessages(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt index eeccbdf20540..79556baed067 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.qs.tiles import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf import android.service.quicksettings.Tile @@ -24,18 +26,26 @@ import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger +import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake +import com.android.systemui.flags.setFlagValue +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.flags.QSComposeFragment +import com.android.systemui.qs.flags.QsDetailedView import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tiles.dialog.InternetDialogManager import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.connectivity.AccessPointController +import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor @@ -256,6 +266,41 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true) } + @Test + @DisableFlags(QsDetailedView.FLAG_NAME) + fun click_withQsDetailedViewDisabled() { + underTest.click(null) + looper.processAllMessages() + + verify(dialogManager, times(1)).create( + aboveStatusBar = true, + accessPointController.canConfigMobileData(), + accessPointController.canConfigWifi(), + null, + ) + } + + @Test + @EnableFlags( + value = [ + QsDetailedView.FLAG_NAME, + FLAG_SCENE_CONTAINER, + KeyguardWmStateRefactor.FLAG_NAME, + NotificationThrottleHun.FLAG_NAME, + DualShade.FLAG_NAME] + ) + fun click_withQsDetailedViewEnabled() { + underTest.click(null) + looper.processAllMessages() + + verify(dialogManager, times(0)).create( + aboveStatusBar = true, + accessPointController.canConfigMobileData(), + accessPointController.canConfigWifi(), + null, + ) + } + companion object { const val WIFI_SSID = "test ssid" val ACTIVE_WIFI = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index fc1d73b62abd..3a3f5371d195 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.app.Dialog; import android.media.projection.StopReason; import android.os.Handler; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; import android.service.quicksettings.Tile; import android.testing.TestableLooper; @@ -52,6 +53,7 @@ import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; +import com.android.systemui.qs.flags.QsDetailedView; import com.android.systemui.qs.flags.QsInCompose; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; @@ -63,6 +65,7 @@ import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,11 +73,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.List; - import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; +import java.util.List; + @RunWith(ParameterizedAndroidJunit4.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest @@ -82,7 +85,8 @@ public class ScreenRecordTileTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX); + return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX, + QsDetailedView.FLAG_NAME); } @Mock @@ -336,6 +340,30 @@ public class ScreenRecordTileTest extends SysuiTestCase { .notifyPermissionRequestDisplayed(mContext.getUserId()); } + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testNotStartingAndRecording_returnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNotNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testStarting_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(true); + when(mController.isRecording()).thenReturn(false); + mTile.getDetailsViewModel(Assert::assertNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) + public void testRecording_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(true); + mTile.getDetailsViewModel(Assert::assertNull); + } + private QSTile.Icon createExpectedIcon(int resId) { if (QsInCompose.isEnabled()) { return new QSTileImpl.DrawableIconWithRes(mContext.getDrawable(resId), resId); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractorTest.kt index 1dfa2cd26491..9099d3d911bc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractorTest.kt @@ -17,12 +17,9 @@ package com.android.systemui.qs.tiles.impl.hearingdevices.domain.interactor import android.os.UserHandle -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.EnabledOnRavenwood 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.accessibility.hearingaid.HearingDevicesChecker import com.android.systemui.coroutines.collectLastValue @@ -66,24 +63,14 @@ class HearingDevicesTileDataInteractorTest : SysuiTestCase() { underTest = HearingDevicesTileDataInteractor(testScope.testScheduler, controller, checker) } - @EnableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) @Test - fun availability_flagEnabled_returnTrue() = + fun availability_returnTrue() = testScope.runTest { val availability by collectLastValue(underTest.availability(testUser)) assertThat(availability).isTrue() } - @DisableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) - @Test - fun availability_flagDisabled_returnFalse() = - testScope.runTest { - val availability by collectLastValue(underTest.availability(testUser)) - - assertThat(availability).isFalse() - } - @Test fun tileData_bluetoothStateChanged_dataMatchesChecker() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 04e094f25f5d..c8b3aba9b846 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -24,6 +24,7 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable @@ -87,9 +88,9 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val activeModes by collectLastValue(zenModeInteractor.activeModes) + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Mode 1").setActive(true).build(), TestModeBuilder().setName("Mode 2").setActive(true).build(), ) @@ -111,7 +112,7 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE) + zenModeRepository.activateMode(MANUAL_DND) assertThat(dndMode?.isActive).isTrue() underTest.handleInput( @@ -127,7 +128,6 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(zenModeInteractor.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) assertThat(dndMode?.isActive).isFalse() underTest.handleInput( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 48edded5df18..de54e75281aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -575,4 +575,50 @@ class SceneInteractorTest : SysuiTestCase() { assertThat(currentScene).isNotEqualTo(disabledScene) } + + @Test + fun transitionAnimations() = + kosmos.runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.onTransitionAnimationStart() + // One animation is active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationCancelled() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + + underTest.setVisible(true, "test") + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationStart() + underTest.onTransitionAnimationStart() + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.setVisible(false, "test") + // Two animations are active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // One animation is still active, forced visible. + assertThat(isVisible).isTrue() + + underTest.onTransitionAnimationEnd() + // No more active animations, not forced visible. + assertThat(isVisible).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 06dd046564df..51f056aa18da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -36,6 +36,8 @@ import com.android.keyguard.AuthInteractionProperties import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.ActivityTransitionAnimator +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -141,6 +143,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever @SmallTest @@ -169,6 +172,7 @@ class SceneContainerStartableTest : SysuiTestCase() { private val uiEventLoggerFake = kosmos.uiEventLoggerFake private val msdlPlayer = kosmos.fakeMSDLPlayer private val authInteractionProperties = AuthInteractionProperties() + private val mockActivityTransitionAnimator = mock<ActivityTransitionAnimator>() private lateinit var underTest: SceneContainerStartable @@ -177,6 +181,8 @@ class SceneContainerStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) .thenReturn(true) + kosmos.activityTransitionAnimator = mockActivityTransitionAnimator + underTest = kosmos.sceneContainerStartable } @@ -2716,6 +2722,27 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentOverlays).isEmpty() } + @Test + fun hydrateActivityTransitionAnimationState() = + kosmos.runTest { + underTest.start() + + val isVisible by collectLastValue(sceneInteractor.isVisible) + assertThat(isVisible).isTrue() + + sceneInteractor.setVisible(false, "reason") + assertThat(isVisible).isFalse() + + val argumentCaptor = argumentCaptor<ActivityTransitionAnimator.Listener>() + verify(mockActivityTransitionAnimator).addListener(argumentCaptor.capture()) + + val listeners = argumentCaptor.allValues + listeners.forEach { it.onTransitionAnimationStart() } + assertThat(isVisible).isTrue() + listeners.forEach { it.onTransitionAnimationEnd() } + assertThat(isVisible).isFalse() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 789ca5158dbf..62c360400582 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -85,7 +85,6 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; @@ -335,16 +334,14 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false); mMainDispatcher = getMainDispatcher(); - KeyguardInteractorFactory.WithDependencies keyguardInteractorDeps = - KeyguardInteractorFactory.create(); - mFakeKeyguardRepository = keyguardInteractorDeps.getRepository(); + mFakeKeyguardRepository = mKosmos.getKeyguardRepository(); mFakeKeyguardClockRepository = new FakeKeyguardClockRepository(); mKeyguardClockInteractor = mKosmos.getKeyguardClockInteractor(); - mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor(); + mKeyguardInteractor = mKosmos.getKeyguardInteractor(); mShadeRepository = new FakeShadeRepository(); mShadeAnimationInteractor = new ShadeAnimationInteractorLegacyImpl( new ShadeAnimationRepository(), mShadeRepository); - mPowerInteractor = keyguardInteractorDeps.getPowerInteractor(); + mPowerInteractor = mKosmos.getPowerInteractor(); when(mKeyguardTransitionInteractor.isInTransitionWhere(any(), any())).thenReturn( MutableStateFlow(false)); when(mKeyguardTransitionInteractor.isInTransition(any(), any())) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 20474c842b51..deaf57999b21 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -526,7 +526,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1)); // Device is not currently locked - when(mKeyguardManager.isDeviceLocked()).thenReturn(false); + mLockscreenUserManager.mLocked.set(false); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, @@ -540,7 +540,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1, mCurrentUser.id); changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Device was locked after this notification arrived mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime + TimeUnit.DAYS.toMillis(1)); @@ -560,7 +560,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // Device has been locked for 1 second before the notification came in, which is too short mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.SECONDS.toMillis(1)); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, @@ -577,7 +577,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { // Claim the device was last locked 1 day ago mLockscreenUserManager.mLastLockTime .set(mSensitiveNotifPostTime - TimeUnit.DAYS.toMillis(1)); - when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + mLockscreenUserManager.mLocked.set(true); // Sensitive Content notifications are always redacted assertEquals(REDACTION_TYPE_NONE, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index 2d7dc2e63650..0a0564994e69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -43,6 +43,7 @@ import com.android.systemui.testKosmos import com.android.systemui.util.WallpaperController import com.android.systemui.util.mockito.eq import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor +import com.android.wm.shell.appzoomout.AppZoomOut import com.google.common.truth.Truth.assertThat import java.util.function.Consumer import org.junit.Before @@ -65,6 +66,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit +import java.util.Optional @RunWith(AndroidJUnit4::class) @RunWithLooper @@ -82,6 +84,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { @Mock private lateinit var wallpaperController: WallpaperController @Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var appZoomOutOptional: Optional<AppZoomOut> @Mock private lateinit var root: View @Mock private lateinit var viewRootImpl: ViewRootImpl @Mock private lateinit var windowToken: IBinder @@ -128,6 +131,7 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { ResourcesSplitShadeStateController(), windowRootViewBlurInteractor, applicationScope, + appZoomOutOptional, dumpManager, configurationController, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt index bb9141afe404..5f73ac45d12f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarSignalPolicyTest.kt @@ -45,8 +45,11 @@ import kotlin.test.Test import org.junit.Before import org.junit.runner.RunWith import org.mockito.Mockito.verify +import org.mockito.kotlin.any import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verifyNoMoreInteractions @SmallTest @@ -106,10 +109,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { // Make sure the legacy code path does not change airplane mode when the refactor // flag is enabled. underTest.setIsAirplaneMode(IconState(true, TelephonyIcons.FLIGHT_MODE_ICON, "")) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) underTest.setIsAirplaneMode(IconState(false, TelephonyIcons.FLIGHT_MODE_ICON, "")) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) } @Test @@ -144,10 +147,10 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { // Make sure changing airplane mode from airplaneModeRepository does nothing // if the StatusBarSignalPolicyRefactor is not enabled. airplaneModeInteractor.setIsAirplaneMode(true) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) airplaneModeInteractor.setIsAirplaneMode(false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotAirplane), any()) } @Test @@ -196,7 +199,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { underTest.setEthernetIndicators( IconState(/* visible= */ true, /* icon= */ 1, /* contentDescription= */ "Ethernet") ) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) underTest.setEthernetIndicators( IconState( @@ -205,7 +208,7 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { /* contentDescription= */ "No ethernet", ) ) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) } @Test @@ -217,13 +220,13 @@ class StatusBarSignalPolicyTest : SysuiTestCase() { clearInvocations(statusBarIconController) connectivityRepository.fake.setEthernetConnected(default = true, validated = true) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) connectivityRepository.fake.setEthernetConnected(default = false, validated = false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) connectivityRepository.fake.setEthernetConnected(default = true, validated = false) - verifyNoMoreInteractions(statusBarIconController) + verify(statusBarIconController, never()).setIconVisibility(eq(slotEthernet), any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt index 0061c4142e8b..ac3089d9286b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt @@ -22,7 +22,6 @@ import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue @@ -126,65 +125,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_positiveStartTime_notifIconFlagOff_iconIsPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 1000, notificationIcon = createStatusBarIconViewOrNull()) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconFlagOn_iconIsNotifIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - val notifIcon = createStatusBarIconViewOrNull() - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, notificationIcon = notifIcon)) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.StatusBarView::class.java) - val actualIcon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.StatusBarView) - .impl - assertThat(actualIcon).isEqualTo(notifIcon) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel( - startTimeMs = 1000, - notificationIcon = createStatusBarIconViewOrNull(), - notificationKey = "notifKey", - ) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isEqualTo(OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon("notifKey")) - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_positiveStartTime_notifIconAndConnectedDisplaysFlagOn_iconIsNotifIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_positiveStartTime_connectedDisplaysFlagOn_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -205,29 +147,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun chip_zeroStartTime_notifIconFlagOff_iconIsPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.chip) - - repo.setOngoingCallState( - inCallModel(startTimeMs = 0, notificationIcon = createStatusBarIconViewOrNull()) - ) - - assertThat((latest as OngoingActivityChipModel.Shown).icon) - .isInstanceOf(OngoingActivityChipModel.ChipIcon.SingleColorIcon::class.java) - val icon = - (((latest as OngoingActivityChipModel.Shown).icon) - as OngoingActivityChipModel.ChipIcon.SingleColorIcon) - .impl as Icon.Resource - assertThat(icon.res).isEqualTo(com.android.internal.R.drawable.ic_phone) - assertThat(icon.contentDescription).isNotNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_notifIconFlagOn_cdFlagOff_iconIsNotifIcon() = + fun chip_zeroStartTime_cdFlagOff_iconIsNotifIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -244,8 +165,8 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun chip_zeroStartTime_notifIconFlagOn_cdFlagOn_iconIsNotifKeyIcon() = + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun chip_zeroStartTime_cdFlagOn_iconIsNotifKeyIcon() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -262,7 +183,6 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_cdFlagOff_iconIsPhone() = testScope.runTest { @@ -281,7 +201,7 @@ class CallChipViewModelTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun chip_notifIconFlagOn_butNullNotifIcon_iconNotifKey() = testScope.runTest { val latest by collectLastValue(underTest.chip) @@ -363,14 +283,16 @@ class CallChipViewModelTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.chip) - val intent = mock<PendingIntent>() - repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = intent)) + val pendingIntent = mock<PendingIntent>() + repo.setOngoingCallState(inCallModel(startTimeMs = 1000, intent = pendingIntent)) val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener assertThat(clickListener).isNotNull() clickListener!!.onClick(chipView) - verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null) + // Ensure that the SysUI didn't modify the notification's intent by verifying it + // directly matches the `PendingIntent` set -- see b/212467440. + verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(pendingIntent, null) } @Test @@ -378,14 +300,16 @@ class CallChipViewModelTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.chip) - val intent = mock<PendingIntent>() - repo.setOngoingCallState(inCallModel(startTimeMs = 0, intent = intent)) + val pendingIntent = mock<PendingIntent>() + repo.setOngoingCallState(inCallModel(startTimeMs = 0, intent = pendingIntent)) val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener assertThat(clickListener).isNotNull() clickListener!!.onClick(chipView) - verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null) + // Ensure that the SysUI didn't modify the notification's intent by verifying it + // directly matches the `PendingIntent` set -- see b/212467440. + verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(pendingIntent, null) } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index e561e3ea27d7..902db5e10589 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -501,7 +501,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) - fun chips_hasHeadsUpByUser_onlyShowsIcon() = + fun chips_hasHeadsUpBySystem_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -523,7 +523,99 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) - // WHEN there's a HUN pinned by a user + // WHEN there's a HUN pinned by the system + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedBySystem), + ) + ) + + // THEN the chip keeps showing time + // (In real life the chip won't show at all, but that's handled in a different part of + // the system. What we know here is that the chip shouldn't shrink to icon only.) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forOtherNotif_showsTime() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val otherPromotedContentBuilder = + PromotedNotificationContentModel.Builder("other notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 654321L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + val icon = createStatusBarIconViewOrNull() + val otherIcon = createStatusBarIconViewOrNull() + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = icon, + promotedContent = promotedContentBuilder.build(), + ), + activeNotificationModel( + key = "other notif", + statusBarChipIcon = otherIcon, + promotedContent = otherPromotedContentBuilder.build(), + ), + ) + ) + + // WHEN there's a HUN pinned for the "other notif" chip + kosmos.headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "other notif", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + // THEN the "notif" chip keeps showing time + val chip = latest!![0] + assertThat(chip).isInstanceOf(OngoingActivityChipModel.Shown.ShortTimeDelta::class.java) + assertIsNotifChip(chip, icon, "notif") + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_hasHeadsUpByUser_forThisNotif_onlyShowsIcon() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock<StatusBarIconView>(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + // WHEN this notification is pinned by the user kosmos.headsUpNotificationRepository.setNotifications( UnconfinedFakeHeadsUpRowRepository( key = "notif", @@ -531,6 +623,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { ) ) + // THEN the chip shrinks to icon only assertThat(latest!![0]) .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt index c9c961791e89..49b95d92129c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderTest.kt @@ -45,7 +45,7 @@ import com.google.common.truth.Truth.assertWithMessage import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.whenever diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt new file mode 100644 index 000000000000..72001758d01f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import android.content.res.Configuration +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.kosmos.Kosmos +import com.android.systemui.kosmos.collectValues +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fake +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) +class StatusBarContentInsetsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val configuration = Configuration() + + private val Kosmos.underTest by Kosmos.Fixture { statusBarContentInsetsViewModel } + + @Test + fun contentArea_onMaxBoundsChanged_emitsNewValue() = + kosmos.runTest { + statusBarContentInsetsProvider.start() + + val values by collectValues(underTest.contentArea) + + // WHEN the content area changes + configurationController.fake.notifyLayoutDirectionChanged(isRtl = true) + configurationController.fake.notifyDensityOrFontScaleChanged() + + // THEN the flow emits the new bounds + assertThat(values[0]).isNotEqualTo(values[1]) + } + + @Test + fun contentArea_onDensityOrFontScaleChanged_emitsLastBounds() = + kosmos.runTest { + configuration.densityDpi = 12 + statusBarContentInsetsProvider.start() + + val values by collectValues(underTest.contentArea) + + // WHEN a change happens but it doesn't affect content area + configuration.densityDpi = 20 + configurationController.onConfigurationChanged(configuration) + configurationController.fake.notifyDensityOrFontScaleChanged() + + // THEN it still has the last bounds + assertThat(values).hasSize(1) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index a3ffd91b1728..609885d0214b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -458,7 +458,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_hasNotifEntry_shownAsHUN() = + fun onPromotedNotificationChipTapped_hasNotifEntry_shownAsHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -473,7 +473,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_noNotifEntry_noHUN() = + fun onPromotedNotificationChipTapped_noNotifEntry_noHUN() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(null) @@ -488,7 +488,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_shownAsHUNEvenIfEntryShouldNot() = + fun onPromotedNotificationChipTapped_shownAsHUNEvenIfEntryShouldNot() = testScope.runTest { whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) @@ -511,7 +511,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun showPromotedNotification_atSameTimeAsOnAdded_promotedShownAsHUN() = + fun onPromotedNotificationChipTapped_atSameTimeAsOnAdded_promotedShownAsHUN() = testScope.runTest { // First, the promoted notification appears as not heads up val promotedEntry = NotificationEntryBuilder().setPkg("promotedPackage").build() @@ -548,6 +548,33 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun onPromotedNotificationChipTapped_chipTappedTwice_hunHiddenOnSecondTap() = + testScope.runTest { + whenever(notifCollection.getEntry(entry.key)).thenReturn(entry) + + // WHEN chip tapped first + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is shown + finishBind(entry) + verify(headsUpManager).showNotification(entry, isPinnedByUser = true) + addHUN(entry) + + // WHEN chip is tapped again + statusBarNotificationChipsInteractor.onPromotedNotificationChipTapped(entry.key) + executor.advanceClockToLast() + executor.runAllReady() + beforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(entry)) + + // THEN HUN is hidden + verify(headsUpManager).removeNotification(eq(entry.key), eq(false), any()) + } + + @Test fun testTransferIsolatedChildAlert_withGroupAlertSummary() { setShouldHeadsUp(groupSummary) whenever(notifPipeline.allNotifs).thenReturn(listOf(groupSummary, groupSibling1)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java index c05b13163d32..c5752691da44 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.platform.test.annotations.EnableFlags; import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -61,6 +62,7 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.ArrayList; @@ -83,6 +85,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { private NotificationEntry mEntry; private NotifFilter mCapturedSuspendedFilter; + private NotifFilter mCapturedDndStatelessFilter; private NotifFilter mCapturedDozingFilter; private StatusBarStateController.StateListener mStatusBarStateCallback; private RankingCoordinator mRankingCoordinator; @@ -107,6 +110,15 @@ public class RankingCoordinatorTest extends SysuiTestCase { mRankingCoordinator.attach(mNotifPipeline); verify(mNotifPipeline, times(2)).addPreGroupFilter(mNotifFilterCaptor.capture()); mCapturedSuspendedFilter = mNotifFilterCaptor.getAllValues().get(0); + if (com.android.systemui.Flags.notificationAmbientSuppressionAfterInflation()) { + verify(mNotifPipeline).addFinalizeFilter(mNotifFilterCaptor.capture()); + mCapturedDndStatelessFilter = mNotifFilterCaptor.getAllValues().get(1); + mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(2); + } else { + verify(mNotifPipeline, never()).addFinalizeFilter(any()); + mCapturedDndStatelessFilter = null; + mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1); + } mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1); mCapturedDozingFilter.setInvalidationListener(mInvalidationListener); @@ -159,6 +171,40 @@ public class RankingCoordinatorTest extends SysuiTestCase { } @Test + @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_AMBIENT_SUPPRESSION_AFTER_INFLATION) + public void filterStatelessVisualEffectsSuppression() { + Mockito.clearInvocations(mStatusBarStateController); + + // WHEN should suppress ambient + mEntry.setRanking(getRankingForUnfilteredNotif() + .setSuppressedVisualEffects(SUPPRESSED_EFFECT_AMBIENT) + .build()); + + // THEN do not filter out the notification + assertFalse(mCapturedDozingFilter.shouldFilterOut(mEntry, 0)); + + // WHEN should suppress list + mEntry.setRanking(getRankingForUnfilteredNotif() + .setSuppressedVisualEffects(SUPPRESSED_EFFECT_NOTIFICATION_LIST) + .build()); + + // THEN do not filter out the notification + assertFalse(mCapturedDozingFilter.shouldFilterOut(mEntry, 0)); + + // WHEN should suppress both ambient and list + mEntry.setRanking(getRankingForUnfilteredNotif() + .setSuppressedVisualEffects( + SUPPRESSED_EFFECT_AMBIENT | SUPPRESSED_EFFECT_NOTIFICATION_LIST) + .build()); + + // THEN we should filter out the notification! + assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0)); + + // VERIFY that we don't check the dozing state + verify(mStatusBarStateController, never()).isDozing(); + } + + @Test public void filterDozingSuppressAmbient() { // GIVEN should suppress ambient mEntry.setRanking(getRankingForUnfilteredNotif() @@ -200,7 +246,7 @@ public class RankingCoordinatorTest extends SysuiTestCase { // WHEN it's not dozing (showing the notification list) when(mStatusBarStateController.isDozing()).thenReturn(false); - + // THEN filter out the notification assertTrue(mCapturedDozingFilter.shouldFilterOut(mEntry, 0)); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt index 912633c874ed..e6fbc725af04 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManagerTest.kt @@ -101,7 +101,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is not queried for group or row controllers inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, never()).getGroupController(any()) verify(spyViewRenderer, never()).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -121,7 +120,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is queried once per group/entry inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, times(2)).getGroupController(any()) verify(spyViewRenderer, times(8)).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -144,7 +142,6 @@ class RenderStageManagerTest : SysuiTestCase() { // VERIFY that the renderer is queried once per group/entry inOrder(spyViewRenderer).apply { verify(spyViewRenderer, times(1)).onRenderList(any()) - verify(spyViewRenderer, times(1)).getStackController() verify(spyViewRenderer, times(2)).getGroupController(any()) verify(spyViewRenderer, times(8)).getRowController(any()) verify(spyViewRenderer, times(1)).onDispatchComplete() @@ -162,7 +159,7 @@ class RenderStageManagerTest : SysuiTestCase() { onRenderListListener.onRenderList(listWith2Groups8Entries()) // VERIFY that the listeners are invoked once per group and once per entry - verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any()) + verify(onAfterRenderListListener, times(1)).onAfterRenderList(any()) verify(onAfterRenderGroupListener, times(2)).onAfterRenderGroup(any(), any()) verify(onAfterRenderEntryListener, times(8)).onAfterRenderEntry(any(), any()) verifyNoMoreInteractions( @@ -182,7 +179,7 @@ class RenderStageManagerTest : SysuiTestCase() { onRenderListListener.onRenderList(listOf()) // VERIFY that the stack listener is invoked once but other listeners are not - verify(onAfterRenderListListener, times(1)).onAfterRenderList(any(), any()) + verify(onAfterRenderListListener, times(1)).onAfterRenderList(any()) verify(onAfterRenderGroupListener, never()).onAfterRenderGroup(any(), any()) verify(onAfterRenderEntryListener, never()).onAfterRenderEntry(any(), any()) verifyNoMoreInteractions( @@ -203,8 +200,6 @@ class RenderStageManagerTest : SysuiTestCase() { private class FakeNotifViewRenderer : NotifViewRenderer { override fun onRenderList(notifList: List<ListEntry>) {} - override fun getStackController(): NotifStackController = mock() - override fun getGroupController(group: GroupEntry): NotifGroupController = mock() override fun getRowController(entry: NotificationEntry): NotifRowController = mock() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt index 54ce88b40c11..83c61507a506 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt @@ -26,7 +26,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -275,7 +275,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -293,7 +292,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -311,7 +309,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 0, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -329,7 +326,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -347,7 +343,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, @@ -365,7 +360,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -383,7 +377,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, @@ -401,7 +394,6 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt index 22ef408e266c..fae7d515d305 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.shadeTestUtil import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -412,46 +413,53 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { @Test fun statusBarHeadsUpState_pinnedBySystem() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedBySystem) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedBySystem) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedBySystem)) + assertThat(status).isEqualTo(PinnedStatus.PinnedBySystem) } @Test fun statusBarHeadsUpState_pinnedByUser() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", pinnedStatus = PinnedStatus.PinnedByUser) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.PinnedByUser) + assertThat(state).isEqualTo(TopPinnedState.Pinned("key 0", PinnedStatus.PinnedByUser)) + assertThat(status).isEqualTo(PinnedStatus.PinnedByUser) } @Test fun statusBarHeadsUpState_withoutPinnedNotifications_notPinned() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) headsUpRepository.setNotifications( FakeHeadsUpRowRepository(key = "key 0", PinnedStatus.NotPinned) ) runCurrent() - assertThat(statusBarHeadsUpState).isEqualTo(PinnedStatus.NotPinned) + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status).isEqualTo(PinnedStatus.NotPinned) } @Test fun statusBarHeadsUpState_whenShadeExpanded_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -463,13 +471,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // should emit `false`. kosmos.fakeShadeRepository.setLegacyShadeExpansion(1.0f) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_notificationsAreHidden_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -477,13 +487,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND the notifications are hidden keyguardViewStateRepository.areNotificationsFullyHidden.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onLockScreen_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -494,13 +506,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { testSetup = true, ) - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } @Test fun statusBarHeadsUpState_onByPassLockScreen_true() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN a row is pinned headsUpRepository.setNotifications(fakeHeadsUpRowRepository("key 0", isPinned = true)) @@ -513,13 +527,15 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isTrue() + assertThat(state).isInstanceOf(TopPinnedState.Pinned::class.java) + assertThat(status!!.isPinned).isTrue() } @Test fun statusBarHeadsUpState_onByPassLockScreen_withoutNotifications_false() = testScope.runTest { - val statusBarHeadsUpState by collectLastValue(underTest.statusBarHeadsUpState) + val state by collectLastValue(underTest.statusBarHeadsUpState) + val status by collectLastValue(underTest.statusBarHeadsUpStatus) // WHEN no pinned rows // AND the lock screen is shown @@ -530,7 +546,8 @@ class HeadsUpNotificationInteractorTest : SysuiTestCase() { // AND bypass is enabled faceAuthRepository.isBypassEnabled.value = true - assertThat(statusBarHeadsUpState!!.isPinned).isFalse() + assertThat(state).isEqualTo(TopPinnedState.NothingPinned) + assertThat(status!!.isPinned).isFalse() } private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 06b1c432955a..b3a60b052d08 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -37,7 +37,7 @@ import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.shadeTestUtil import com.android.systemui.shared.settings.data.repository.fakeSecureSettingsRepository -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter @@ -115,7 +115,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -133,7 +132,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, @@ -151,7 +149,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -183,7 +180,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // AND there are clearable notifications activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, @@ -217,7 +213,6 @@ class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { // AND there are clearable notifications activeNotificationListRepository.notifStats.value = NotifStats( - numActiveNotifs = 2, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 72a91bc12f8d..14bbd38ece2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -279,7 +279,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { notification); RemoteViews headerRemoteViews; if (lowPriority) { - headerRemoteViews = builder.makeLowPriorityContentView(true, false); + headerRemoteViews = builder.makeLowPriorityContentView(true); } else { headerRemoteViews = builder.makeNotificationGroupHeader(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 459778868ccd..a045b37a8119 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -70,6 +70,7 @@ import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition import com.android.systemui.testKosmos +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlin.test.assertIs @@ -1395,6 +1396,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S assertThat(stackAbsoluteBottom).isEqualTo(100F) } + @Test + fun blurRadius_emitsValues_fromPrimaryBouncerTransitions() = + testScope.runTest { + val blurRadius by collectLastValue(underTest.blurRadius) + assertThat(blurRadius).isEqualTo(0.0f) + + kosmos.fakeBouncerTransitions.first().notificationBlurRadius.value = 30.0f + assertThat(blurRadius).isEqualTo(30.0f) + + kosmos.fakeBouncerTransitions.last().notificationBlurRadius.value = 40.0f + assertThat(blurRadius).isEqualTo(40.0f) + } + private suspend fun TestScope.showLockscreen() { shadeTestUtil.setQsExpansion(0f) shadeTestUtil.setLockscreenShadeExpansion(0f) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 9eafcdbadfa5..e2330f448a1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.hardware.biometrics.BiometricSourceType; +import android.hardware.fingerprint.FingerprintManager; import android.os.Handler; import android.os.PowerManager; import android.os.UserHandle; @@ -431,9 +432,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { - // GIVEN UDFPS is supported - when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true); + public void onOpticalUdfpsConsecutivelyFailedThreeTimes_showPrimaryBouncer() { + // GIVEN optical UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(true); // WHEN udfps fails once - then don't show the bouncer yet mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); @@ -451,6 +452,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onUltrasonicUdfpsLockout_showPrimaryBouncer() { + // GIVEN ultrasonic UDFPS is supported + when(mUpdateMonitor.isOpticalUdfpsSupported()).thenReturn(false); + + // WHEN udfps fails three times, don't show bouncer + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + verify(mStatusBarKeyguardViewManager, never()).showPrimaryBouncer(anyBoolean()); + + // WHEN lockout is received + mBiometricUnlockController.onBiometricError(FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "Lockout", BiometricSourceType.FINGERPRINT); + + // THEN show bouncer + verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(true); + } + + @Test public void onFinishedGoingToSleep_authenticatesWhenPending() { when(mUpdateMonitor.isGoingToSleep()).thenReturn(true); mBiometricUnlockController.mWakefulnessObserver.onFinishedGoingToSleep(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt index ffd349d744a8..43ad042ecf78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.kt @@ -53,7 +53,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler -import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider +import com.android.systemui.statusbar.layout.mockStatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager import com.android.systemui.statusbar.policy.BatteryController @@ -153,7 +153,8 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { shadeViewStateProvider = TestShadeViewStateProvider() Mockito.`when`( - kosmos.statusBarContentInsetsProvider.getStatusBarContentInsetsForCurrentRotation() + kosmos.mockStatusBarContentInsetsProvider + .getStatusBarContentInsetsForCurrentRotation() ) .thenReturn(Insets.of(0, 0, 0, 0)) @@ -162,7 +163,7 @@ class KeyguardStatusBarViewControllerTest : SysuiTestCase() { Mockito.`when`(iconManagerFactory.create(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(iconManager) Mockito.`when`(statusBarContentInsetsProviderStore.defaultDisplay) - .thenReturn(kosmos.statusBarContentInsetsProvider) + .thenReturn(kosmos.mockStatusBarContentInsetsProvider) allowTestableLooperAsMainThread() looper.runWithLooper { keyguardStatusBarView = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt index e1589b6ad4bd..4e0393341faf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt @@ -45,13 +45,10 @@ import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.privacy.logging.PrivacyLogger -import com.android.systemui.screenrecord.RecordingController import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.policy.BluetoothController -import com.android.systemui.statusbar.policy.CastController -import com.android.systemui.statusbar.policy.CastDevice import com.android.systemui.statusbar.policy.DataSaverController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.HotspotController @@ -92,7 +89,6 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import org.mockito.kotlin.any -import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.reset @@ -108,8 +104,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { companion object { private const val ZEN_SLOT = "zen" private const val ALARM_SLOT = "alarm" - private const val CAST_SLOT = "cast" - private const val SCREEN_RECORD_SLOT = "screen_record" private const val CONNECTED_DISPLAY_SLOT = "connected_display" private const val MANAGED_PROFILE_SLOT = "managed_profile" } @@ -117,7 +111,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { @Mock private lateinit var iconController: StatusBarIconController @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher - @Mock private lateinit var castController: CastController @Mock private lateinit var hotspotController: HotspotController @Mock private lateinit var bluetoothController: BluetoothController @Mock private lateinit var nextAlarmController: NextAlarmController @@ -133,7 +126,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var devicePolicyManagerResources: DevicePolicyResourcesManager - @Mock private lateinit var recordingController: RecordingController @Mock private lateinit var telecomManager: TelecomManager @Mock private lateinit var sharedPreferences: SharedPreferences @Mock private lateinit var dateFormatUtil: DateFormatUtil @@ -302,101 +294,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { } @Test - @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun cast_chipsFlagOff_iconShown() { - statusBarPolicy.init() - clearInvocations(iconController) - - val callbackCaptor = argumentCaptor<CastController.Callback>() - verify(castController).addCallback(callbackCaptor.capture()) - - whenever(castController.castDevices) - .thenReturn( - listOf( - CastDevice( - "id", - "name", - "description", - CastDevice.CastState.Connected, - CastDevice.CastOrigin.MediaProjection, - ) - ) - ) - callbackCaptor.firstValue.onCastDevicesChanged() - - verify(iconController).setIconVisibility(CAST_SLOT, true) - } - - @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun cast_chipsFlagOn_noCallbackRegistered() { - statusBarPolicy.init() - - verify(castController, never()).addCallback(any()) - } - - @Test - @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun screenRecord_chipsFlagOff_iconShown_forAllStates() { - statusBarPolicy.init() - clearInvocations(iconController) - - val callbackCaptor = argumentCaptor<RecordingController.RecordingStateChangeCallback>() - verify(recordingController).addCallback(callbackCaptor.capture()) - - callbackCaptor.firstValue.onCountdown(3000) - testableLooper.processAllMessages() - verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true) - clearInvocations(iconController) - - callbackCaptor.firstValue.onCountdownEnd() - testableLooper.processAllMessages() - verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false) - clearInvocations(iconController) - - callbackCaptor.firstValue.onRecordingStart() - testableLooper.processAllMessages() - verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true) - clearInvocations(iconController) - - callbackCaptor.firstValue.onRecordingEnd() - testableLooper.processAllMessages() - verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false) - clearInvocations(iconController) - } - - @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun screenRecord_chipsFlagOn_noCallbackRegistered() { - statusBarPolicy.init() - - verify(recordingController, never()).addCallback(any()) - } - - @Test - @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun screenRecord_chipsFlagOn_methodsDoNothing() { - statusBarPolicy.init() - clearInvocations(iconController) - - statusBarPolicy.onCountdown(3000) - testableLooper.processAllMessages() - verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any()) - - statusBarPolicy.onCountdownEnd() - testableLooper.processAllMessages() - verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any()) - - statusBarPolicy.onRecordingStart() - testableLooper.processAllMessages() - verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any()) - - statusBarPolicy.onRecordingEnd() - testableLooper.processAllMessages() - verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any()) - } - - @Test @EnableFlags(android.app.Flags.FLAG_MODES_UI, android.app.Flags.FLAG_MODES_UI_ICONS) fun zenModeInteractorActiveModeChanged_showsModeIcon() = testScope.runTest { @@ -514,7 +411,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { executor, testableLooper.looper, context.resources, - castController, hotspotController, bluetoothController, nextAlarmController, @@ -530,7 +426,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { userManager, userTracker, devicePolicyManager, - recordingController, telecomManager, /* displayId = */ 0, sharedPreferences, @@ -589,11 +484,6 @@ class PhoneStatusBarPolicyTest : SysuiTestCase() { override fun getConfig(): ZenModeConfig = throw NotImplementedError() - fun setConsolidatedPolicy(policy: NotificationManager.Policy) { - this.consolidatedPolicy = policy - callbacks.forEach { it.onConsolidatedPolicyChanged(consolidatedPolicy) } - } - override fun getConsolidatedPolicy(): NotificationManager.Policy = consolidatedPolicy override fun getNextAlarm() = throw NotImplementedError() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt index cf512cdee800..6da06a36f63d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaRepoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt @@ -21,28 +21,22 @@ import android.app.IActivityManager import android.app.IUidObserver import android.app.PendingIntent import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.dump.DumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.plugins.activityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository @@ -53,7 +47,6 @@ import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingC import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent @@ -76,12 +69,10 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @OptIn(ExperimentalCoroutinesApi::class) -@EnableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP) @DisableFlags(StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaRepoTest : SysuiTestCase() { +class OngoingCallControllerTest : SysuiTestCase() { private val kosmos = Kosmos() - private val clock = kosmos.fakeSystemClock private val mainExecutor = kosmos.fakeExecutor private val testScope = kosmos.testScope private val statusBarModeRepository = kosmos.fakeStatusBarModeRepository @@ -92,7 +83,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { private val mockSwipeStatusBarAwayGestureHandler = mock<SwipeStatusBarAwayGestureHandler>() private val mockOngoingCallListener = mock<OngoingCallListener>() - private val mockActivityStarter = kosmos.activityStarter private val mockIActivityManager = mock<IActivityManager>() private val mockStatusBarWindowController = mock<StatusBarWindowController>() private val mockStatusBarWindowControllerStore = mock<StatusBarWindowControllerStore>() @@ -114,10 +104,7 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { testScope.backgroundScope, context, ongoingCallRepository, - mock<CommonNotifCollection>(), kosmos.activeNotificationsInteractor, - clock, - mockActivityStarter, mainExecutor, mockIActivityManager, DumpManager(), @@ -162,28 +149,7 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOff_repoHasNoNotifIcon() = - testScope.runTest { - val icon = mock<StatusBarIconView>() - setNotifOnRepo( - activeNotificationModel( - key = "ongoingNotif", - callType = CallType.Ongoing, - uid = CALL_UID, - statusBarChipIcon = icon, - whenTime = 567, - ) - ) - - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).notificationIconView).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun interactorHasOngoingCallNotif_notifIconFlagOn_repoHasNotifIcon() = + fun interactorHasOngoingCallNotif_repoHasNotifIcon() = testScope.runTest { val icon = mock<StatusBarIconView>() @@ -249,48 +215,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { ) } - /** Regression test for b/192379214. */ - @Test - @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun notifRepoHasCallNotif_notificationWhenIsZero_timeHidden() { - setNotifOnRepo( - activeNotificationModel( - key = "notif", - callType = CallType.Ongoing, - contentIntent = null, - whenTime = 0, - ) - ) - - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isEqualTo(0) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun notifRepoHasCallNotif_notificationWhenIsValid_timeShown() { - setNotifOnRepo( - activeNotificationModel( - key = "notif", - callType = CallType.Ongoing, - whenTime = clock.currentTimeMillis(), - ) - ) - - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - /** Regression test for b/194731244. */ @Test fun repoHasCallNotif_updatedManyTimes_uidObserverOnlyRegisteredOnce() { @@ -519,52 +443,6 @@ class OngoingCallControllerViaRepoTest : SysuiTestCase() { verify(mockOngoingCallListener).onOngoingCallStateChanged(any()) } - /** Regression test for b/212467440. */ - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() { - val pendingIntent = mock<PendingIntent>() - setNotifOnRepo( - activeNotificationModel( - key = "notif", - uid = CALL_UID, - contentIntent = pendingIntent, - callType = CallType.Ongoing, - ) - ) - - chipView.performClick() - - // Ensure that the sysui didn't modify the notification's intent -- see b/212467440. - verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_chipIsClickable() { - setCallNotifOnRepo() - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_newChipsEnabled_chipNotClickable() { - setCallNotifOnRepo() - - assertThat(chipView.hasOnClickListeners()).isFalse() - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun fullscreenIsTrue_chipStillClickable() { - setCallNotifOnRepo() - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - @Test fun callStartedInImmersiveMode_swipeGestureCallbackAdded() { statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt deleted file mode 100644 index cd3539d6b9a5..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt +++ /dev/null @@ -1,694 +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.systemui.statusbar.phone.ongoingcall - -import android.app.ActivityManager -import android.app.IActivityManager -import android.app.IUidObserver -import android.app.Notification -import android.app.PendingIntent -import android.app.Person -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import android.service.notification.NotificationListenerService.REASON_USER_STOPPED -import android.testing.TestableLooper -import android.view.LayoutInflater -import android.view.View -import android.widget.LinearLayout -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP -import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.res.R -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository -import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository -import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel -import com.android.systemui.statusbar.window.StatusBarWindowController -import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -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.anyString -import org.mockito.ArgumentMatchers.nullable -import org.mockito.Mock -import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.reset -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.whenever - -private const val CALL_UID = 900 - -// A process state that represents the process being visible to the user. -private const val PROC_STATE_VISIBLE = ActivityManager.PROCESS_STATE_TOP - -// A process state that represents the process being invisible to the user. -private const val PROC_STATE_INVISIBLE = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE - -@SmallTest -@RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) -@DisableFlags(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, StatusBarChipsModernization.FLAG_NAME) -class OngoingCallControllerViaListenerTest : SysuiTestCase() { - private val kosmos = Kosmos() - - private val clock = FakeSystemClock() - private val mainExecutor = FakeExecutor(clock) - private val testScope = TestScope() - private val statusBarModeRepository = FakeStatusBarModeRepository() - private val ongoingCallRepository = kosmos.ongoingCallRepository - - private lateinit var controller: OngoingCallController - private lateinit var notifCollectionListener: NotifCollectionListener - - @Mock - private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler - @Mock private lateinit var mockOngoingCallListener: OngoingCallListener - @Mock private lateinit var mockActivityStarter: ActivityStarter - @Mock private lateinit var mockIActivityManager: IActivityManager - @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController - @Mock private lateinit var mockStatusBarWindowControllerStore: StatusBarWindowControllerStore - - private lateinit var chipView: View - - @Before - fun setUp() { - allowTestableLooperAsMainThread() - TestableLooper.get(this).runWithLooper { - chipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - MockitoAnnotations.initMocks(this) - val notificationCollection = mock(CommonNotifCollection::class.java) - whenever(mockStatusBarWindowControllerStore.defaultDisplay) - .thenReturn(mockStatusBarWindowController) - - controller = - OngoingCallController( - testScope.backgroundScope, - context, - ongoingCallRepository, - notificationCollection, - kosmos.activeNotificationsInteractor, - clock, - mockActivityStarter, - mainExecutor, - mockIActivityManager, - DumpManager(), - mockStatusBarWindowControllerStore, - mockSwipeStatusBarAwayGestureHandler, - statusBarModeRepository, - logcatLogBuffer("OngoingCallControllerViaListenerTest"), - ) - controller.start() - controller.addCallback(mockOngoingCallListener) - controller.setChipView(chipView) - onTeardown { controller.tearDownChipView() } - - val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java) - verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture()) - notifCollectionListener = collectionListenerCaptor.value!! - - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(567) - notifCollectionListener.onEntryUpdated(notification.build()) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - val repoState = ongoingCallRepository.ongoingCallState.value - assertThat(repoState).isInstanceOf(OngoingCallModel.InCall::class.java) - assertThat((repoState as OngoingCallModel.InCall).startTimeMs).isEqualTo(567) - } - - @Test - fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true) - } - - @Test - fun onEntryUpdated_notOngoingCallNotif_listenerAndRepoNotNotified() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_listenerNotifiedTwice() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - verify(mockOngoingCallListener, times(2)).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryUpdated_ongoingCallNotifThenScreeningCallNotif_repoUpdated() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - /** Regression test for b/191472854. */ - @Test - fun onEntryUpdated_notifHasNullContentIntent_noCrash() { - notifCollectionListener.onEntryUpdated( - createCallNotifEntry(ongoingCallStyle, nullContentIntent = true) - ) - } - - /** Regression test for b/192379214. */ - @Test - @DisableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME, FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeHidden() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isEqualTo(0) - } - - @Test - @EnableFlags(android.app.Flags.FLAG_SORT_SECTION_BY_TIME) - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsZero_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(0) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun onEntryUpdated_notificationWhenIsValid_timeShown() { - val notification = NotificationEntryBuilder(createOngoingCallNotifEntry()) - notification.modifyNotification(context).setWhen(clock.currentTimeMillis()) - - notifCollectionListener.onEntryUpdated(notification.build()) - chipView.measure( - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), - ) - - assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth) - .isGreaterThan(0) - } - - /** Regression test for b/194731244. */ - @Test - fun onEntryUpdated_calledManyTimes_uidObserverOnlyRegisteredOnce() { - for (i in 0 until 4) { - // Re-create the notification each time so that it's considered a different object and - // will re-trigger the whole flow. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - verify(mockIActivityManager, times(1)).registerUidObserver(any(), any(), any(), any()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_getUidProcessStateThrowsException_noCrash() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_registerUidObserverThrowsException_noCrash() { - `when`( - mockIActivityManager.registerUidObserver( - any(), - any(), - any(), - nullable(String::class.java), - ) - ) - .thenThrow(SecurityException()) - - // No assert required, just check no crash - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - } - - /** Regression test for b/216248574. */ - @Test - fun entryUpdated_packageNameProvidedToActivityManager() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - val packageNameCaptor = ArgumentCaptor.forClass(String::class.java) - verify(mockIActivityManager) - .registerUidObserver(any(), any(), any(), packageNameCaptor.capture()) - assertThat(packageNameCaptor.value).isNotNull() - } - - /** - * If a call notification is never added before #onEntryRemoved is called, then the listener - * should never be notified. - */ - @Test - fun onEntryRemoved_callNotifNeverAddedBeforehand_listenerNotNotified() { - notifCollectionListener.onEntryRemoved(createOngoingCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_callNotifAddedThenRemoved_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockOngoingCallListener) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/188491504. */ - @Test - fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_repoUpdated() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - - // Create another notification based on the ongoing call one, but remove the features that - // made it a call notification. - val removedEntryBuilder = NotificationEntryBuilder(ongoingCallNotifEntry) - removedEntryBuilder.modifyNotification(context).style = null - - notifCollectionListener.onEntryRemoved(removedEntryBuilder.build(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.NoCall::class.java) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_listenerNotNotified() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - verify(mockOngoingCallListener, never()).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun onEntryRemoved_notifKeyDoesNotMatchOngoingCallNotif_repoNotUpdated() { - notifCollectionListener.onEntryAdded(createOngoingCallNotifEntry()) - - notifCollectionListener.onEntryRemoved(createNotCallNotifEntry(), REASON_USER_STOPPED) - - assertThat(ongoingCallRepository.ongoingCallState.value) - .isInstanceOf(OngoingCallModel.InCall::class.java) - } - - @Test - fun hasOngoingCall_noOngoingCallNotifSent_returnsFalse() { - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_unrelatedNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_screeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentAndCallAppNotVisible_returnsTrue() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButCallAppVisible_returnsFalse() { - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentButInvalidChipView_returnsFalse() { - val invalidChipView = LinearLayout(context) - controller.setChipView(invalidChipView) - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenRemoved_returnsFalse() { - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - - notifCollectionListener.onEntryUpdated(ongoingCallNotifEntry) - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenScreeningCallNotifSent_returnsFalse() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createScreeningCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isFalse() - } - - @Test - fun hasOngoingCall_ongoingCallNotifSentThenUnrelatedNotifSent_returnsTrue() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - notifCollectionListener.onEntryUpdated(createNotCallNotifEntry()) - - assertThat(controller.hasOngoingCall()).isTrue() - } - - /** - * This test fakes a theme change during an ongoing call. - * - * When a theme change happens, [CollapsedStatusBarFragment] and its views get re-created, so - * [OngoingCallController.setChipView] gets called with a new view. If there's an active ongoing - * call when the theme changes, the new view needs to be updated with the call information. - */ - @Test - fun setChipView_whenHasOngoingCallIsTrue_listenerNotifiedWithNewView() { - // Start an ongoing call. - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - lateinit var newChipView: View - TestableLooper.get(this).runWithLooper { - newChipView = - LayoutInflater.from(mContext).inflate(R.layout.ongoing_activity_chip_primary, null) - } - - // Change the chip view associated with the controller. - controller.setChipView(newChipView) - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToVisible_listenerNotified() { - // Start the call while the process is invisible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_INVISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to visible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_VISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - @Test - fun callProcessChangesToInvisible_listenerNotified() { - // Start the call while the process is visible. - `when`(mockIActivityManager.getUidProcessState(eq(CALL_UID), nullable(String::class.java))) - .thenReturn(PROC_STATE_VISIBLE) - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockOngoingCallListener) - - val captor = ArgumentCaptor.forClass(IUidObserver.Stub::class.java) - verify(mockIActivityManager) - .registerUidObserver(captor.capture(), any(), any(), nullable(String::class.java)) - val uidObserver = captor.value - - // Update the process to invisible. - uidObserver.onUidStateChanged(CALL_UID, PROC_STATE_INVISIBLE, 0, 0) - mainExecutor.advanceClockToLast() - mainExecutor.runAllReady() - - verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean()) - } - - /** Regression test for b/212467440. */ - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun chipClicked_activityStarterTriggeredWithUnmodifiedIntent() { - val notifEntry = createOngoingCallNotifEntry() - val pendingIntent = notifEntry.sbn.notification.contentIntent - notifCollectionListener.onEntryUpdated(notifEntry) - - chipView.performClick() - - // Ensure that the sysui didn't modify the notification's intent -- see b/212467440. - verify(mockActivityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent), any()) - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_chipIsClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun callNotificationAdded_newChipsEnabled_chipNotClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - assertThat(chipView.hasOnClickListeners()).isFalse() - } - - @Test - @DisableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) - fun fullscreenIsTrue_chipStillClickable() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - assertThat(chipView.hasOnClickListeners()).isTrue() - } - - // Swipe gesture tests - - @Test - fun callStartedInImmersiveMode_swipeGestureCallbackAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - verify(mockSwipeStatusBarAwayGestureHandler, never()) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionToImmersiveMode_swipeGestureCallbackAdded() { - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler) - .addOnGestureDetectedCallback(anyString(), any()) - } - - @Test - fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry()) - reset(mockSwipeStatusBarAwayGestureHandler) - - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false - testScope.runCurrent() - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - @Test - fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() { - statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true - testScope.runCurrent() - val ongoingCallNotifEntry = createOngoingCallNotifEntry() - notifCollectionListener.onEntryAdded(ongoingCallNotifEntry) - reset(mockSwipeStatusBarAwayGestureHandler) - - notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED) - - verify(mockSwipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(anyString()) - } - - // TODO(b/195839150): Add test - // swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's - // difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s - // callbacks in test. - - // END swipe gesture tests - - private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle) - - private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle) - - private fun createCallNotifEntry( - callStyle: Notification.CallStyle, - nullContentIntent: Boolean = false, - ): NotificationEntry { - val notificationEntryBuilder = NotificationEntryBuilder() - notificationEntryBuilder.modifyNotification(context).style = callStyle - notificationEntryBuilder.setUid(CALL_UID) - - if (nullContentIntent) { - notificationEntryBuilder.modifyNotification(context).setContentIntent(null) - } else { - val contentIntent = mock(PendingIntent::class.java) - notificationEntryBuilder.modifyNotification(context).setContentIntent(contentIntent) - } - - return notificationEntryBuilder.build() - } - - private fun createNotCallNotifEntry() = NotificationEntryBuilder().build() -} - -private val person = Person.Builder().setName("name").build() -private val hangUpIntent = mock(PendingIntent::class.java) - -private val ongoingCallStyle = Notification.CallStyle.forOngoingCall(person, hangUpIntent) -private val screeningCallStyle = - Notification.CallStyle.forScreeningCall( - person, - hangUpIntent, - /* answerIntent= */ mock(PendingIntent::class.java), - ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt index a9db0b70dd4d..937f333b0065 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeHomeStatusBarViewModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeHomeStatusBarViewModel( override val operatorNameViewModel: StatusBarOperatorNameViewModel ) : HomeStatusBarViewModel { - private val areNotificationLightsOut = MutableStateFlow(false) + override val areNotificationsLightsOut = MutableStateFlow(false) override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false) @@ -46,6 +46,8 @@ class FakeHomeStatusBarViewModel( override val isHomeStatusBarAllowedByScene = MutableStateFlow(false) + override val shouldHomeStatusBarBeVisible = MutableStateFlow(false) + override val shouldShowOperatorNameView = MutableStateFlow(false) override val isClockVisible = @@ -77,14 +79,14 @@ class FakeHomeStatusBarViewModel( override val iconBlockList: MutableStateFlow<List<String>> = MutableStateFlow(listOf()) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut + override val contentArea = MutableStateFlow(Rect(0, 0, 1, 1)) val darkRegions = mutableListOf<Rect>() var darkIconTint = Color.BLACK var lightIconTint = Color.WHITE - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = MutableStateFlow( StatusBarTintColor { viewBounds -> if (DarkIconDispatcher.isInAreas(darkRegions, viewBounds)) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt index e91875cd0885..be4af868b740 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt @@ -22,13 +22,17 @@ import android.app.StatusBarManager.DISABLE_CLOCK import android.app.StatusBarManager.DISABLE_NONE import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS import android.app.StatusBarManager.DISABLE_SYSTEM_INFO +import android.content.testableContext import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.view.Display.DEFAULT_DISPLAY import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.display.data.repository.fake import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository @@ -59,7 +63,6 @@ import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip import com.android.systemui.statusbar.data.model.StatusBarMode -import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel @@ -85,6 +88,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.Before import org.junit.Test @@ -104,6 +108,9 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { setUpPackageManagerForMediaProjection(kosmos) } + @Before + fun addDisplays() = runBlocking { kosmos.displayRepository.fake.addDisplay(DEFAULT_DISPLAY) } + @Test fun isTransitioningFromLockscreenToOccluded_started_isTrue() = kosmos.runTest { @@ -363,7 +370,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isTrue() } @@ -377,7 +384,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -391,7 +398,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(emptyList()) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -405,7 +412,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { activeNotificationListRepository.activeNotifications.value = activeNotificationsStore(testNotifications) - val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID)) + val actual by collectLastValue(underTest.areNotificationsLightsOut) assertThat(actual).isFalse() } @@ -415,7 +422,7 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { fun areNotificationsLightsOut_requiresFlagEnabled() = kosmos.runTest { assertLogsWtf { - val flow = underTest.areNotificationsLightsOut(DISPLAY_ID) + val flow = underTest.areNotificationsLightsOut assertThat(flow).isEqualTo(emptyFlow<Boolean>()) } } @@ -568,6 +575,98 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { } @Test + fun shouldHomeStatusBarBeVisible_keyguardNotGone_noHun_false() = + kosmos.runTest { + // Do not transition from keyguard. i.e., we don't call transitionKeyguardToGone() + + // Nothing disabled + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + + val latest by collectLastValue(underTest.shouldHomeStatusBarBeVisible) + assertThat(latest).isFalse() + } + + @Test + fun shouldHomeStatusBarBeVisible_keyguardNotGone_hun_true() = + kosmos.runTest { + // Keyguard gone + transitionKeyguardToGone() + + // Nothing disabled + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + + // there is an active HUN + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + val latest by collectLastValue(underTest.shouldHomeStatusBarBeVisible) + assertThat(latest).isTrue() + } + + @Test + fun shouldHomeStatusBarBeVisible_keyguardGone_noHun_notInCamera_true() = + kosmos.runTest { + // Keyguard gone + transitionKeyguardToGone() + + // Nothing disabled + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + + val latest by collectLastValue(underTest.shouldHomeStatusBarBeVisible) + assertThat(latest).isTrue() + } + + @Test + fun shouldHomeStatusBarBeVisible_keyguardGone_hun_notInCamera_true() = + kosmos.runTest { + // Keyguard gone + transitionKeyguardToGone() + + // Nothing disabled + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + + // there is an active HUN + headsUpNotificationRepository.setNotifications( + UnconfinedFakeHeadsUpRowRepository( + key = "key", + pinnedStatus = MutableStateFlow(PinnedStatus.PinnedByUser), + ) + ) + + val latest by collectLastValue(underTest.shouldHomeStatusBarBeVisible) + assertThat(latest).isTrue() + } + + @Test + fun shouldHomeStatusBarBeVisible_keyguardGone_noHun_inCamera_false() = + kosmos.runTest { + // Keyguard gone + transitionKeyguardToGone() + + // Nothing disabled + fakeDisableFlagsRepository.disableFlags.value = + DisableFlagsModel(DISABLE_NONE, DISABLE2_NONE) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + testScope = testScope, + ) + kosmos.keyguardInteractor.onCameraLaunchDetected(CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP) + + val latest by collectLastValue(underTest.shouldHomeStatusBarBeVisible) + assertThat(latest).isFalse() + } + + @Test fun isClockVisible_allowedByDisableFlags_visible() = kosmos.runTest { val latest by collectLastValue(underTest.isClockVisible) @@ -1005,11 +1104,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_getsDarkTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(1, 1, 3, 3)) @@ -1019,11 +1118,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsNotInDarkBounds_getsDefaultTint() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) val tint = areaTint?.tint(Rect(6, 6, 7, 7)) @@ -1033,11 +1132,11 @@ class HomeStatusBarViewModelImplTest : SysuiTestCase() { @Test fun areaTint_viewIsInDarkBounds_darkBoundsChange_viewUpdates() = kosmos.runTest { - val displayId = 321 + val displayId = testableContext.displayId fakeDarkIconRepository.darkState(displayId).value = SysuiDarkIconDispatcher.DarkChange(listOf(Rect(0, 0, 5, 5)), 0f, 0xAABBCC) - val areaTint by collectLastValue(underTest.areaTint(displayId)) + val areaTint by collectLastValue(underTest.areaTint) var tint = areaTint?.tint(Rect(1, 1, 3, 3)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModelTest.kt index 20cc85f08b01..8608b0bf2f0b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModelTest.kt @@ -39,14 +39,12 @@ class StatusBarOperatorNameViewModelTest : SysuiTestCase() { kosmos.runTest { val intr1 = fakeMobileIconsInteractor.getMobileConnectionInteractorForSubId(1) val intr2 = fakeMobileIconsInteractor.getMobileConnectionInteractorForSubId(2) - val invalidIntr = fakeMobileIconsInteractor.getMobileConnectionInteractorForSubId(-1) // GIVEN default data subId is 1 fakeMobileIconsInteractor.defaultDataSubId.value = 1 intr1.carrierName.value = "Test Name 1" intr2.carrierName.value = "Test Name 2" - invalidIntr.carrierName.value = "default network name" val latest by collectLastValue(underTest.operatorName) @@ -56,8 +54,19 @@ class StatusBarOperatorNameViewModelTest : SysuiTestCase() { assertThat(latest).isEqualTo("Test Name 2") - fakeMobileIconsInteractor.defaultDataSubId.value = -1 + fakeMobileIconsInteractor.defaultDataSubId.value = null - assertThat(latest).isEqualTo("default network name") + assertThat(latest).isNull() + } + + @Test + fun operatorName_noDefaultDataSubId_null() = + kosmos.runTest { + // GIVEN defaultDataSubId is null + fakeMobileIconsInteractor.defaultDataSubId.value = null + + val latest by collectLastValue(underTest.operatorName) + + assertThat(latest).isNull() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt index c6bae197ad76..7802b921eae0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest import com.android.internal.R import com.android.settingslib.notification.data.repository.updateNotificationPolicy import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.volume.shared.model.AudioStream import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -204,7 +205,7 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun shouldAskForZenDuration_changesWithSetting() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE + val manualDnd = TestModeBuilder().makeManualDnd().setActive(true).build() settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() @@ -233,29 +234,27 @@ class ZenModeInteractorTest : SysuiTestCase() { @Test fun activateMode_usesCorrectDuration() = testScope.runTest { - val manualDnd = TestModeBuilder.MANUAL_DND_ACTIVE - zenModeRepository.addModes(listOf(manualDnd)) settingsRepository.setInt(ZEN_DURATION, ZEN_DURATION_FOREVER) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)).isNull() + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)).isNull() - zenModeRepository.deactivateMode(manualDnd.id) + zenModeRepository.deactivateMode(MANUAL_DND) settingsRepository.setInt(ZEN_DURATION, 60) runCurrent() - underTest.activateMode(manualDnd) - assertThat(zenModeRepository.getModeActiveDuration(manualDnd.id)) + underTest.activateMode(MANUAL_DND) + assertThat(zenModeRepository.getModeActiveDuration(MANUAL_DND.id)) .isEqualTo(Duration.ofMinutes(60)) } @Test fun deactivateAllModes_updatesCorrectModes() = testScope.runTest { + zenModeRepository.activateMode(MANUAL_DND) zenModeRepository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder().setName("Inactive").setActive(false).build(), TestModeBuilder().setName("Active").setActive(true).build(), ) @@ -389,12 +388,9 @@ class ZenModeInteractorTest : SysuiTestCase() { testScope.runTest { val dndMode by collectLastValue(underTest.dndMode) - zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE) - runCurrent() - assertThat(dndMode!!.isActive).isFalse() - zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() assertThat(dndMode!!.isActive).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt index 07d088bbb3d6..856de8ee1c80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -28,6 +28,7 @@ import android.service.notification.ZenModeConfig.ScheduleInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue @@ -111,7 +112,6 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setName("Disabled by other") .setEnabled(false, /* byUser= */ false) .build(), - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setName("Enabled") .setEnabled(true) @@ -128,14 +128,14 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles).hasSize(3) with(tiles?.elementAt(0)!!) { - assertThat(this.text).isEqualTo("Disabled by other") - assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(1)!!) { - assertThat(this.text).isEqualTo("Do Not Disturb") - assertThat(this.subtext).isEqualTo("On") - assertThat(this.enabled).isEqualTo(true) + assertThat(this.text).isEqualTo("Disabled by other") + assertThat(this.subtext).isEqualTo("Not set") + assertThat(this.enabled).isEqualTo(false) } with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Enabled") @@ -176,18 +176,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Do Not Disturb") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.text).isEqualTo("Active with manual") assertThat(this.subtext).isEqualTo("On • trigger description") assertThat(this.enabled).isEqualTo(true) } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.text).isEqualTo("Inactive with manual") assertThat(this.subtext).isEqualTo("Off") assertThat(this.enabled).isEqualTo(false) @@ -226,10 +232,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) // Check that tile is initially present - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("On") assertThat(this.enabled).isEqualTo(true) @@ -239,8 +246,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() } // Check that tile is still present at the same location, but turned off - assertThat(tiles).hasSize(3) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(4) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -252,9 +259,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { runCurrent() // Check that tile is now gone - assertThat(tiles2).hasSize(2) - assertThat(tiles2?.elementAt(0)!!.text).isEqualTo("Active with manual") - assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Inactive with manual") + assertThat(tiles2).hasSize(3) + assertThat(tiles2?.elementAt(1)!!.text).isEqualTo("Active with manual") + assertThat(tiles2?.elementAt(2)!!.text).isEqualTo("Inactive with manual") } @Test @@ -287,22 +294,23 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(3) + // Manual DND is included by default + assertThat(tiles).hasSize(4) repository.removeMode("A") runCurrent() - assertThat(tiles).hasSize(2) + assertThat(tiles).hasSize(3) repository.removeMode("B") runCurrent() - assertThat(tiles).hasSize(1) + assertThat(tiles).hasSize(2) repository.removeMode("C") runCurrent() - assertThat(tiles).hasSize(0) + assertThat(tiles).hasSize(1) } @Test @@ -353,14 +361,15 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - assertThat(tiles!![0].subtext).isEqualTo("When the going gets tough") - assertThat(tiles!![1].subtext).isEqualTo("On • When in Rome") - assertThat(tiles!![2].subtext).isEqualTo("Not set") - assertThat(tiles!![3].subtext).isEqualTo("Off") - assertThat(tiles!![4].subtext).isEqualTo("On") - assertThat(tiles!![5].subtext).isEqualTo("Not set") - assertThat(tiles!![6].subtext).isEqualTo(timeScheduleMode.triggerDescription) + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + assertThat(tiles!![1].subtext).isEqualTo("When the going gets tough") + assertThat(tiles!![2].subtext).isEqualTo("On • When in Rome") + assertThat(tiles!![3].subtext).isEqualTo("Not set") + assertThat(tiles!![4].subtext).isEqualTo("Off") + assertThat(tiles!![5].subtext).isEqualTo("On") + assertThat(tiles!![6].subtext).isEqualTo("Not set") + assertThat(tiles!![7].subtext).isEqualTo(timeScheduleMode.triggerDescription) } @Test @@ -411,32 +420,33 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles!!).hasSize(7) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles!!).hasSize(8) + with(tiles?.elementAt(1)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("When the going gets tough") } - with(tiles?.elementAt(1)!!) { + with(tiles?.elementAt(2)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEqualTo("When in Rome") } - with(tiles?.elementAt(2)!!) { + with(tiles?.elementAt(3)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(3)!!) { + with(tiles?.elementAt(4)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(4)!!) { + with(tiles?.elementAt(5)!!) { assertThat(this.stateDescription).isEqualTo("On") assertThat(this.subtextDescription).isEmpty() } - with(tiles?.elementAt(5)!!) { + with(tiles?.elementAt(6)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription).isEqualTo("Not set") } - with(tiles?.elementAt(6)!!) { + with(tiles?.elementAt(7)!!) { assertThat(this.stateDescription).isEqualTo("Off") assertThat(this.subtextDescription) .isEqualTo( @@ -456,31 +466,30 @@ class ModesDialogViewModelTest : SysuiTestCase() { val tiles by collectLastValue(underTest.tiles) val modeId = "id" - repository.addModes( - listOf( - TestModeBuilder() - .setId(modeId) - .setName("Test") - .setManualInvocationAllowed(true) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - assertThat(tiles?.elementAt(0)?.enabled).isFalse() + // Manual DND is included by default + assertThat(tiles).hasSize(2) + assertThat(tiles?.elementAt(1)?.enabled).isFalse() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isTrue() + assertThat(tiles?.elementAt(1)?.enabled).isTrue() // Trigger onClick - tiles?.first()?.onClick?.let { it() } + tiles?.elementAt(1)?.onClick?.let { it() } runCurrent() - assertThat(tiles?.first()?.enabled).isFalse() + assertThat(tiles?.elementAt(1)?.enabled).isFalse() } @Test @@ -489,25 +498,24 @@ class ModesDialogViewModelTest : SysuiTestCase() { val job = Job() val tiles by collectLastValue(underTest.tiles, context = job) - repository.addModes( - listOf( - TestModeBuilder() - .setName("Active without manual") - .setActive(true) - .setManualInvocationAllowed(false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) + // Manual DND is included by default + assertThat(tiles).hasSize(2) // Click tile to toggle it off - tiles?.elementAt(0)!!.onClick() + tiles?.elementAt(1)!!.onClick() runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -518,7 +526,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { } // Check that nothing happened - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Active without manual") assertThat(this.subtext).isEqualTo("Manage in settings") assertThat(this.enabled).isEqualTo(false) @@ -530,19 +538,18 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder() - .setId("ID") - .setName("Disabled by other") - .setEnabled(false, /* byUser= */ false) - .build() - ) + repository.addMode( + TestModeBuilder() + .setId("ID") + .setName("Disabled by other") + .setEnabled(false, /* byUser= */ false) + .build() ) runCurrent() - assertThat(tiles).hasSize(1) - with(tiles?.elementAt(0)!!) { + // Manual DND is included by default + assertThat(tiles).hasSize(2) + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -561,7 +568,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { .isEqualTo("ID") // Check that nothing happened to the tile - with(tiles?.elementAt(0)!!) { + with(tiles?.elementAt(1)!!) { assertThat(this.text).isEqualTo("Disabled by other") assertThat(this.subtext).isEqualTo("Not set") assertThat(this.enabled).isEqualTo(false) @@ -593,10 +600,11 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() - assertThat(tiles).hasSize(2) + // Manual DND is included by default + assertThat(tiles).hasSize(3) // Trigger onLongClick for A - tiles?.first()?.onLongClick?.let { it() } + tiles?.elementAt(1)?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent @@ -625,9 +633,9 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) + repository.activateMode(MANUAL_DND) repository.addModes( listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, TestModeBuilder() .setId("id1") .setName("Inactive Mode One") @@ -644,6 +652,7 @@ class ModesDialogViewModelTest : SysuiTestCase() { ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(3) // Trigger onClick for each tile in sequence @@ -672,19 +681,17 @@ class ModesDialogViewModelTest : SysuiTestCase() { testScope.runTest { val tiles by collectLastValue(underTest.tiles) - repository.addModes( - listOf( - TestModeBuilder.MANUAL_DND_ACTIVE, - TestModeBuilder() - .setId("id1") - .setName("Inactive Mode One") - .setActive(false) - .setManualInvocationAllowed(true) - .build(), - ) + repository.addMode( + TestModeBuilder() + .setId("id1") + .setName("Inactive Mode One") + .setActive(false) + .setManualInvocationAllowed(true) + .build() ) runCurrent() + // Manual DND is included by default assertThat(tiles).hasSize(2) val modeCaptor = argumentCaptor<ZenMode>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt index 61c719319de8..824955de83e2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImplTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.policy.statusBarConfigurationController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -41,13 +42,18 @@ class StatusBarWindowControllerImplTest : SysuiTestCase() { private val kosmos = testKosmos().also { it.statusBarWindowViewInflater = it.fakeStatusBarWindowViewInflater } - private val underTest = kosmos.statusBarWindowControllerImpl + private lateinit var underTest: StatusBarWindowControllerImpl private val fakeExecutor = kosmos.fakeExecutor private val fakeWindowManager = kosmos.fakeWindowManager private val mockFragmentService = kosmos.fragmentService private val fakeStatusBarWindowViewInflater = kosmos.fakeStatusBarWindowViewInflater private val statusBarConfigurationController = kosmos.statusBarConfigurationController + @Before + fun setUp() { + underTest = kosmos.statusBarWindowControllerImpl + } + @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun attach_connectedDisplaysFlagEnabled_setsConfigControllerOnWindowView() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt new file mode 100644 index 000000000000..fec186e862be --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2025 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.volume.dialog.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class VolumeDialogCallbacksInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos().apply { useUnconfinedTestDispatcher() } + + private val underTest: VolumeDialogCallbacksInteractor by lazy { + kosmos.volumeDialogCallbacksInteractor + } + + @Test + fun initialEvent_isSubscribedToEvents() = + kosmos.runTest { + val event by collectLastValue(underTest.event) + assertThat(event).isInstanceOf(VolumeDialogEventModel.SubscribedToEvents::class.java) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt index bfafdab003aa..1001c245cc95 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt @@ -16,75 +16,115 @@ package com.android.systemui.volume.dialog.sliders.domain.interactor +import android.media.AudioManager +import android.service.notification.ZenPolicy import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.plugins.VolumeDialogController import com.android.systemui.plugins.fakeVolumeDialogController +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.testKosmos +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper class VolumeDialogSliderInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private lateinit var underTest: VolumeDialogSliderInteractor + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + zenModeRepository.addMode( + TestModeBuilder() + .setName("Blocks media, Active") + .setZenPolicy(ZenPolicy.Builder().allowMedia(false).build()) + .setActive(true) + .build() + ) + } - @Before - fun setUp() { - underTest = kosmos.volumeDialogSliderInteractor + private val underTest: VolumeDialogSliderInteractor by lazy { + kosmos.volumeDialogSliderInteractor } @Test fun settingStreamVolume_setsActiveStream() = - with(kosmos) { - testScope.runTest { - runCurrent() - // initialize the stream model - fakeVolumeDialogController.setStreamVolume(volumeDialogSliderType.audioStream, 0) + kosmos.runTest { + // initialize the stream model + fakeVolumeDialogController.setStreamVolume(volumeDialogSliderType.audioStream, 0) - val sliderModel by collectLastValue(underTest.slider) - underTest.setStreamVolume(1) - runCurrent() + val sliderModel by collectLastValue(underTest.slider) + underTest.setStreamVolume(1) - assertThat(sliderModel!!.isActive).isTrue() - } + assertThat(sliderModel!!.isActive).isTrue() } @Test fun streamVolumeIs_minMaxAreEnforced() = - with(kosmos) { - testScope.runTest { - runCurrent() - fakeVolumeDialogController.updateState { - states.put( - volumeDialogSliderType.audioStream, - VolumeDialogController.StreamState().apply { - levelMin = 0 - level = 2 - levelMax = 1 - }, - ) - } - - val sliderModel by collectLastValue(underTest.slider) - runCurrent() - - assertThat(sliderModel!!.level).isEqualTo(1) + kosmos.runTest { + fakeVolumeDialogController.updateState { + states.put( + volumeDialogSliderType.audioStream, + VolumeDialogController.StreamState().apply { + levelMin = 0 + level = 2 + levelMax = 1 + }, + ) } + + val sliderModel by collectLastValue(underTest.slider) + + assertThat(sliderModel!!.level).isEqualTo(1) + } + + @Test + fun streamCantBeBlockedByZenMode_isDisabledByZenMode_false() = + kosmos.runTest { + volumeDialogSliderType = VolumeDialogSliderType.Stream(AudioManager.STREAM_VOICE_CALL) + + val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode) + + assertThat(isDisabledByZenMode).isFalse() + } + + @Test + fun remoteMediaStream_zenModeRestrictive_IsNotDisabledByZenMode() = + kosmos.runTest { + volumeDialogSliderType = VolumeDialogSliderType.RemoteMediaStream(0) + + val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode) + + assertThat(isDisabledByZenMode).isFalse() + } + + @Test + fun audioSharingStream_zenModeRestrictive_IsNotDisabledByZenMode() = + kosmos.runTest { + volumeDialogSliderType = VolumeDialogSliderType.AudioSharingStream(0) + + val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode) + + assertThat(isDisabledByZenMode).isFalse() + } + + @Test + fun streamBlockedByZenMode_isDisabledByZenMode_true() = + kosmos.runTest { + volumeDialogSliderType = VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC) + + val isDisabledByZenMode by collectLastValue(underTest.isDisabledByZenMode) + + assertThat(isDisabledByZenMode).isTrue() } } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt index d84d89087349..812a964bf8bc 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt @@ -30,10 +30,6 @@ data class ClockConfig( /** Transition to AOD should move smartspace like large clock instead of small clock */ val useAlternateSmartspaceAODTransition: Boolean = false, - /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */ - @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone") - val isReactiveToTone: Boolean = true, - /** True if the clock is large frame clock, which will use weather in compose. */ val useCustomClockScene: Boolean = false, ) diff --git a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt index d93f7d3093b8..81156d9698d8 100644 --- a/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt +++ b/packages/SystemUI/plugin_core/processor/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt @@ -24,6 +24,7 @@ import javax.annotation.processing.RoundEnvironment import javax.lang.model.element.Element import javax.lang.model.element.ElementKind import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier import javax.lang.model.element.PackageElement import javax.lang.model.element.TypeElement import javax.lang.model.type.TypeKind @@ -183,11 +184,17 @@ class ProtectedPluginProcessor : AbstractProcessor() { // Method implementations for (method in methods) { val methodName = method.simpleName + if (methods.any { methodName.startsWith("${it.simpleName}\$") }) { + continue + } val returnTypeName = method.returnType.toString() val callArgs = StringBuilder() var isFirst = true + val isStatic = method.modifiers.contains(Modifier.STATIC) - line("@Override") + if (!isStatic) { + line("@Override") + } parenBlock("public $returnTypeName $methodName") { // While copying the method signature for the proxy type, we also // accumulate arguments for the nested callsite. @@ -202,7 +209,8 @@ class ProtectedPluginProcessor : AbstractProcessor() { } val isVoid = method.returnType.kind == TypeKind.VOID - val nestedCall = "mInstance.$methodName($callArgs)" + val methodContainer = if (isStatic) sourceName else "mInstance" + val nestedCall = "$methodContainer.$methodName($callArgs)" val callStatement = when { isVoid -> "$nestedCall;" diff --git a/packages/SystemUI/proguard_common.flags b/packages/SystemUI/proguard_common.flags index 162d8aebfc62..02b2bcf8e40d 100644 --- a/packages/SystemUI/proguard_common.flags +++ b/packages/SystemUI/proguard_common.flags @@ -1,5 +1,11 @@ -include proguard_kotlin.flags --keep class com.android.systemui.VendorServices + +# VendorServices implements CoreStartable and may be instantiated reflectively in +# SystemUIApplication#startAdditionalStartable. +# TODO(b/373579455): Rewrite this to a @UsesReflection keep annotation. +-keep class com.android.systemui.VendorServices { + public void <init>(); +} # Needed to ensure callback field references are kept in their respective # owning classes when the downstream callback registrars only store weak refs. diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml new file mode 100644 index 000000000000..f8c0fa04cd39 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_connecting_status_container.xml @@ -0,0 +1,199 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.45561" + android:valueTo="0.69699" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.6288400000000001" + android:valueTo="0.81618" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.69699" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.81618" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="0" + android:valueFrom="90" + android:valueTo="135" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="500" + android:propertyName="rotation" + android:startOffset="500" + android:valueFrom="135" + android:valueTo="180" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="scaleX" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.05063" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="scaleY" + android:startOffset="1000" + android:valueFrom="0.0434" + android:valueTo="0.042350000000000006" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleX" + android:startOffset="1083" + android:valueFrom="0.05063" + android:valueTo="0.06146" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="417" + android:propertyName="scaleY" + android:startOffset="1083" + android:valueFrom="0.042350000000000006" + android:valueTo="0.040780000000000004" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="1017" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " /> + </group> + <group + android:name="_R_G_L_0_G" + android:rotation="0" + android:scaleX="0.06146" + android:scaleY="0.040780000000000004" + android:translateX="44" + android:translateY="28"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M-0.65 -437.37 C-0.65,-437.37 8.33,-437.66 8.33,-437.66 C8.33,-437.66 17.31,-437.95 17.31,-437.95 C17.31,-437.95 26.25,-438.78 26.25,-438.78 C26.25,-438.78 35.16,-439.95 35.16,-439.95 C35.16,-439.95 44.07,-441.11 44.07,-441.11 C44.07,-441.11 52.85,-443 52.85,-443 C52.85,-443 61.6,-445.03 61.6,-445.03 C61.6,-445.03 70.35,-447.09 70.35,-447.09 C70.35,-447.09 78.91,-449.83 78.91,-449.83 C78.91,-449.83 87.43,-452.67 87.43,-452.67 C87.43,-452.67 95.79,-455.97 95.79,-455.97 C95.79,-455.97 104.11,-459.35 104.11,-459.35 C104.11,-459.35 112.36,-462.93 112.36,-462.93 C112.36,-462.93 120.6,-466.51 120.6,-466.51 C120.6,-466.51 128.84,-470.09 128.84,-470.09 C128.84,-470.09 137.09,-473.67 137.09,-473.67 C137.09,-473.67 145.49,-476.84 145.49,-476.84 C145.49,-476.84 153.9,-480.01 153.9,-480.01 C153.9,-480.01 162.31,-483.18 162.31,-483.18 C162.31,-483.18 170.98,-485.54 170.98,-485.54 C170.98,-485.54 179.66,-487.85 179.66,-487.85 C179.66,-487.85 188.35,-490.15 188.35,-490.15 C188.35,-490.15 197.22,-491.58 197.22,-491.58 C197.22,-491.58 206.09,-493.01 206.09,-493.01 C206.09,-493.01 214.98,-494.28 214.98,-494.28 C214.98,-494.28 223.95,-494.81 223.95,-494.81 C223.95,-494.81 232.93,-495.33 232.93,-495.33 C232.93,-495.33 241.9,-495.5 241.9,-495.5 C241.9,-495.5 250.88,-495.13 250.88,-495.13 C250.88,-495.13 259.86,-494.75 259.86,-494.75 C259.86,-494.75 268.78,-493.78 268.78,-493.78 C268.78,-493.78 277.68,-492.52 277.68,-492.52 C277.68,-492.52 286.57,-491.26 286.57,-491.26 C286.57,-491.26 295.31,-489.16 295.31,-489.16 C295.31,-489.16 304.04,-487.04 304.04,-487.04 C304.04,-487.04 312.7,-484.65 312.7,-484.65 C312.7,-484.65 321.19,-481.72 321.19,-481.72 C321.19,-481.72 329.68,-478.78 329.68,-478.78 C329.68,-478.78 337.96,-475.31 337.96,-475.31 C337.96,-475.31 346.14,-471.59 346.14,-471.59 C346.14,-471.59 354.3,-467.82 354.3,-467.82 C354.3,-467.82 362.11,-463.38 362.11,-463.38 C362.11,-463.38 369.92,-458.93 369.92,-458.93 C369.92,-458.93 377.53,-454.17 377.53,-454.17 C377.53,-454.17 384.91,-449.04 384.91,-449.04 C384.91,-449.04 392.29,-443.91 392.29,-443.91 C392.29,-443.91 399.26,-438.24 399.26,-438.24 C399.26,-438.24 406.15,-432.48 406.15,-432.48 C406.15,-432.48 412.92,-426.57 412.92,-426.57 C412.92,-426.57 419.27,-420.22 419.27,-420.22 C419.27,-420.22 425.62,-413.87 425.62,-413.87 C425.62,-413.87 431.61,-407.18 431.61,-407.18 C431.61,-407.18 437.38,-400.29 437.38,-400.29 C437.38,-400.29 443.14,-393.39 443.14,-393.39 C443.14,-393.39 448.27,-386.01 448.27,-386.01 C448.27,-386.01 453.4,-378.64 453.4,-378.64 C453.4,-378.64 458.26,-371.09 458.26,-371.09 C458.26,-371.09 462.71,-363.28 462.71,-363.28 C462.71,-363.28 467.16,-355.47 467.16,-355.47 C467.16,-355.47 471.03,-347.37 471.03,-347.37 C471.03,-347.37 474.75,-339.19 474.75,-339.19 C474.75,-339.19 478.34,-330.95 478.34,-330.95 C478.34,-330.95 481.28,-322.46 481.28,-322.46 C481.28,-322.46 484.21,-313.97 484.21,-313.97 C484.21,-313.97 486.72,-305.35 486.72,-305.35 C486.72,-305.35 488.84,-296.62 488.84,-296.62 C488.84,-296.62 490.96,-287.88 490.96,-287.88 C490.96,-287.88 492.33,-279.01 492.33,-279.01 C492.33,-279.01 493.59,-270.11 493.59,-270.11 C493.59,-270.11 494.69,-261.2 494.69,-261.2 C494.69,-261.2 495.07,-252.22 495.07,-252.22 C495.07,-252.22 495.44,-243.24 495.44,-243.24 C495.44,-243.24 495.41,-234.27 495.41,-234.27 C495.41,-234.27 494.88,-225.29 494.88,-225.29 C494.88,-225.29 494.35,-216.32 494.35,-216.32 C494.35,-216.32 493.22,-207.42 493.22,-207.42 C493.22,-207.42 491.79,-198.55 491.79,-198.55 C491.79,-198.55 490.36,-189.68 490.36,-189.68 C490.36,-189.68 488.19,-180.96 488.19,-180.96 C488.19,-180.96 485.88,-172.28 485.88,-172.28 C485.88,-172.28 483.56,-163.6 483.56,-163.6 C483.56,-163.6 480.48,-155.16 480.48,-155.16 C480.48,-155.16 477.31,-146.75 477.31,-146.75 C477.31,-146.75 474.14,-138.34 474.14,-138.34 C474.14,-138.34 470.62,-130.07 470.62,-130.07 C470.62,-130.07 467.04,-121.83 467.04,-121.83 C467.04,-121.83 463.46,-113.59 463.46,-113.59 C463.46,-113.59 459.88,-105.35 459.88,-105.35 C459.88,-105.35 456.54,-97.01 456.54,-97.01 C456.54,-97.01 453.37,-88.6 453.37,-88.6 C453.37,-88.6 450.21,-80.19 450.21,-80.19 C450.21,-80.19 447.68,-71.57 447.68,-71.57 C447.68,-71.57 445.36,-62.89 445.36,-62.89 C445.36,-62.89 443.04,-54.21 443.04,-54.21 C443.04,-54.21 441.54,-45.35 441.54,-45.35 C441.54,-45.35 440.09,-36.48 440.09,-36.48 C440.09,-36.48 438.78,-27.6 438.78,-27.6 C438.78,-27.6 438.19,-18.63 438.19,-18.63 C438.19,-18.63 437.61,-9.66 437.61,-9.66 C437.61,-9.66 437.36,-0.69 437.36,-0.69 C437.36,-0.69 437.65,8.29 437.65,8.29 C437.65,8.29 437.95,17.27 437.95,17.27 C437.95,17.27 438.77,26.21 438.77,26.21 C438.77,26.21 439.94,35.12 439.94,35.12 C439.94,35.12 441.11,44.03 441.11,44.03 C441.11,44.03 442.99,52.81 442.99,52.81 C442.99,52.81 445.02,61.57 445.02,61.57 C445.02,61.57 447.07,70.31 447.07,70.31 C447.07,70.31 449.82,78.87 449.82,78.87 C449.82,78.87 452.65,87.4 452.65,87.4 C452.65,87.4 455.96,95.75 455.96,95.75 C455.96,95.75 459.33,104.08 459.33,104.08 C459.33,104.08 462.91,112.32 462.91,112.32 C462.91,112.32 466.49,120.57 466.49,120.57 C466.49,120.57 470.07,128.81 470.07,128.81 C470.07,128.81 473.65,137.05 473.65,137.05 C473.65,137.05 476.82,145.46 476.82,145.46 C476.82,145.46 479.99,153.87 479.99,153.87 C479.99,153.87 483.17,162.28 483.17,162.28 C483.17,162.28 485.52,170.94 485.52,170.94 C485.52,170.94 487.84,179.63 487.84,179.63 C487.84,179.63 490.14,188.31 490.14,188.31 C490.14,188.31 491.57,197.18 491.57,197.18 C491.57,197.18 493,206.06 493,206.06 C493,206.06 494.27,214.95 494.27,214.95 C494.27,214.95 494.8,223.92 494.8,223.92 C494.8,223.92 495.33,232.89 495.33,232.89 C495.33,232.89 495.5,241.86 495.5,241.86 C495.5,241.86 495.12,250.84 495.12,250.84 C495.12,250.84 494.75,259.82 494.75,259.82 C494.75,259.82 493.78,268.74 493.78,268.74 C493.78,268.74 492.52,277.64 492.52,277.64 C492.52,277.64 491.27,286.54 491.27,286.54 C491.27,286.54 489.16,295.27 489.16,295.27 C489.16,295.27 487.05,304.01 487.05,304.01 C487.05,304.01 484.66,312.66 484.66,312.66 C484.66,312.66 481.73,321.16 481.73,321.16 C481.73,321.16 478.79,329.65 478.79,329.65 C478.79,329.65 475.32,337.93 475.32,337.93 C475.32,337.93 471.6,346.11 471.6,346.11 C471.6,346.11 467.84,354.27 467.84,354.27 C467.84,354.27 463.39,362.08 463.39,362.08 C463.39,362.08 458.94,369.89 458.94,369.89 C458.94,369.89 454.19,377.5 454.19,377.5 C454.19,377.5 449.06,384.88 449.06,384.88 C449.06,384.88 443.93,392.26 443.93,392.26 C443.93,392.26 438.26,399.23 438.26,399.23 C438.26,399.23 432.5,406.12 432.5,406.12 C432.5,406.12 426.6,412.89 426.6,412.89 C426.6,412.89 420.24,419.24 420.24,419.24 C420.24,419.24 413.89,425.6 413.89,425.6 C413.89,425.6 407.2,431.59 407.2,431.59 C407.2,431.59 400.31,437.36 400.31,437.36 C400.31,437.36 393.42,443.12 393.42,443.12 C393.42,443.12 386.04,448.25 386.04,448.25 C386.04,448.25 378.66,453.38 378.66,453.38 C378.66,453.38 371.11,458.24 371.11,458.24 C371.11,458.24 363.31,462.69 363.31,462.69 C363.31,462.69 355.5,467.14 355.5,467.14 C355.5,467.14 347.4,471.02 347.4,471.02 C347.4,471.02 339.22,474.73 339.22,474.73 C339.22,474.73 330.99,478.33 330.99,478.33 C330.99,478.33 322.49,481.27 322.49,481.27 C322.49,481.27 314,484.2 314,484.2 C314,484.2 305.38,486.71 305.38,486.71 C305.38,486.71 296.65,488.83 296.65,488.83 C296.65,488.83 287.91,490.95 287.91,490.95 C287.91,490.95 279.04,492.33 279.04,492.33 C279.04,492.33 270.14,493.59 270.14,493.59 C270.14,493.59 261.23,494.69 261.23,494.69 C261.23,494.69 252.25,495.07 252.25,495.07 C252.25,495.07 243.28,495.44 243.28,495.44 C243.28,495.44 234.3,495.41 234.3,495.41 C234.3,495.41 225.33,494.88 225.33,494.88 C225.33,494.88 216.36,494.35 216.36,494.35 C216.36,494.35 207.45,493.23 207.45,493.23 C207.45,493.23 198.58,491.8 198.58,491.8 C198.58,491.8 189.71,490.37 189.71,490.37 C189.71,490.37 180.99,488.21 180.99,488.21 C180.99,488.21 172.31,485.89 172.31,485.89 C172.31,485.89 163.63,483.57 163.63,483.57 C163.63,483.57 155.19,480.5 155.19,480.5 C155.19,480.5 146.78,477.32 146.78,477.32 C146.78,477.32 138.37,474.15 138.37,474.15 C138.37,474.15 130.11,470.63 130.11,470.63 C130.11,470.63 121.86,467.06 121.86,467.06 C121.86,467.06 113.62,463.48 113.62,463.48 C113.62,463.48 105.38,459.9 105.38,459.9 C105.38,459.9 97.04,456.56 97.04,456.56 C97.04,456.56 88.63,453.39 88.63,453.39 C88.63,453.39 80.22,450.22 80.22,450.22 C80.22,450.22 71.6,447.7 71.6,447.7 C71.6,447.7 62.92,445.37 62.92,445.37 C62.92,445.37 54.24,443.05 54.24,443.05 C54.24,443.05 45.38,441.55 45.38,441.55 C45.38,441.55 36.52,440.1 36.52,440.1 C36.52,440.1 27.63,438.78 27.63,438.78 C27.63,438.78 18.66,438.2 18.66,438.2 C18.66,438.2 9.7,437.61 9.7,437.61 C9.7,437.61 0.72,437.36 0.72,437.36 C0.72,437.36 -8.26,437.65 -8.26,437.65 C-8.26,437.65 -17.24,437.95 -17.24,437.95 C-17.24,437.95 -26.18,438.77 -26.18,438.77 C-26.18,438.77 -35.09,439.94 -35.09,439.94 C-35.09,439.94 -44,441.1 -44,441.1 C-44,441.1 -52.78,442.98 -52.78,442.98 C-52.78,442.98 -61.53,445.02 -61.53,445.02 C-61.53,445.02 -70.28,447.07 -70.28,447.07 C-70.28,447.07 -78.84,449.81 -78.84,449.81 C-78.84,449.81 -87.37,452.64 -87.37,452.64 C-87.37,452.64 -95.72,455.95 -95.72,455.95 C-95.72,455.95 -104.05,459.32 -104.05,459.32 C-104.05,459.32 -112.29,462.9 -112.29,462.9 C-112.29,462.9 -120.53,466.48 -120.53,466.48 C-120.53,466.48 -128.78,470.06 -128.78,470.06 C-128.78,470.06 -137.02,473.63 -137.02,473.63 C-137.02,473.63 -145.43,476.81 -145.43,476.81 C-145.43,476.81 -153.84,479.98 -153.84,479.98 C-153.84,479.98 -162.24,483.15 -162.24,483.15 C-162.24,483.15 -170.91,485.52 -170.91,485.52 C-170.91,485.52 -179.59,487.83 -179.59,487.83 C-179.59,487.83 -188.28,490.13 -188.28,490.13 C-188.28,490.13 -197.15,491.56 -197.15,491.56 C-197.15,491.56 -206.02,492.99 -206.02,492.99 C-206.02,492.99 -214.91,494.27 -214.91,494.27 C-214.91,494.27 -223.88,494.8 -223.88,494.8 C-223.88,494.8 -232.85,495.33 -232.85,495.33 C-232.85,495.33 -241.83,495.5 -241.83,495.5 C-241.83,495.5 -250.81,495.13 -250.81,495.13 C-250.81,495.13 -259.79,494.75 -259.79,494.75 C-259.79,494.75 -268.71,493.79 -268.71,493.79 C-268.71,493.79 -277.61,492.53 -277.61,492.53 C-277.61,492.53 -286.51,491.27 -286.51,491.27 C-286.51,491.27 -295.24,489.17 -295.24,489.17 C-295.24,489.17 -303.98,487.06 -303.98,487.06 C-303.98,487.06 -312.63,484.67 -312.63,484.67 C-312.63,484.67 -321.12,481.74 -321.12,481.74 C-321.12,481.74 -329.62,478.8 -329.62,478.8 C-329.62,478.8 -337.9,475.33 -337.9,475.33 C-337.9,475.33 -346.08,471.62 -346.08,471.62 C-346.08,471.62 -354.24,467.85 -354.24,467.85 C-354.24,467.85 -362.05,463.41 -362.05,463.41 C-362.05,463.41 -369.86,458.96 -369.86,458.96 C-369.86,458.96 -377.47,454.21 -377.47,454.21 C-377.47,454.21 -384.85,449.08 -384.85,449.08 C-384.85,449.08 -392.23,443.95 -392.23,443.95 C-392.23,443.95 -399.2,438.29 -399.2,438.29 C-399.2,438.29 -406.09,432.52 -406.09,432.52 C-406.09,432.52 -412.86,426.62 -412.86,426.62 C-412.86,426.62 -419.22,420.27 -419.22,420.27 C-419.22,420.27 -425.57,413.91 -425.57,413.91 C-425.57,413.91 -431.57,407.23 -431.57,407.23 C-431.57,407.23 -437.33,400.34 -437.33,400.34 C-437.33,400.34 -443.1,393.44 -443.1,393.44 C-443.1,393.44 -448.23,386.07 -448.23,386.07 C-448.23,386.07 -453.36,378.69 -453.36,378.69 C-453.36,378.69 -458.23,371.15 -458.23,371.15 C-458.23,371.15 -462.67,363.33 -462.67,363.33 C-462.67,363.33 -467.12,355.53 -467.12,355.53 C-467.12,355.53 -471,347.43 -471,347.43 C-471,347.43 -474.72,339.25 -474.72,339.25 C-474.72,339.25 -478.32,331.02 -478.32,331.02 C-478.32,331.02 -481.25,322.52 -481.25,322.52 C-481.25,322.52 -484.19,314.03 -484.19,314.03 C-484.19,314.03 -486.71,305.42 -486.71,305.42 C-486.71,305.42 -488.82,296.68 -488.82,296.68 C-488.82,296.68 -490.94,287.95 -490.94,287.95 C-490.94,287.95 -492.32,279.07 -492.32,279.07 C-492.32,279.07 -493.58,270.18 -493.58,270.18 C-493.58,270.18 -494.69,261.27 -494.69,261.27 C-494.69,261.27 -495.07,252.29 -495.07,252.29 C-495.07,252.29 -495.44,243.31 -495.44,243.31 C-495.44,243.31 -495.42,234.33 -495.42,234.33 C-495.42,234.33 -494.89,225.36 -494.89,225.36 C-494.89,225.36 -494.36,216.39 -494.36,216.39 C-494.36,216.39 -493.23,207.49 -493.23,207.49 C-493.23,207.49 -491.8,198.61 -491.8,198.61 C-491.8,198.61 -490.37,189.74 -490.37,189.74 C-490.37,189.74 -488.22,181.02 -488.22,181.02 C-488.22,181.02 -485.9,172.34 -485.9,172.34 C-485.9,172.34 -483.58,163.66 -483.58,163.66 C-483.58,163.66 -480.51,155.22 -480.51,155.22 C-480.51,155.22 -477.34,146.81 -477.34,146.81 C-477.34,146.81 -474.17,138.41 -474.17,138.41 C-474.17,138.41 -470.65,130.14 -470.65,130.14 C-470.65,130.14 -467.07,121.9 -467.07,121.9 C-467.07,121.9 -463.49,113.65 -463.49,113.65 C-463.49,113.65 -459.91,105.41 -459.91,105.41 C-459.91,105.41 -456.57,97.07 -456.57,97.07 C-456.57,97.07 -453.4,88.66 -453.4,88.66 C-453.4,88.66 -450.23,80.25 -450.23,80.25 C-450.23,80.25 -447.7,71.64 -447.7,71.64 C-447.7,71.64 -445.38,62.96 -445.38,62.96 C-445.38,62.96 -443.06,54.28 -443.06,54.28 C-443.06,54.28 -441.56,45.42 -441.56,45.42 C-441.56,45.42 -440.1,36.55 -440.1,36.55 C-440.1,36.55 -438.78,27.67 -438.78,27.67 C-438.78,27.67 -438.2,18.7 -438.2,18.7 C-438.2,18.7 -437.62,9.73 -437.62,9.73 C-437.62,9.73 -437.36,0.76 -437.36,0.76 C-437.36,0.76 -437.66,-8.22 -437.66,-8.22 C-437.66,-8.22 -437.95,-17.2 -437.95,-17.2 C-437.95,-17.2 -438.77,-26.14 -438.77,-26.14 C-438.77,-26.14 -439.93,-35.05 -439.93,-35.05 C-439.93,-35.05 -441.1,-43.96 -441.1,-43.96 C-441.1,-43.96 -442.98,-52.75 -442.98,-52.75 C-442.98,-52.75 -445.01,-61.5 -445.01,-61.5 C-445.01,-61.5 -447.06,-70.25 -447.06,-70.25 C-447.06,-70.25 -449.8,-78.81 -449.8,-78.81 C-449.8,-78.81 -452.63,-87.33 -452.63,-87.33 C-452.63,-87.33 -455.94,-95.69 -455.94,-95.69 C-455.94,-95.69 -459.31,-104.02 -459.31,-104.02 C-459.31,-104.02 -462.89,-112.26 -462.89,-112.26 C-462.89,-112.26 -466.47,-120.5 -466.47,-120.5 C-466.47,-120.5 -470.05,-128.74 -470.05,-128.74 C-470.05,-128.74 -473.68,-137.12 -473.68,-137.12 C-473.68,-137.12 -476.85,-145.53 -476.85,-145.53 C-476.85,-145.53 -480.03,-153.94 -480.03,-153.94 C-480.03,-153.94 -483.2,-162.34 -483.2,-162.34 C-483.2,-162.34 -485.55,-171.02 -485.55,-171.02 C-485.55,-171.02 -487.86,-179.7 -487.86,-179.7 C-487.86,-179.7 -490.15,-188.39 -490.15,-188.39 C-490.15,-188.39 -491.58,-197.26 -491.58,-197.26 C-491.58,-197.26 -493.01,-206.13 -493.01,-206.13 C-493.01,-206.13 -494.28,-215.02 -494.28,-215.02 C-494.28,-215.02 -494.81,-223.99 -494.81,-223.99 C-494.81,-223.99 -495.33,-232.96 -495.33,-232.96 C-495.33,-232.96 -495.5,-241.94 -495.5,-241.94 C-495.5,-241.94 -495.12,-250.92 -495.12,-250.92 C-495.12,-250.92 -494.75,-259.9 -494.75,-259.9 C-494.75,-259.9 -493.78,-268.82 -493.78,-268.82 C-493.78,-268.82 -492.52,-277.72 -492.52,-277.72 C-492.52,-277.72 -491.26,-286.61 -491.26,-286.61 C-491.26,-286.61 -489.15,-295.35 -489.15,-295.35 C-489.15,-295.35 -487.03,-304.08 -487.03,-304.08 C-487.03,-304.08 -484.64,-312.73 -484.64,-312.73 C-484.64,-312.73 -481.7,-321.23 -481.7,-321.23 C-481.7,-321.23 -478.77,-329.72 -478.77,-329.72 C-478.77,-329.72 -475.29,-338 -475.29,-338 C-475.29,-338 -471.57,-346.18 -471.57,-346.18 C-471.57,-346.18 -467.8,-354.33 -467.8,-354.33 C-467.8,-354.33 -463.36,-362.14 -463.36,-362.14 C-463.36,-362.14 -458.91,-369.95 -458.91,-369.95 C-458.91,-369.95 -454.15,-377.56 -454.15,-377.56 C-454.15,-377.56 -449.02,-384.94 -449.02,-384.94 C-449.02,-384.94 -443.88,-392.32 -443.88,-392.32 C-443.88,-392.32 -438.22,-399.28 -438.22,-399.28 C-438.22,-399.28 -432.45,-406.18 -432.45,-406.18 C-432.45,-406.18 -426.55,-412.94 -426.55,-412.94 C-426.55,-412.94 -420.19,-419.3 -420.19,-419.3 C-420.19,-419.3 -413.84,-425.65 -413.84,-425.65 C-413.84,-425.65 -407.15,-431.64 -407.15,-431.64 C-407.15,-431.64 -400.26,-437.41 -400.26,-437.41 C-400.26,-437.41 -393.36,-443.16 -393.36,-443.16 C-393.36,-443.16 -385.98,-448.29 -385.98,-448.29 C-385.98,-448.29 -378.6,-453.43 -378.6,-453.43 C-378.6,-453.43 -371.05,-458.28 -371.05,-458.28 C-371.05,-458.28 -363.24,-462.73 -363.24,-462.73 C-363.24,-462.73 -355.43,-467.18 -355.43,-467.18 C-355.43,-467.18 -347.33,-471.05 -347.33,-471.05 C-347.33,-471.05 -339.15,-474.76 -339.15,-474.76 C-339.15,-474.76 -330.92,-478.35 -330.92,-478.35 C-330.92,-478.35 -322.42,-481.29 -322.42,-481.29 C-322.42,-481.29 -313.93,-484.23 -313.93,-484.23 C-313.93,-484.23 -305.31,-486.73 -305.31,-486.73 C-305.31,-486.73 -296.58,-488.85 -296.58,-488.85 C-296.58,-488.85 -287.85,-490.97 -287.85,-490.97 C-287.85,-490.97 -278.97,-492.34 -278.97,-492.34 C-278.97,-492.34 -270.07,-493.6 -270.07,-493.6 C-270.07,-493.6 -261.16,-494.7 -261.16,-494.7 C-261.16,-494.7 -252.18,-495.07 -252.18,-495.07 C-252.18,-495.07 -243.2,-495.44 -243.2,-495.44 C-243.2,-495.44 -234.23,-495.41 -234.23,-495.41 C-234.23,-495.41 -225.26,-494.88 -225.26,-494.88 C-225.26,-494.88 -216.29,-494.35 -216.29,-494.35 C-216.29,-494.35 -207.38,-493.22 -207.38,-493.22 C-207.38,-493.22 -198.51,-491.79 -198.51,-491.79 C-198.51,-491.79 -189.64,-490.36 -189.64,-490.36 C-189.64,-490.36 -180.92,-488.19 -180.92,-488.19 C-180.92,-488.19 -172.24,-485.87 -172.24,-485.87 C-172.24,-485.87 -163.56,-483.56 -163.56,-483.56 C-163.56,-483.56 -155.12,-480.47 -155.12,-480.47 C-155.12,-480.47 -146.72,-477.3 -146.72,-477.3 C-146.72,-477.3 -138.31,-474.13 -138.31,-474.13 C-138.31,-474.13 -130.04,-470.61 -130.04,-470.61 C-130.04,-470.61 -121.8,-467.03 -121.8,-467.03 C-121.8,-467.03 -113.55,-463.45 -113.55,-463.45 C-113.55,-463.45 -105.31,-459.87 -105.31,-459.87 C-105.31,-459.87 -96.97,-456.53 -96.97,-456.53 C-96.97,-456.53 -88.56,-453.37 -88.56,-453.37 C-88.56,-453.37 -80.15,-450.2 -80.15,-450.2 C-80.15,-450.2 -71.53,-447.68 -71.53,-447.68 C-71.53,-447.68 -62.85,-445.36 -62.85,-445.36 C-62.85,-445.36 -54.17,-443.04 -54.17,-443.04 C-54.17,-443.04 -45.31,-441.54 -45.31,-441.54 C-45.31,-441.54 -36.44,-440.09 -36.44,-440.09 C-36.44,-440.09 -27.56,-438.78 -27.56,-438.78 C-27.56,-438.78 -18.59,-438.19 -18.59,-438.19 C-18.59,-438.19 -9.62,-437.61 -9.62,-437.61 C-9.62,-437.61 -0.65,-437.37 -0.65,-437.37c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button.xml b/packages/SystemUI/res/drawable/ic_media_pause_button.xml new file mode 100644 index 000000000000..6ae89f91c5ee --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueTo="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.449,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="56" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="15.485" + android:valueTo="12.321" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="278" + android:propertyName="translateX" + android:startOffset="56" + android:valueFrom="12.321" + android:valueTo="7.576" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.05,0.7 0.1,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="15.485" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml new file mode 100644 index 000000000000..571f69d51ac4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="88dp" + android:height="56dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972" + android:translateX="43.528999999999996" + android:translateY="27.898"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#3d90ff" + android:fillType="nonZero" + android:pathData=" M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="133" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="367" + android:propertyName="pathData" + android:startOffset="133" + android:valueFrom="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.473,0 0.065,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="167" + android:propertyName="scaleX" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="scaleY" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleX" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="333" + android:propertyName="scaleY" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button.xml b/packages/SystemUI/res/drawable/ic_media_play_button.xml new file mode 100644 index 000000000000..f64690268cfe --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-1.24 -5.06,-1.24 C-5.06,-1.24 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " + android:valueTo="M-5.06 -18 C-5.06,-18 -5.06,-0.75 -5.06,-0.75 C-5.06,-0.75 -5.06,17.7 -5.06,17.7 C-5.06,19.36 -6.41,20.7 -8.06,20.7 C-8.06,20.7 -16,20.7 -16,20.7 C-17.66,20.7 -19,19.36 -19,17.7 C-19,17.7 -19,-18 -19,-18 C-19,-19.66 -17.66,-21 -16,-21 C-16,-21 -8.06,-21 -8.06,-21 C-6.41,-21 -5.06,-19.66 -5.06,-18c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.433,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="333" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="7.576" + android:valueTo="15.485" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.583,0 0.089,0.874 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="517" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:pivotX="-12.031" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="19.524" + android:translateY="12.084"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + <group + android:name="_R_G_L_0_G_T_1" + android:scaleX="0.33299999999999996" + android:scaleY="0.33299999999999996" + android:translateX="7.576" + android:translateY="12.084"> + <group + android:name="_R_G_L_0_G" + android:translateX="12.031"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-4.69 -16.69 C-4.69,-16.69 20.25,-1.25 20.31,0.25 C20.38,1.75 -4.88,16.89 -4.88,16.89 C-6.94,18.25 -8.56,19.4 -9.75,19.58 C-10.94,19.75 -12.19,18.94 -12.12,16.14 C-12.09,14.76 -12.12,15.92 -12.12,14.26 C-12.12,14.26 -11.94,-16.44 -11.94,-16.44 C-11.94,-18.09 -12.09,-19.69 -10.44,-19.69 C-10.44,-19.69 -9.5,-19.56 -9.5,-19.56 C-8.62,-19.12 -6.19,-17.44 -4.69,-16.69c " /> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_media_play_button_container.xml b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml new file mode 100644 index 000000000000..aa4e09fa4033 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play_button_container.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 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. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:height="56dp" + android:width="88dp" + android:viewportHeight="56" + android:viewportWidth="88"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_0_G" + android:translateX="43.528999999999996" + android:translateY="27.898" + android:pivotX="0.493" + android:pivotY="0.124" + android:scaleX="1.05905" + android:scaleY="1.0972"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillColor="#3d90ff" + android:fillAlpha="1" + android:fillType="nonZero" + android:pathData=" M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c "/> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="pathData" + android:duration="167" + android:startOffset="0" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="pathData" + android:duration="333" + android:startOffset="167" + android:valueFrom="M34.47 0.63 C34.47,0.63 34.42,0.64 34.42,0.64 C33.93,12.88 24.69,21.84 13.06,21.97 C13.06,21.97 -12.54,21.97 -12.54,21.97 C-23.11,21.84 -33.38,13.11 -33.52,-0.27 C-33.52,-0.27 -33.52,-0.05 -33.52,-0.05 C-33.5,-13.21 -21.73,-21.76 -12.9,-21.76 C-12.9,-21.76 14.59,-21.76 14.59,-21.76 C24.81,-21.88 34.49,-10.58 34.47,0.63c " + android:valueTo="M34.49 -5.75 C34.49,-5.75 34.49,6 34.49,6 C34.49,14.84 27.32,22 18.49,22 C18.49,22 -17.5,22 -17.5,22 C-26.34,22 -33.5,14.84 -33.5,6 C-33.5,6 -33.5,-5.75 -33.5,-5.75 C-33.5,-14.59 -26.34,-21.75 -17.5,-21.75 C-17.5,-21.75 18.49,-21.75 18.49,-21.75 C27.32,-21.75 34.49,-14.59 34.49,-5.75c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.493,0 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="scaleX" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.05905" + android:valueTo="1.17758" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="167" + android:startOffset="0" + android:valueFrom="1.0972" + android:valueTo="1.22" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.226,0 0.667,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleX" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.17758" + android:valueTo="1.05905" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:propertyName="scaleY" + android:duration="333" + android:startOffset="167" + android:valueFrom="1.22" + android:valueTo="1.0972" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.213,0 0.248,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:propertyName="translateX" + android:duration="517" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 58f2d3ccc6a8..67f620f6fc54 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -19,6 +19,7 @@ android:id="@+id/volume_dialog_root" android:layout_width="match_parent" android:layout_height="match_parent" + android:alpha="0" android:clipChildren="false" app:layoutDescription="@xml/volume_dialog_scene"> diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml index e65d0b938b65..6748cfa05c35 100644 --- a/packages/SystemUI/res/layout/volume_ringer_button.xml +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -20,10 +20,9 @@ <ImageButton android:id="@+id/volume_drawer_button" - android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" - android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_width="match_parent" + android:layout_height="match_parent" android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" - android:layout_marginBottom="@dimen/volume_dialog_components_spacing" android:contentDescription="@string/volume_ringer_mode" android:gravity="center" android:tint="@androidprv:color/materialColorOnSurface" diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml index afd4fa7a5edd..7f7350472fa5 100644 --- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml +++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml @@ -132,7 +132,8 @@ android:layout_height="wrap_content" android:track="@drawable/settingslib_track_selector" android:thumb="@drawable/settingslib_thumb_selector" - android:theme="@style/MainSwitch.Settingslib"/> + android:theme="@style/MainSwitch.Settingslib" + android:minHeight="48dp" /> </LinearLayout> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 1f7889214bd5..2ffa3d19e161 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1278,6 +1278,7 @@ <dimen name="qs_center_guideline_padding">10dp</dimen> <dimen name="qs_media_action_spacing">4dp</dimen> <dimen name="qs_media_action_margin">12dp</dimen> + <dimen name="qs_media_action_play_pause_width">72dp</dimen> <dimen name="qs_seamless_height">24dp</dimen> <dimen name="qs_seamless_icon_size">12dp</dimen> <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index a01ff3d5258f..a8ee60973586 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1963,7 +1963,7 @@ <!-- Text displayed indicating that the user is connected to a satellite signal. --> <string name="satellite_connected_carrier_text">Satellite SOS</string> <!-- Text displayed indicating that the user might be able to use satellite SOS. --> - <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS</string> + <string name="satellite_emergency_only_carrier_text">Emergency calls or SOS only</string> <!-- Content description skeleton. Input strings should be carrier name and signal bar description [CHAR LIMIT=NONE]--> <string name="accessibility_phone_string_format"><xliff:g id="carrier_name" example="Carrier Name">%1$s</xliff:g>, <xliff:g id="signal_strength_description" example="two bars">%2$s</xliff:g>.</string> @@ -2309,6 +2309,9 @@ <string name="group_system_lock_screen">Lock screen</string> <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] --> <string name="group_system_quick_memo">Take a note</string> + <!-- TODO(b/383734125): make it translatable once string is finalized by UXW.--> + <!-- User visible title for the keyboard shortcut that toggles Voice Access. [CHAR LIMIT=70] --> + <string name="group_system_toggle_voice_access" translatable="false">Toggle Voice Access</string> <!-- User visible title for the multitasking keyboard shortcuts list. [CHAR LIMIT=70] --> <string name="keyboard_shortcut_group_system_multitasking">Multitasking</string> @@ -3919,6 +3922,16 @@ The helper is a component that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] --> <string name="shortcut_helper_plus_symbol">+</string> + <!-- Accessibility label for the plus icon on a shortcut in shortcut helper that allows the user + to add a new custom shortcut. + The helper is a component that shows the user which keyboard shortcuts they can use. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_add_shortcut_button_label">Add shortcut</string> + <!-- Accessibility label for the bin(trash) icon on a shortcut in shortcut helper that allows the + user to delete an existing custom shortcut. + The helper is a component that shows the user which keyboard shortcuts they can use. + [CHAR LIMIT=NONE] --> + <string name="shortcut_helper_delete_shortcut_button_label">Delete shortcut</string> <!-- Keyboard touchpad tutorial scheduler--> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index acfa08643b63..c7ae02b61bff 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -142,33 +142,28 @@ public class KeyguardDisplayManager { private boolean isKeyguardShowable(Display display) { if (display == null) { - if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display"); + Log.i(TAG, "Cannot show Keyguard on null display"); return false; } if (ShadeWindowGoesAround.isEnabled()) { int shadeDisplayId = mShadePositionRepositoryProvider.get().getDisplayId().getValue(); if (display.getDisplayId() == shadeDisplayId) { - if (DEBUG) { - Log.i(TAG, - "Do not show KeyguardPresentation on the shade window display"); - } + Log.i(TAG, "Do not show KeyguardPresentation on the shade window display"); return false; } } else { if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) { - if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display"); + Log.i(TAG, "Do not show KeyguardPresentation on the default display"); return false; } } display.getDisplayInfo(mTmpDisplayInfo); if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) { - if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display"); + Log.i(TAG, "Do not show KeyguardPresentation on a private display"); return false; } if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) { - if (DEBUG) { - Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display"); - } + Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display"); return false; } @@ -176,14 +171,11 @@ public class KeyguardDisplayManager { mDeviceStateHelper.isConcurrentDisplayActive(display) || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display); if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) { - if (DEBUG) { - // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the - // Keyguard state becomes "occluded". In this case, we should not show the - // KeyguardPresentation, since the activity is presenting content onto the - // non-default display. - Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" - + " display is active"); - } + // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the Keyguard + // state becomes "occluded". In this case, we should not show the KeyguardPresentation, + // since the activity is presenting content onto the non-default display. + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" + + " display is active"); return false; } @@ -197,7 +189,7 @@ public class KeyguardDisplayManager { */ private boolean showPresentation(Display display) { if (!isKeyguardShowable(display)) return false; - if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display); + Log.i(TAG, "Keyguard enabled on display: " + display); final int displayId = display.getDisplayId(); Presentation presentation = mPresentations.get(displayId); if (presentation == null) { @@ -239,7 +231,7 @@ public class KeyguardDisplayManager { public void show() { if (!mShowing) { - if (DEBUG) Log.v(TAG, "show"); + Log.v(TAG, "show"); if (mMediaRouter != null) { mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); @@ -253,7 +245,7 @@ public class KeyguardDisplayManager { public void hide() { if (mShowing) { - if (DEBUG) Log.v(TAG, "hide"); + Log.v(TAG, "hide"); if (mMediaRouter != null) { mMediaRouter.removeCallback(mMediaRouterCallback); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 622b67f25da3..e22736b69213 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -297,10 +297,13 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { DisappearAnimationUtils disappearAnimationUtils = needsSlowUnlockTransition ? mDisappearAnimationUtilsLocked : mDisappearAnimationUtils; + android.util.Log.i("KeyguardPINView", "startDisappearAnimation: " + finishRunnable); disappearAnimationUtils.createAnimation( this, 0, 200, mDisappearYTranslation, false, mDisappearAnimationUtils.getInterpolator(), () -> { if (finishRunnable != null) { + android.util.Log.i("KeyguardPINView", + "startDisappearAnimation, invoking run()"); finishRunnable.run(); } }, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index a703b027b691..7d291c311ca3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -2591,6 +2591,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * @return true if optical udfps HW is supported on this device. Can return true even if the + * user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return mAuthController.isOpticalUdfpsSupported(); + } + + /** * @return true if there's at least one sfps enrollment for the current user. */ public boolean isSfpsEnrolled() { diff --git a/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt b/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt new file mode 100644 index 000000000000..5e29ba91ce42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/KairosActivatable.kt @@ -0,0 +1,212 @@ +/* + * 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 + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.Events +import com.android.systemui.kairos.EventsLoop +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.Incremental +import com.android.systemui.kairos.IncrementalLoop +import com.android.systemui.kairos.KairosNetwork +import com.android.systemui.kairos.State +import com.android.systemui.kairos.StateLoop +import com.android.systemui.kairos.launchKairosNetwork +import com.android.systemui.kairos.launchScope +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import dagger.multibindings.Multibinds +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * A Kairos-powered class that needs late-initialization within a Kairos [BuildScope]. + * + * If your class is a [SysUISingleton], you can leverage Dagger to automatically initialize your + * instance after SystemUI has initialized: + * ```kotlin + * class MyClass : KairosActivatable { ... } + * + * @dagger.Module + * interface MyModule { + * @Binds + * @IntoSet + * fun bindKairosActivatable(impl: MyClass): KairosActivatable + * } + * ``` + * + * Alternatively, you can utilize Dagger's [dagger.assisted.AssistedInject]: + * ```kotlin + * class MyClass @AssistedInject constructor(...) : KairosActivatable { + * @AssistedFactory + * interface Factory { + * fun create(...): MyClass + * } + * } + * + * // When you need an instance: + * + * class OtherClass @Inject constructor( + * private val myClassFactory: MyClass.Factory, + * ) { + * fun BuildScope.foo() { + * val myClass = activated { myClassFactory.create() } + * ... + * } + * } + * ``` + * + * @see activated + */ +@ExperimentalKairosApi +fun interface KairosActivatable { + /** Initializes any Kairos fields that require a [BuildScope] in order to be constructed. */ + fun BuildScope.activate() +} + +/** Constructs [KairosActivatable] instances. */ +@ExperimentalKairosApi +fun interface KairosActivatableFactory<T : KairosActivatable> { + fun BuildScope.create(): T +} + +/** Instantiates, [activates][KairosActivatable.activate], and returns a [KairosActivatable]. */ +@ExperimentalKairosApi +fun <T : KairosActivatable> BuildScope.activated(factory: KairosActivatableFactory<T>): T = + factory.run { create() }.apply { activate() } + +/** + * Utilities for defining [State] and [Events] from a constructor without a provided [BuildScope]. + * These instances are not active until the builder is [activated][activate]; while you can + * immediately use them with other Kairos APIs, the Kairos transaction will be suspended until + * initialization is complete. + * + * ```kotlin + * class MyRepository(private val dataSource: DataSource) : KairosBuilder by kairosBuilder() { + * val dataSourceEvent = buildEvents<SomeData> { + * // inside this lambda, we have access to a BuildScope, which can be used to create + * // new inputs to the Kairos network + * dataSource.someDataFlow.toEvents() + * } + * } + * ``` + */ +@ExperimentalKairosApi +interface KairosBuilder : KairosActivatable { + /** + * Returns a forward-reference to a [State] that will be instantiated when this builder is + * [activated][activate]. + */ + fun <R> buildState(block: BuildScope.() -> State<R>): State<R> + + /** + * Returns a forward-reference to an [Events] that will be instantiated when this builder is + * [activated][activate]. + */ + fun <R> buildEvents(block: BuildScope.() -> Events<R>): Events<R> + + fun <K, V> buildIncremental(block: BuildScope.() -> Incremental<K, V>): Incremental<K, V> + + /** Defers [block] until this builder is [activated][activate]. */ + fun onActivated(block: BuildScope.() -> Unit) +} + +/** Returns an [KairosBuilder] that can only be [activated][KairosActivatable.activate] once. */ +@ExperimentalKairosApi fun kairosBuilder(): KairosBuilder = KairosBuilderImpl() + +@OptIn(ExperimentalKairosApi::class) +private class KairosBuilderImpl @Inject constructor() : KairosBuilder { + + // TODO: atomic? + // TODO: are two lists really necessary? + private var _builds: MutableList<KairosActivatable>? = mutableListOf() + private var _startables: MutableList<KairosActivatable>? = mutableListOf() + + private val startables + get() = checkNotNull(_startables) { "Kairos network has already been initialized" } + + private val builds + get() = checkNotNull(_builds) { "Kairos network has already been initialized" } + + override fun <R> buildState(block: BuildScope.() -> State<R>): State<R> = + StateLoop<R>().apply { builds.add { loopback = block() } } + + override fun <R> buildEvents(block: BuildScope.() -> Events<R>): Events<R> = + EventsLoop<R>().apply { builds.add { loopback = block() } } + + override fun <K, V> buildIncremental( + block: BuildScope.() -> Incremental<K, V> + ): Incremental<K, V> = IncrementalLoop<K, V>().apply { builds.add { loopback = block() } } + + override fun onActivated(block: BuildScope.() -> Unit) { + startables.add { block() } + } + + override fun BuildScope.activate() { + builds.forEach { it.run { activate() } } + _builds = null + deferredBuildScopeAction { + startables.forEach { it.run { activate() } } + _startables = null + } + } +} + +/** Initializes [KairosActivatables][KairosActivatable] after SystemUI is initialized. */ +@SysUISingleton +@ExperimentalKairosApi +class KairosCoreStartable +@Inject +constructor( + @Application private val appScope: CoroutineScope, + private val kairosNetwork: KairosNetwork, + private val activatables: dagger.Lazy<Set<@JvmSuppressWildcards KairosActivatable>>, +) : CoreStartable { + override fun start() { + appScope.launch { + kairosNetwork.activateSpec { + for (activatable in activatables.get()) { + launchScope { activatable.run { activate() } } + } + } + } + } +} + +@Module +@ExperimentalKairosApi +interface KairosCoreStartableModule { + @Binds + @IntoMap + @ClassKey(KairosCoreStartable::class) + fun bindCoreStartable(impl: KairosCoreStartable): CoreStartable + + @Multibinds fun kairosActivatables(): Set<@JvmSuppressWildcards KairosActivatable> + + companion object { + @Provides + @SysUISingleton + fun provideKairosNetwork(@Application scope: CoroutineScope): KairosNetwork = + scope.launchKairosNetwork() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java index f530522fb707..5f79c8cada45 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java @@ -100,7 +100,8 @@ public abstract class SystemUIInitializer { .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper()) .setRecentTasks(mWMComponent.getRecentTasks()) .setBackAnimation(mWMComponent.getBackAnimation()) - .setDesktopMode(mWMComponent.getDesktopMode()); + .setDesktopMode(mWMComponent.getDesktopMode()) + .setAppZoomOut(mWMComponent.getAppZoomOut()); // Only initialize when not starting from tests since this currently initializes some // components that shouldn't be run in the test environment @@ -121,7 +122,8 @@ public abstract class SystemUIInitializer { .setStartingSurface(Optional.ofNullable(null)) .setRecentTasks(Optional.ofNullable(null)) .setBackAnimation(Optional.ofNullable(null)) - .setDesktopMode(Optional.ofNullable(null)); + .setDesktopMode(Optional.ofNullable(null)) + .setAppZoomOut(Optional.ofNullable(null)); } mSysUIComponent = builder.build(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java index 04afd8693e04..caf043a1b1be 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java @@ -22,6 +22,8 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.annotation.Nullable; import android.annotation.UiContext; import android.content.ComponentCallbacks; import android.content.Context; @@ -44,7 +46,8 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import androidx.annotation.NonNull; @@ -57,12 +60,16 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; import com.android.systemui.util.leak.RotationUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.concurrent.Executor; import java.util.function.Supplier; public class FullscreenMagnificationController implements ComponentCallbacks { - private static final String TAG = "FullscreenMagnificationController"; + private static final String TAG = "FullscreenMagController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; private final AccessibilityManager mAccessibilityManager; private final WindowManager mWindowManager; @@ -77,12 +84,14 @@ public class FullscreenMagnificationController implements ComponentCallbacks { private int mBorderStoke; private final int mDisplayId; private static final Region sEmptyRegion = new Region(); - private ValueAnimator mShowHideBorderAnimator; + @VisibleForTesting + @Nullable + ValueAnimator mShowHideBorderAnimator; private Handler mHandler; private Executor mExecutor; - private boolean mFullscreenMagnificationActivated = false; private final Configuration mConfiguration; - private final Runnable mShowBorderRunnable = this::showBorderWithNullCheck; + private final Runnable mHideBorderImmediatelyRunnable = this::hideBorderImmediately; + private final Runnable mShowBorderRunnable = this::showBorder; private int mRotation; private final IRotationWatcher mRotationWatcher = new IRotationWatcher.Stub() { @Override @@ -95,6 +104,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks { private final DisplayManager.DisplayListener mDisplayListener; private String mCurrentDisplayUniqueId; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DISABLED, + DISABLING, + ENABLING, + ENABLED + }) + @interface FullscreenMagnificationActivationState {} + private static final int DISABLED = 0; + private static final int DISABLING = 1; + private static final int ENABLING = 2; + private static final int ENABLED = 3; + @FullscreenMagnificationActivationState + private int mActivationState = DISABLED; + public FullscreenMagnificationController( @UiContext Context context, @Main Handler handler, @@ -106,7 +130,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { Supplier<SurfaceControlViewHost> scvhSupplier) { this(context, handler, executor, displayManager, accessibilityManager, windowManager, iWindowManager, scvhSupplier, - new SurfaceControl.Transaction(), null); + new SurfaceControl.Transaction()); } @VisibleForTesting @@ -119,8 +143,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { WindowManager windowManager, IWindowManager iWindowManager, Supplier<SurfaceControlViewHost> scvhSupplier, - SurfaceControl.Transaction transaction, - ValueAnimator valueAnimator) { + SurfaceControl.Transaction transaction) { mContext = context; mHandler = handler; mExecutor = executor; @@ -135,18 +158,6 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mConfiguration = new Configuration(context.getResources().getConfiguration()); mLongAnimationTimeMs = mContext.getResources().getInteger( com.android.internal.R.integer.config_longAnimTime); - mShowHideBorderAnimator = (valueAnimator == null) - ? createNullTargetObjectAnimator() : valueAnimator; - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - if (isReverse) { - // The animation was played in reverse, which means we are hiding the border. - // We would like to perform clean up after the border is fully hidden. - cleanUpBorder(); - } - } - }); mCurrentDisplayUniqueId = mContext.getDisplayNoVerify().getUniqueId(); mDisplayManager = displayManager; mDisplayListener = new DisplayManager.DisplayListener() { @@ -167,20 +178,51 @@ public class FullscreenMagnificationController implements ComponentCallbacks { // Same unique ID means the physical display doesn't change. Early return. return; } - mCurrentDisplayUniqueId = uniqueId; - applyCornerRadiusToBorder(); + mHandler.post(FullscreenMagnificationController.this::applyCornerRadiusToBorder); } }; } - private ValueAnimator createNullTargetObjectAnimator() { + @VisibleForTesting + @UiThread + ValueAnimator createShowTargetAnimator(@NonNull View target) { + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + final ValueAnimator valueAnimator = - ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); - Interpolator interpolator = new AccelerateDecelerateInterpolator(); + ObjectAnimator.ofFloat(target, View.ALPHA, 0f, 1f); + Interpolator interpolator = new AccelerateInterpolator(); valueAnimator.setInterpolator(interpolator); valueAnimator.setDuration(mLongAnimationTimeMs); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + mHandler.post(() -> setState(ENABLED)); + }}); + return valueAnimator; + } + + @VisibleForTesting + @UiThread + ValueAnimator createHideTargetAnimator(@NonNull View target) { + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + + final ValueAnimator valueAnimator = + ObjectAnimator.ofFloat(target, View.ALPHA, 1f, 0f); + Interpolator interpolator = new DecelerateInterpolator(); + + valueAnimator.setInterpolator(interpolator); + valueAnimator.setDuration(mLongAnimationTimeMs); + valueAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation) { + mHandler.post(() -> cleanUpBorder()); + }}); return valueAnimator; } @@ -190,14 +232,10 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread public void onFullscreenMagnificationActivationChanged(boolean activated) { - final boolean changed = (mFullscreenMagnificationActivated != activated); - if (changed) { - mFullscreenMagnificationActivated = activated; - if (activated) { - createFullscreenMagnificationBorder(); - } else { - removeFullscreenMagnificationBorder(); - } + if (activated) { + createFullscreenMagnificationBorder(); + } else { + removeFullscreenMagnificationBorder(); } } @@ -207,16 +245,21 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread private void removeFullscreenMagnificationBorder() { - if (mHandler.hasCallbacks(mShowBorderRunnable)) { - mHandler.removeCallbacks(mShowBorderRunnable); + int state = getState(); + if (state == DISABLING || state == DISABLED) { + // If there is an ongoing disable process or it is already disabled, return + return; } - mContext.unregisterComponentCallbacks(this); - - - mShowHideBorderAnimator.reverse(); + setState(DISABLING); + mShowHideBorderAnimator = createHideTargetAnimator(mFullscreenBorder); + mShowHideBorderAnimator.start(); } - private void cleanUpBorder() { + @VisibleForTesting + @UiThread + void cleanUpBorder() { + mContext.unregisterComponentCallbacks(this); + if (Flags.updateCornerRadiusOnDisplayChanged()) { mDisplayManager.unregisterDisplayListener(mDisplayListener); } @@ -227,6 +270,12 @@ public class FullscreenMagnificationController implements ComponentCallbacks { } if (mFullscreenBorder != null) { + if (mHandler.hasCallbacks(mHideBorderImmediatelyRunnable)) { + mHandler.removeCallbacks(mHideBorderImmediatelyRunnable); + } + if (mHandler.hasCallbacks(mShowBorderRunnable)) { + mHandler.removeCallbacks(mShowBorderRunnable); + } mFullscreenBorder = null; try { mIWindowManager.removeRotationWatcher(mRotationWatcher); @@ -234,6 +283,7 @@ public class FullscreenMagnificationController implements ComponentCallbacks { Log.w(TAG, "Failed to remove rotation watcher", e); } } + setState(DISABLED); } /** @@ -242,44 +292,47 @@ public class FullscreenMagnificationController implements ComponentCallbacks { */ @UiThread private void createFullscreenMagnificationBorder() { + int state = getState(); + if (state == ENABLING || state == ENABLED) { + // If there is an ongoing enable process or it is already enabled, return + return; + } + if (mShowHideBorderAnimator != null) { + mShowHideBorderAnimator.cancel(); + } + setState(ENABLING); + onConfigurationChanged(mContext.getResources().getConfiguration()); mContext.registerComponentCallbacks(this); if (mSurfaceControlViewHost == null) { - // Create the view only if it does not exist yet. If we are trying to enable fullscreen - // magnification before it was fully disabled, we use the previous view instead of - // creating a new one. + // Create the view only if it does not exist yet. If we are trying to enable + // fullscreen magnification before it was fully disabled, we use the previous view + // instead of creating a new one. mFullscreenBorder = LayoutInflater.from(mContext) .inflate(R.layout.fullscreen_magnification_border, null); - // Set the initial border view alpha manually so we won't show the border accidentally - // after we apply show() to the SurfaceControl and before the animation starts to run. + // Set the initial border view alpha manually so we won't show the border + // accidentally after we apply show() to the SurfaceControl and before the + // animation starts to run. mFullscreenBorder.setAlpha(0f); - mShowHideBorderAnimator.setTarget(mFullscreenBorder); mSurfaceControlViewHost = mScvhSupplier.get(); mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams()); - mBorderSurfaceControl = mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); + mBorderSurfaceControl = + mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(); try { mIWindowManager.watchRotation(mRotationWatcher, Display.DEFAULT_DISPLAY); } catch (Exception e) { Log.w(TAG, "Failed to register rotation watcher", e); } if (Flags.updateCornerRadiusOnDisplayChanged()) { - mHandler.post(this::applyCornerRadiusToBorder); + applyCornerRadiusToBorder(); } } mTransaction .addTransactionCommittedListener( mExecutor, - () -> { - if (mShowHideBorderAnimator.isRunning()) { - // Since the method is only called when there is an activation - // status change, the running animator is hiding the border. - mShowHideBorderAnimator.reverse(); - } else { - mShowHideBorderAnimator.start(); - } - }) + this::showBorder) .setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset) .setLayer(mBorderSurfaceControl, Integer.MAX_VALUE) .show(mBorderSurfaceControl) @@ -380,19 +433,25 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mHandler.removeCallbacks(mShowBorderRunnable); } - // We hide the border immediately as early as possible to beat the redrawing of window - // in response to the orientation change so users won't see a weird shape border. - mHandler.postAtFrontOfQueue(() -> { - mFullscreenBorder.setAlpha(0f); - }); - + // We hide the border immediately as early as possible to beat the redrawing of + // window in response to the orientation change so users won't see a weird shape + // border. + mHandler.postAtFrontOfQueue(mHideBorderImmediatelyRunnable); mHandler.postDelayed(mShowBorderRunnable, mLongAnimationTimeMs); } - private void showBorderWithNullCheck() { + @UiThread + private void hideBorderImmediately() { if (mShowHideBorderAnimator != null) { - mShowHideBorderAnimator.start(); + mShowHideBorderAnimator.cancel(); } + mFullscreenBorder.setAlpha(0f); + } + + @UiThread + private void showBorder() { + mShowHideBorderAnimator = createShowTargetAnimator(mFullscreenBorder); + mShowHideBorderAnimator.start(); } private void updateDimensions() { @@ -404,7 +463,9 @@ public class FullscreenMagnificationController implements ComponentCallbacks { R.dimen.magnifier_border_width_fullscreen_with_offset); } - private void applyCornerRadiusToBorder() { + @UiThread + @VisibleForTesting + void applyCornerRadiusToBorder() { if (!isActivated()) { return; } @@ -422,6 +483,20 @@ public class FullscreenMagnificationController implements ComponentCallbacks { backgroundDrawable.setCornerRadius(cornerRadius); } + @UiThread + private void setState(@FullscreenMagnificationActivationState int state) { + if (DEBUG) { + Log.d(TAG, "setState from " + mActivationState + " to " + state); + } + mActivationState = state; + } + + @VisibleForTesting + @UiThread + int getState() { + return mActivationState; + } + @Override public void onLowMemory() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 5f0acfa644dc..67aa4ff577b8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.display.DisplayManager; import android.os.Handler; -import android.os.UserHandle; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; @@ -58,7 +57,7 @@ public class AccessibilityFloatingMenuController implements private final AccessibilityButtonTargetsObserver mAccessibilityButtonTargetsObserver; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private Context mContext; + private final Context mContext; private final WindowManager mWindowManager; private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; private final DisplayManager mDisplayManager; @@ -226,7 +225,6 @@ public class AccessibilityFloatingMenuController implements @Override public void onUserInitializationComplete(int userId) { mIsUserInInitialization = false; - mContext = mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0); mBtnMode = mAccessibilityButtonModeObserver.getCurrentAccessibilityButtonMode(); mBtnTargets = mAccessibilityButtonTargetsObserver.getCurrentAccessibilityButtonTargets(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java index 121b51f768e7..bb6ab51aa56a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -79,6 +79,8 @@ class MenuInfoRepository { private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED; private final Context mContext; + // Pref always get the userId from the context to store SharedPreferences for the correct user + private final Context mCurrentUserContext; private final Configuration mConfiguration; private final AccessibilityManager mAccessibilityManager; private final AccessibilityManager.AccessibilityServicesStateChangeListener @@ -157,6 +159,9 @@ class MenuInfoRepository { OnContentsChanged settingsContentsChanged, SecureSettings secureSettings, @Nullable HearingAidDeviceManager hearingAidDeviceManager) { mContext = context; + final int currentUserId = secureSettings.getRealUserHandle(UserHandle.USER_CURRENT); + mCurrentUserContext = context.createContextAsUser( + UserHandle.of(currentUserId), /* flags= */ 0); mAccessibilityManager = accessibilityManager; mConfiguration = new Configuration(context.getResources().getConfiguration()); mSettingsContentsCallback = settingsContentsChanged; @@ -168,12 +173,13 @@ class MenuInfoRepository { void loadMenuMoveToTucked(OnInfoReady<Boolean> callback) { callback.onReady( - Prefs.getBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, + Prefs.getBoolean( + mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, DEFAULT_MOVE_TO_TUCKED_VALUE)); } void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) { - callback.onReady(Prefs.getBoolean(mContext, + callback.onReady(Prefs.getBoolean(mCurrentUserContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE)); } @@ -215,19 +221,19 @@ class MenuInfoRepository { } void updateMoveToTucked(boolean isMoveToTucked) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, + Prefs.putBoolean(mCurrentUserContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, isMoveToTucked); } void updateMenuSavingPosition(Position percentagePosition) { mPercentagePosition = percentagePosition; - Prefs.putString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, + Prefs.putString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, percentagePosition.toString()); } void updateDockTooltipVisibility(boolean hasSeen) { - Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, - hasSeen); + Prefs.putBoolean(mCurrentUserContext, + Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP, hasSeen); } void updateMigrationTooltipVisibility(boolean visible) { @@ -243,7 +249,7 @@ class MenuInfoRepository { } private Position getStartPosition() { - final String absolutePositionString = Prefs.getString(mContext, + final String absolutePositionString = Prefs.getString(mCurrentUserContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); final float defaultPositionXPercent = @@ -260,7 +266,7 @@ class MenuInfoRepository { mSecureSettings.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), /* notifyForDescendants */ false, mMenuTargetFeaturesContentObserver, UserHandle.USER_CURRENT); - if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) { + if (com.android.systemui.Flags.floatingMenuNotifyTargetsChangedOnStrictDiff()) { mSecureSettings.registerContentObserverForUserSync( mSecureSettings.getUriFor(ENABLED_ACCESSIBILITY_SERVICES), /* notifyForDescendants */ false, @@ -281,7 +287,7 @@ class MenuInfoRepository { UserHandle.USER_CURRENT); mContext.registerComponentCallbacks(mComponentCallbacks); - if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) { + if (com.android.systemui.Flags.floatingMenuNotifyTargetsChangedOnStrictDiff()) { mAccessibilityManager.addAccessibilityServicesStateChangeListener( mA11yServicesStateChangeListener); } @@ -311,7 +317,7 @@ class MenuInfoRepository { mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver); mContext.unregisterComponentCallbacks(mComponentCallbacks); - if (!com.android.systemui.Flags.floatingMenuNarrowTargetContentObserver()) { + if (com.android.systemui.Flags.floatingMenuNotifyTargetsChangedOnStrictDiff()) { mAccessibilityManager.removeAccessibilityServicesStateChangeListener( mA11yServicesStateChangeListener); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 3f49010aaaab..ae39b72b2585 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -284,13 +284,36 @@ class MenuView extends FrameLayout implements onEdgeChanged(); onPositionChanged(); - if (mFeaturesChangeListener != null) { + boolean shouldSendFeatureChangeNotification = + com.android.systemui.Flags.floatingMenuNotifyTargetsChangedOnStrictDiff() + ? !areFeatureListsIdentical(targetFeatures, newTargetFeatures) + : true; + if (mFeaturesChangeListener != null && shouldSendFeatureChangeNotification) { mFeaturesChangeListener.onChange(newTargetFeatures); } mMenuAnimationController.fadeOutIfEnabled(); } + /** + * Returns true if the given feature lists are identical lists, i.e. the same list of {@link + * AccessibilityTarget} (equality checked via UID) in the same order. + */ + private boolean areFeatureListsIdentical( + List<AccessibilityTarget> currentFeatures, List<AccessibilityTarget> newFeatures) { + if (currentFeatures.size() != newFeatures.size()) { + return false; + } + + for (int i = 0; i < currentFeatures.size(); i++) { + if (currentFeatures.get(i).getUid() != newFeatures.get(i).getUid()) { + return false; + } + } + + return true; + } + private void onMenuFadeEffectInfoChanged(MenuFadeEffectInfo fadeEffectInfo) { mMenuAnimationController.updateOpacityWith(fadeEffectInfo.isFadeEffectEnabled(), fadeEffectInfo.getOpacity()); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 184518ac35eb..e7470a34a065 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; import android.content.Context; import android.graphics.PixelFormat; @@ -90,7 +91,8 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT); params.receiveInsetsIgnoringZOrder = true; - params.privateFlags |= PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION; + params.privateFlags |= + PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION | SYSTEM_FLAG_SHOW_FOR_ALL_USERS; params.windowAnimations = android.R.style.Animation_Translucent; // Insets are configured to allow the menu to display over navigation and system bars. params.setFitInsetsTypes(0); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 73aabc3cf95a..438184d4d2d6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -286,9 +286,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { setupAmbientControls(); } - if (com.android.systemui.Flags.hearingDevicesDialogRelatedTools()) { - setupRelatedToolsView(dialog); - } + setupRelatedToolsView(dialog); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java index 02e65fd9031b..90b8fa0177b7 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogReceiver.java @@ -22,8 +22,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import com.android.systemui.Flags; - import javax.inject.Inject; /** @@ -43,10 +41,6 @@ public class HearingDevicesDialogReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (!Flags.hearingAidsQsTileDialog()) { - return; - } - if (ACTION.equals(intent.getAction())) { mDialogManager.showDialog(/* expandable= */ null, LAUNCH_SOURCE_A11Y); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt index 610e3f8a8c84..fb47d429e271 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt @@ -411,7 +411,7 @@ interface QSAccessibilityModule { stateInteractor: HearingDevicesTileDataInteractor, userActionInteractor: HearingDevicesTileUserActionInteractor, ): QSTileViewModel { - return if (Flags.hearingAidsQsTileDialog() && Flags.qsNewTilesFuture()) { + return if (Flags.qsNewTilesFuture()) { factory.create( TileSpec.create(HEARING_DEVICES_TILE_SPEC), userActionInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index d0cb507789a1..eee5f9e34317 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -1011,6 +1011,16 @@ public class AuthController implements } /** + * @return true if optical udfps HW is supported on this device. Can return true even if + * the user has not enrolled udfps. This may be false if called before + * onAllAuthenticatorsRegistered. + */ + public boolean isOpticalUdfpsSupported() { + return getUdfpsProps() != null && !getUdfpsProps().isEmpty() && getUdfpsProps() + .get(0).isOpticalUdfps(); + } + + /** * @return true if ultrasonic udfps HW is supported on this device. Can return true even if * the user has not enrolled udfps. This may be false if called before * onAllAuthenticatorsRegistered. diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt index 4dc2a13480f5..0303048436c9 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt @@ -104,6 +104,31 @@ constructor( } } + override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) { + withContext(backgroundDispatcher) { + if (!audioSharingInteractor.audioSharingAvailable()) { + return@withContext deviceItemActionInteractorImpl.onActionIconClick( + deviceItem, + onIntent, + ) + } + + when (deviceItem.type) { + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.CHECK_MARK_ACTION_BUTTON_CLICKED) + audioSharingInteractor.stopAudioSharing() + } + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.PLUS_ACTION_BUTTON_CLICKED) + audioSharingInteractor.startAudioSharing() + } + else -> { + deviceItemActionInteractorImpl.onActionIconClick(deviceItem, onIntent) + } + } + } + } + private fun inSharingAndDeviceNoSource( inAudioSharing: Boolean, deviceItem: DeviceItem, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt index c4f26cd46bf8..116e76c82008 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt @@ -29,6 +29,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest @@ -54,6 +55,8 @@ interface AudioSharingInteractor { suspend fun startAudioSharing() + suspend fun stopAudioSharing() + suspend fun audioSharingAvailable(): Boolean suspend fun qsDialogImprovementAvailable(): Boolean @@ -61,7 +64,7 @@ interface AudioSharingInteractor { @SysUISingleton @OptIn(ExperimentalCoroutinesApi::class) -class AudioSharingInteractorImpl +open class AudioSharingInteractorImpl @Inject constructor( private val context: Context, @@ -99,6 +102,9 @@ constructor( if (audioSharingAvailable()) { audioSharingRepository.leAudioBroadcastProfile?.let { profile -> isAudioSharingOn + // Skip the default value, we only care about adding source for newly + // started audio sharing session + .drop(1) .mapNotNull { audioSharingOn -> if (audioSharingOn) { // onBroadcastMetadataChanged could emit multiple times during one @@ -145,6 +151,13 @@ constructor( audioSharingRepository.startAudioSharing() } + override suspend fun stopAudioSharing() { + if (!audioSharingAvailable()) { + return + } + audioSharingRepository.stopAudioSharing() + } + // TODO(b/367965193): Move this after flags rollout override suspend fun audioSharingAvailable(): Boolean { return audioSharingRepository.audioSharingAvailable() @@ -181,6 +194,8 @@ class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingIntera override suspend fun startAudioSharing() {} + override suspend fun stopAudioSharing() {} + override suspend fun audioSharingAvailable(): Boolean = false override suspend fun qsDialogImprovementAvailable(): Boolean = false diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt index b9b8d36d41e6..44f9769f5930 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt @@ -45,6 +45,8 @@ interface AudioSharingRepository { suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) suspend fun startAudioSharing() + + suspend fun stopAudioSharing() } @SysUISingleton @@ -100,6 +102,15 @@ class AudioSharingRepositoryImpl( leAudioBroadcastProfile?.startPrivateBroadcast() } } + + override suspend fun stopAudioSharing() { + withContext(backgroundDispatcher) { + if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { + return@withContext + } + leAudioBroadcastProfile?.stopLatestBroadcast() + } + } } @SysUISingleton @@ -117,4 +128,6 @@ class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} override suspend fun startAudioSharing() {} + + override suspend fun stopAudioSharing() {} } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index b294dd1b0b71..56caddfbd637 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -56,6 +56,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext +data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) { + enum class Target { + ENTIRE_ROW, + ACTION_ICON, + } +} + /** Dialog for showing active, connected and saved bluetooth devices. */ class BluetoothTileDialogDelegate @AssistedInject @@ -80,7 +87,7 @@ internal constructor( internal val bluetoothAutoOnToggle get() = mutableBluetoothAutoOnToggle.asStateFlow() - private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> = + private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> = MutableSharedFlow(extraBufferCapacity = 1) internal val deviceItemClick get() = mutableDeviceItemClick.asSharedFlow() @@ -90,7 +97,7 @@ internal constructor( internal val contentHeight get() = mutableContentHeight.asSharedFlow() - private val deviceItemAdapter: Adapter = Adapter(bluetoothTileDialogCallback) + private val deviceItemAdapter: Adapter = Adapter() private var lastUiUpdateMs: Long = -1 @@ -334,8 +341,7 @@ internal constructor( } } - internal inner class Adapter(private val onClickCallback: BluetoothTileDialogCallback) : - RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { + internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() { private val diffUtilCallback = object : DiffUtil.ItemCallback<DeviceItem>() { @@ -376,7 +382,7 @@ internal constructor( override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) { val item = getItem(position) - holder.bind(item, onClickCallback) + holder.bind(item) } internal fun getItem(position: Int) = asyncListDiffer.currentList[position] @@ -390,19 +396,18 @@ internal constructor( private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name) private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary) private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon) - private val iconGear = view.requireViewById<ImageView>(R.id.gear_icon_image) - private val gearView = view.requireViewById<View>(R.id.gear_icon) + private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image) + private val actionIconView = view.requireViewById<View>(R.id.gear_icon) private val divider = view.requireViewById<View>(R.id.divider) - internal fun bind( - item: DeviceItem, - deviceItemOnClickCallback: BluetoothTileDialogCallback, - ) { + internal fun bind(item: DeviceItem) { container.apply { isEnabled = item.isEnabled background = item.background?.let { context.getDrawable(it) } setOnClickListener { - mutableDeviceItemClick.tryEmit(item) + mutableDeviceItemClick.tryEmit( + DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW) + ) uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED) } @@ -421,7 +426,8 @@ internal constructor( } } - iconGear.apply { drawable?.let { it.mutate()?.setTint(tintColor) } } + actionIcon.setImageResource(item.actionIconRes) + actionIcon.drawable?.setTint(tintColor) divider.setBackgroundColor(tintColor) @@ -454,8 +460,10 @@ internal constructor( nameView.text = item.deviceName summaryView.text = item.connectionSummary - gearView.setOnClickListener { - deviceItemOnClickCallback.onDeviceItemGearClicked(item, it) + actionIconView.setOnClickListener { + mutableDeviceItemClick.tryEmit( + DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt index aad233fe40ca..7c66ec059e64 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt @@ -49,7 +49,7 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED(1719), @Deprecated( "Use case no longer needed", - ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED") + ReplaceWith("LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED"), ) @UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked") LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720), @@ -59,7 +59,11 @@ enum class BluetoothTileDialogUiEvent(val metricId: Int) : UiEventLogger.UiEvent @UiEvent(doc = "Clicked on switch active button on audio sharing dialog") AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890), @UiEvent(doc = "Clicked on share audio button on audio sharing dialog") - AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891); + AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891), + @UiEvent(doc = "Clicked on plus action button") + PLUS_ACTION_BUTTON_CLICKED(2061), + @UiEvent(doc = "Clicked on checkmark action button") + CHECK_MARK_ACTION_BUTTON_CLICKED(2062); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt index 497d8cf2e159..9460e7c2c8d5 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt @@ -35,7 +35,6 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE import com.android.systemui.dagger.SysUISingleton @@ -227,8 +226,22 @@ constructor( // deviceItemClick is emitted when user clicked on a device item. dialogDelegate.deviceItemClick .onEach { - deviceItemActionInteractor.onClick(it, dialog) - logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type) + when (it.target) { + DeviceItemClick.Target.ENTIRE_ROW -> { + deviceItemActionInteractor.onClick(it.deviceItem, dialog) + logger.logDeviceClick( + it.deviceItem.cachedBluetoothDevice.address, + it.deviceItem.type, + ) + } + + DeviceItemClick.Target.ACTION_ICON -> { + deviceItemActionInteractor.onActionIconClick(it.deviceItem) { intent + -> + startSettingsActivity(intent, it.clickedView) + } + } + } } .launchIn(this) @@ -287,20 +300,6 @@ constructor( ) } - override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) { - uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED) - val intent = - Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { - putExtra( - EXTRA_SHOW_FRAGMENT_ARGUMENTS, - Bundle().apply { - putString("device_address", deviceItem.cachedBluetoothDevice.address) - }, - ) - } - startSettingsActivity(intent, view) - } - override fun onSeeAllClicked(view: View) { uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED) startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view) @@ -382,8 +381,6 @@ constructor( } interface BluetoothTileDialogCallback { - fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) - fun onSeeAllClicked(view: View) fun onPairNewDeviceClicked(view: View) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt index 2ba4c73a0293..f7af16d99fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItem.kt @@ -53,5 +53,6 @@ data class DeviceItem( val background: Int? = null, var isEnabled: Boolean = true, var actionAccessibilityLabel: String = "", - var isActive: Boolean = false + var isActive: Boolean = false, + val actionIconRes: Int = -1, ) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt index 2b55e1c51f5f..cb4ec37a1a66 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt @@ -16,6 +16,8 @@ package com.android.systemui.bluetooth.qsdialog +import android.content.Intent +import android.os.Bundle import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -25,7 +27,9 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext interface DeviceItemActionInteractor { - suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {} + suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) + + suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) } @SysUISingleton @@ -67,4 +71,44 @@ constructor( } } } + + override suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit) { + withContext(backgroundDispatcher) { + deviceItem.cachedBluetoothDevice.apply { + when (deviceItem.type) { + DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, + DeviceItemType.SAVED_BLUETOOTH_DEVICE -> { + uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_GEAR_CLICKED) + val intent = + Intent(ACTION_BLUETOOTH_DEVICE_DETAILS).apply { + putExtra( + EXTRA_SHOW_FRAGMENT_ARGUMENTS, + Bundle().apply { + putString( + "device_address", + deviceItem.cachedBluetoothDevice.address, + ) + }, + ) + } + onIntent(intent) + } + DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, + DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { + throw IllegalArgumentException("Invalid device type: ${deviceItem.type}") + // Throw exception. Should already be handled in + // AudioSharingDeviceItemActionInteractor. + } + } + } + } + } + + private companion object { + const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" + const val ACTION_BLUETOOTH_DEVICE_DETAILS = + "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS" + } } diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt index 92f05803f7cf..095e6e741584 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt @@ -30,6 +30,8 @@ private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy private val connected = R.string.quick_settings_bluetooth_device_connected private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing +private val audioSharingAddIcon = R.drawable.ic_add +private val audioSharingOnGoingIcon = R.drawable.ic_check private val saved = R.string.quick_settings_bluetooth_device_saved private val actionAccessibilityLabelActivate = R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate @@ -63,6 +65,7 @@ abstract class DeviceItemFactory { background: Int, actionAccessibilityLabel: String, isActive: Boolean, + actionIconRes: Int = R.drawable.ic_settings_24dp, ): DeviceItem { return DeviceItem( type = type, @@ -75,6 +78,7 @@ abstract class DeviceItemFactory { isEnabled = !cachedDevice.isBusy, actionAccessibilityLabel = actionAccessibilityLabel, isActive = isActive, + actionIconRes = actionIconRes, ) } } @@ -125,6 +129,7 @@ internal class AudioSharingMediaDeviceItemFactory( if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn, "", isActive = !cachedDevice.isBusy, + actionIconRes = audioSharingOnGoingIcon, ) } } @@ -156,6 +161,7 @@ internal class AvailableAudioSharingMediaDeviceItemFactory( if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, "", isActive = false, + actionIconRes = audioSharingAddIcon, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt index 434a9ce58c3b..d5c815d649c4 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt @@ -170,6 +170,8 @@ object KeyguardBouncerViewBinder { launch { viewModel.startDisappearAnimation.collect { + android.util.Log.i("KeyguardBouncerViewBinder", + "viewModel.startDisappearAnimation: $it") securityContainerController.startDisappearAnimation(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt index 8814a1298e44..c0ad3b8c97c9 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardImageLoader.kt @@ -20,15 +20,17 @@ import android.graphics.Bitmap import android.net.Uri import android.util.Log import android.util.Size -import com.android.systemui.res.R +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.Flags.clipboardOverlayMultiuser import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import com.android.systemui.settings.UserTracker import java.io.IOException import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull @@ -36,8 +38,9 @@ class ClipboardImageLoader @Inject constructor( private val context: Context, + private val userTracker: UserTracker, @Background private val bgDispatcher: CoroutineDispatcher, - @Application private val mainScope: CoroutineScope + @Application private val mainScope: CoroutineScope, ) { private val TAG: String = "ClipboardImageLoader" @@ -46,7 +49,15 @@ constructor( withContext(bgDispatcher) { try { val size = context.resources.getDimensionPixelSize(R.dimen.overlay_x_scale) - context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null) + if (clipboardOverlayMultiuser()) { + userTracker.userContentResolver.loadThumbnail( + uri, + Size(size, size * 4), + null, + ) + } else { + context.contentResolver.loadThumbnail(uri, Size(size, size * 4), null) + } } catch (e: IOException) { Log.e(TAG, "Thumbnail loading failed!", e) null diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt index 78156dbc8964..ca49de3b1510 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt @@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TransitionKey */ object CommunalTransitionKeys { /** Fades the glanceable hub without any translation */ + @Deprecated("No longer supported as all hub transitions will be fades.") val SimpleFade = TransitionKey("SimpleFade") /** Transition from the glanceable hub before entering edit mode */ val ToEditMode = TransitionKey("ToEditMode") diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 00eead6eb7fc..555fe6ef157d 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.phone.ConfigurationForwarder; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -115,6 +116,9 @@ public interface SysUIComponent { @BindsInstance Builder setDesktopMode(Optional<DesktopMode> d); + @BindsInstance + Builder setAppZoomOut(Optional<AppZoomOut> a); + SysUIComponent build(); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 7ebe52f3bd58..c02784dfab1b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -31,6 +31,7 @@ import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.CameraProtectionModule; import com.android.systemui.CoreStartable; +import com.android.systemui.KairosCoreStartableModule; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.activity.ActivityManagerModule; import com.android.systemui.ambient.dagger.AmbientModule; @@ -232,6 +233,7 @@ import javax.inject.Named; FlagsModule.class, FlagDependenciesModule.class, FooterActionsModule.class, + KairosCoreStartableModule.class, GestureModule.class, InputMethodModule.class, KeyEventRepositoryModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 41a59a959771..ae6238724042 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.util.kotlin.FlowDumperImpl @@ -50,6 +51,8 @@ class DeviceEntryHapticsInteractor constructor( biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + keyguardBypassInteractor: KeyguardBypassInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -82,7 +85,7 @@ constructor( emit(recentPowerButtonPressThresholdMs * -1L - 1L) } - val playSuccessHaptic: Flow<Unit> = + private val playHapticsOnDeviceEntry: Flow<Boolean> = deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( @@ -92,17 +95,29 @@ constructor( ::Triple, ) ) - .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> val sideFpsAllowsHaptic = !powerButtonDown && systemClock.uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic if (!allowHaptic) { - logger.d("Skip success haptic. Recent power button press or button is down.") + logger.d( + "Skip success entry haptic from power button. Recent power button press or button is down." + ) } allowHaptic } + + private val playHapticsOnFaceAuthSuccessAndBypassDisabled: Flow<Boolean> = + deviceEntryFaceAuthInteractor.isAuthenticated + .filter { it } + .sample(keyguardBypassInteractor.isBypassAvailable) + .map { !it } + + val playSuccessHaptic: Flow<Unit> = + merge(playHapticsOnDeviceEntry, playHapticsOnFaceAuthSuccessAndBypassDisabled) + .filter { it } // map to Unit .map {} .dumpWhileCollecting("playSuccessHaptic") diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt index 21002c676ec6..d7a4dba3188a 100644 --- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt @@ -278,11 +278,11 @@ constructor( } private suspend fun hasInitialDelayElapsed(deviceType: DeviceType): Boolean { - val oobeLaunchTime = - tutorialRepository.getScheduledTutorialLaunchTime(deviceType) ?: return false - return clock - .instant() - .isAfter(oobeLaunchTime.plusSeconds(initialDelayDuration.inWholeSeconds)) + val oobeTime = + tutorialRepository.getScheduledTutorialLaunchTime(deviceType) + ?: tutorialRepository.getNotifiedTime(deviceType) + ?: return false + return clock.instant().isAfter(oobeTime.plusSeconds(initialDelayDuration.inWholeSeconds)) } private data class StatsUpdateRequest( diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 129a6bb72996..2ed0671a570b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,13 +23,7 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON -import com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS -import com.android.systemui.Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP import com.android.systemui.Flags.communalHub -import com.android.systemui.Flags.statusBarCallChipNotificationIcon -import com.android.systemui.Flags.statusBarScreenSharingChips -import com.android.systemui.Flags.statusBarUseReposForCallChip import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.shared.flag.DualShade @@ -63,10 +57,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // DualShade dependencies DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag() - - // Status bar chip dependencies - statusBarCallChipNotificationIconToken dependsOn statusBarUseReposForCallChipToken - statusBarCallChipNotificationIconToken dependsOn statusBarScreenSharingChipsToken } private inline val politeNotifications @@ -86,17 +76,4 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) - - private inline val statusBarCallChipNotificationIconToken - get() = - FlagToken( - FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, - statusBarCallChipNotificationIcon(), - ) - - private inline val statusBarScreenSharingChipsToken - get() = FlagToken(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, statusBarScreenSharingChips()) - - private inline val statusBarUseReposForCallChipToken - get() = FlagToken(FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP, statusBarUseReposForCallChip()) } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index e1ebf7cdf472..cf5c3402792e 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -463,6 +463,9 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mTelephonyListenerManager.removeServiceStateListener(mPhoneStateListener); mGlobalSettings.unregisterContentObserverSync(mAirplaneModeObserver); mConfigurationController.removeCallback(this); + if (mShowSilentToggle) { + mRingerModeTracker.getRingerMode().removeObservers(this); + } } protected Context getContext() { diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index c40adfe6baf8..08e0a9d52faa 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -45,11 +46,13 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted +import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize sealed interface TutorialActionState { data object NotStarted : TutorialActionState @@ -82,18 +85,25 @@ fun ActionTutorialContent( ) { Column( verticalArrangement = Arrangement.Center, - modifier = - Modifier.fillMaxSize() - .background(config.colors.background) - .safeDrawingPadding() - .padding(start = 48.dp, top = 100.dp, end = 48.dp, bottom = 8.dp), + modifier = Modifier.fillMaxSize().background(config.colors.background).safeDrawingPadding(), ) { + val isCompactWindow = hasCompactWindowSize() when (LocalConfiguration.current.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { - HorizontalDescriptionAndAnimation(actionState, config, Modifier.weight(1f)) + HorizontalDescriptionAndAnimation( + actionState, + config, + isCompactWindow, + Modifier.weight(1f), + ) } else -> { - VerticalDescriptionAndAnimation(actionState, config, Modifier.weight(1f)) + VerticalDescriptionAndAnimation( + actionState, + config, + isCompactWindow, + Modifier.weight(1f), + ) } } val buttonAlpha by animateFloatAsState(if (actionState is Finished) 1f else 0f) @@ -109,11 +119,15 @@ fun ActionTutorialContent( private fun HorizontalDescriptionAndAnimation( actionState: TutorialActionState, config: TutorialScreenConfig, + isCompactWindow: Boolean, modifier: Modifier = Modifier, ) { - Row(modifier = modifier.fillMaxWidth()) { - TutorialDescription(actionState, config, modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.width(70.dp)) + Row( + modifier = + modifier.fillMaxWidth().padding(start = 48.dp, top = 100.dp, end = 48.dp, bottom = 8.dp) + ) { + TutorialDescription(actionState, config, isCompactWindow, modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(24.dp)) TutorialAnimation(actionState, config, modifier = Modifier.weight(1f)) } } @@ -122,20 +136,25 @@ private fun HorizontalDescriptionAndAnimation( private fun VerticalDescriptionAndAnimation( actionState: TutorialActionState, config: TutorialScreenConfig, + isCompactWindow: Boolean, modifier: Modifier = Modifier, ) { - Column(modifier = modifier.fillMaxWidth().padding(horizontal = 40.dp, vertical = 40.dp)) { - Spacer(modifier = Modifier.weight(0.1f)) + val horizontalPadding = if (isCompactWindow) 24.dp else 96.dp + // Represents the majority of tablets in portrait - we need extra spacer at the top and bottom + val isTablet = LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Expanded + Column( + modifier = + modifier.fillMaxWidth().padding(start = 0.dp, top = 100.dp, end = 0.dp, bottom = 8.dp) + ) { + if (isTablet) Spacer(modifier = Modifier.weight(0.3f)) TutorialDescription( actionState, config, - modifier = - Modifier.weight(0.2f) - // extra padding to better align with animation which has embedded padding - .padding(horizontal = 15.dp), + isCompactWindow, + modifier = Modifier.weight(1f).padding(horizontal = horizontalPadding), ) - Spacer(modifier = Modifier.width(70.dp)) - TutorialAnimation(actionState, config, modifier = Modifier.weight(1f)) + TutorialAnimation(actionState, config, modifier = Modifier.weight(1.8f).fillMaxWidth()) + if (isTablet) Spacer(modifier = Modifier.weight(0.3f)) } } @@ -143,6 +162,7 @@ private fun VerticalDescriptionAndAnimation( fun TutorialDescription( actionState: TutorialActionState, config: TutorialScreenConfig, + isCompactWindow: Boolean, modifier: Modifier = Modifier, ) { val focusRequester = remember { FocusRequester() } @@ -159,7 +179,9 @@ fun TutorialDescription( Column(verticalArrangement = Arrangement.Top, modifier = modifier) { Text( text = stringResource(id = titleTextId), - style = MaterialTheme.typography.displayLarge, + style = + if (isCompactWindow) MaterialTheme.typography.headlineLarge + else MaterialTheme.typography.displayMedium, color = config.colors.title, modifier = Modifier.focusRequester(focusRequester).focusable(), ) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt index e255bdea6100..6a42cdc876ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt @@ -41,6 +41,8 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVI import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System @@ -63,6 +65,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to System, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to System, KEY_GESTURE_TYPE_ALL_APPS to System, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to System, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to MultiTasking, @@ -100,6 +103,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.shortcut_helper_category_system_apps, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.shortcut_helper_category_system_apps, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.shortcut_helper_category_system_apps, // Multitasking Category KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to @@ -148,6 +152,7 @@ class InputGestureMaps @Inject constructor(private val context: Context) { KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to R.string.group_system_access_google_assistant, KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to R.string.group_system_access_google_assistant, + KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS to R.string.group_system_toggle_voice_access, // Multitasking Category KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt index 5060abdda247..c3c9df97a682 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/SystemShortcutsSource.kt @@ -32,12 +32,14 @@ import android.view.KeyEvent.KEYCODE_RECENT_APPS import android.view.KeyEvent.KEYCODE_S import android.view.KeyEvent.KEYCODE_SLASH import android.view.KeyEvent.KEYCODE_TAB +import android.view.KeyEvent.KEYCODE_V import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import android.view.KeyEvent.META_SHIFT_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +import com.android.hardware.input.Flags.enableVoiceAccessKeyGestures import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyboard.shortcut.data.model.shortcutInfo @@ -118,8 +120,8 @@ constructor(@Main private val resources: Resources, private val inputManager: In return shortcuts } - private fun systemControlsShortcuts() = - listOf( + private fun systemControlsShortcuts(): List<KeyboardShortcutInfo> { + val shortcuts = mutableListOf( // Access list of all apps and search (i.e. Search/Launcher): // - Meta shortcutInfo(resources.getString(R.string.group_system_access_all_apps_search)) { @@ -176,6 +178,19 @@ constructor(@Main private val resources: Resources, private val inputManager: In }, ) + if (enableVoiceAccessKeyGestures()) { + shortcuts.add( + // Toggle voice access: + // - Meta + Alt + V + shortcutInfo(resources.getString(R.string.group_system_toggle_voice_access)) { + command(META_META_ON or META_ALT_ON, KEYCODE_V) + } + ) + } + + return shortcuts + } + private fun systemAppsShortcuts() = listOf( // Pull up Notes app for quick memo: diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt index 274fa59045d7..a16b4a6892b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt @@ -53,11 +53,11 @@ constructor( override suspend fun onActivated(): Nothing { viewModel.shortcutCustomizationUiState.collect { uiState -> - when(uiState){ + when (uiState) { is AddShortcutDialog, is DeleteShortcutDialog, is ResetShortcutDialog -> { - if (dialog == null){ + if (dialog == null) { dialog = createDialog().also { it.show() } } } @@ -85,7 +85,9 @@ constructor( ShortcutCustomizationDialog( uiState = uiState, modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp), - onKeyPress = { viewModel.onKeyPressed(it) }, + onShortcutKeyCombinationSelected = { + viewModel.onShortcutKeyCombinationSelected(it) + }, onCancel = { dialog.dismiss() }, onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } }, onConfirmDeleteShortcut = { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt index 3819f6d41856..d9e55f89cda5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutCustomizer.kt @@ -49,8 +49,12 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -65,7 +69,7 @@ import com.android.systemui.res.R fun ShortcutCustomizationDialog( uiState: ShortcutCustomizationUiState, modifier: Modifier = Modifier, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, onConfirmSetShortcut: () -> Unit, onConfirmDeleteShortcut: () -> Unit, @@ -73,7 +77,13 @@ fun ShortcutCustomizationDialog( ) { when (uiState) { is ShortcutCustomizationUiState.AddShortcutDialog -> { - AddShortcutDialog(modifier, uiState, onKeyPress, onCancel, onConfirmSetShortcut) + AddShortcutDialog( + modifier, + uiState, + onShortcutKeyCombinationSelected, + onCancel, + onConfirmSetShortcut, + ) } is ShortcutCustomizationUiState.DeleteShortcutDialog -> { DeleteShortcutDialog(modifier, onCancel, onConfirmDeleteShortcut) @@ -91,29 +101,27 @@ fun ShortcutCustomizationDialog( private fun AddShortcutDialog( modifier: Modifier, uiState: ShortcutCustomizationUiState.AddShortcutDialog, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, onCancel: () -> Unit, - onConfirmSetShortcut: () -> Unit -){ + onConfirmSetShortcut: () -> Unit, +) { Column(modifier = modifier) { Title(uiState.shortcutLabel) Description( - text = - stringResource( - id = R.string.shortcut_customize_mode_add_shortcut_description - ) + text = stringResource(id = R.string.shortcut_customize_mode_add_shortcut_description) ) PromptShortcutModifier( modifier = - Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) - .width(131.dp) - .height(48.dp), + Modifier.padding(top = 24.dp, start = 116.5.dp, end = 116.5.dp) + .width(131.dp) + .height(48.dp), defaultModifierKey = uiState.defaultCustomShortcutModifierKey, ) SelectedKeyCombinationContainer( shouldShowError = uiState.errorMessage.isNotEmpty(), - onKeyPress = onKeyPress, + onShortcutKeyCombinationSelected = onShortcutKeyCombinationSelected, pressedKeys = uiState.pressedKeys, + onConfirmSetShortcut = onConfirmSetShortcut, ) ErrorMessageContainer(uiState.errorMessage) DialogButtons( @@ -121,9 +129,7 @@ private fun AddShortcutDialog( isConfirmButtonEnabled = uiState.pressedKeys.isNotEmpty(), onConfirm = onConfirmSetShortcut, confirmButtonText = - stringResource( - R.string.shortcut_helper_customize_dialog_set_shortcut_button_label - ), + stringResource(R.string.shortcut_helper_customize_dialog_set_shortcut_button_label), ) } } @@ -132,20 +138,15 @@ private fun AddShortcutDialog( private fun DeleteShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmDeleteShortcut: () -> Unit -){ + onConfirmDeleteShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_remove_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_remove_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_remove_button_label), onCancel = onCancel, onConfirm = onConfirmDeleteShortcut, ) @@ -155,20 +156,15 @@ private fun DeleteShortcutDialog( private fun ResetShortcutDialog( modifier: Modifier, onCancel: () -> Unit, - onConfirmResetShortcut: () -> Unit -){ + onConfirmResetShortcut: () -> Unit, +) { ConfirmationDialog( modifier = modifier, - title = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title - ), + title = stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_dialog_title), description = - stringResource( - id = R.string.shortcut_customize_mode_reset_shortcut_description - ), + stringResource(id = R.string.shortcut_customize_mode_reset_shortcut_description), confirmButtonText = - stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), + stringResource(R.string.shortcut_helper_customize_dialog_reset_button_label), onCancel = onCancel, onConfirm = onConfirmResetShortcut, ) @@ -201,6 +197,9 @@ private fun DialogButtons( onConfirm: () -> Unit, confirmButtonText: String, ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.requestFocus() } + Row( modifier = Modifier.padding(top = 24.dp, start = 24.dp, end = 24.dp) @@ -218,6 +217,10 @@ private fun DialogButtons( ) Spacer(modifier = Modifier.width(8.dp)) ShortcutHelperButton( + modifier = + Modifier.focusRequester(focusRequester).focusProperties { + canFocus = true + }, // enable focus on touch/click mode onClick = onConfirm, color = MaterialTheme.colorScheme.primary, width = 116.dp, @@ -248,8 +251,9 @@ private fun ErrorMessageContainer(errorMessage: String) { @Composable private fun SelectedKeyCombinationContainer( shouldShowError: Boolean, - onKeyPress: (KeyEvent) -> Boolean, + onShortcutKeyCombinationSelected: (KeyEvent) -> Boolean, pressedKeys: List<ShortcutKey>, + onConfirmSetShortcut: () -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() @@ -269,7 +273,17 @@ private fun SelectedKeyCombinationContainer( Modifier.padding(all = 16.dp) .sizeIn(minWidth = 332.dp, minHeight = 56.dp) .border(width = 2.dp, color = outlineColor, shape = RoundedCornerShape(50.dp)) - .onKeyEvent { onKeyPress(it) } + .onPreviewKeyEvent { keyEvent -> + val keyEventProcessed = onShortcutKeyCombinationSelected(keyEvent) + if ( + !keyEventProcessed && + keyEvent.key == Key.Enter && + keyEvent.type == KeyEventType.KeyUp + ) { + onConfirmSetShortcut() + true + } else keyEventProcessed + } .focusProperties { canFocus = true } // enables keyboard focus when in touch mode .focusRequester(focusRequester), interactionSource = interactionSource, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index aea583d67289..ba31d08c9c1b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -729,6 +729,7 @@ private fun AddShortcutButton(onClick: () -> Unit) { contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_add_shortcut_button_label), ) } @@ -749,6 +750,7 @@ private fun DeleteShortcutButton(onClick: () -> Unit) { contentColor = MaterialTheme.colorScheme.primary, contentPaddingVertical = 0.dp, contentPaddingHorizontal = 0.dp, + contentDescription = stringResource(R.string.shortcut_helper_delete_shortcut_button_label), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt index 55c0fe297bcb..9a380f495176 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/Surfaces.kt @@ -230,6 +230,7 @@ fun ShortcutHelperButton( contentPaddingVertical: Dp = 10.dp, enabled: Boolean = true, border: BorderStroke? = null, + contentDescription: String? = null, ) { ShortcutHelperButtonSurface( onClick = onClick, @@ -254,8 +255,7 @@ fun ShortcutHelperButton( Icon( tint = contentColor, imageVector = iconSource.imageVector, - contentDescription = - null, // TODO this probably should not be null for accessibility. + contentDescription = contentDescription, modifier = Modifier.size(20.dp).wrapContentSize(Alignment.Center), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt index 373eb250d61d..915a66c43a12 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt @@ -46,6 +46,7 @@ constructor( private val context: Context, private val shortcutCustomizationInteractor: ShortcutCustomizationInteractor, ) { + private var keyDownEventCache: KeyEvent? = null private val _shortcutCustomizationUiState = MutableStateFlow<ShortcutCustomizationUiState>(ShortcutCustomizationUiState.Inactive) @@ -94,9 +95,16 @@ constructor( shortcutCustomizationInteractor.updateUserSelectedKeyCombination(null) } - fun onKeyPressed(keyEvent: KeyEvent): Boolean { - if ((keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown)) { - updatePressedKeys(keyEvent) + fun onShortcutKeyCombinationSelected(keyEvent: KeyEvent): Boolean { + if (isModifier(keyEvent)) { + return false + } + if (keyEvent.isMetaPressed && keyEvent.type == KeyEventType.KeyDown) { + keyDownEventCache = keyEvent + return true + } else if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == keyDownEventCache?.key) { + updatePressedKeys(keyDownEventCache!!) + clearKeyDownEventCache() return true } return false @@ -157,16 +165,21 @@ constructor( return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState } + private fun isModifier(keyEvent: KeyEvent) = SUPPORTED_MODIFIERS.contains(keyEvent.key) + private fun updatePressedKeys(keyEvent: KeyEvent) { - val isModifier = SUPPORTED_MODIFIERS.contains(keyEvent.key) val keyCombination = KeyCombination( modifiers = keyEvent.nativeKeyEvent.modifiers, - keyCode = if (!isModifier) keyEvent.key.nativeKeyCode else null, + keyCode = if (!isModifier(keyEvent)) keyEvent.key.nativeKeyCode else null, ) shortcutCustomizationInteractor.updateUserSelectedKeyCombination(keyCombination) } + private fun clearKeyDownEventCache() { + keyDownEventCache = null + } + @AssistedFactory interface Factory { fun create(): ShortcutCustomizationViewModel 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 9896365abff9..b42da5265d86 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 @@ -132,6 +132,8 @@ constructor( if (SceneContainerFlag.isEnabled) return@collect startTransitionTo( toState = KeyguardState.GONE, + modeOnCanceled = TransitionModeOnCanceled.REVERSE, + ownerReason = "canWakeDirectlyToGone = true", ) } else if (shouldTransitionToLockscreen) { val modeOnCanceled = @@ -146,7 +148,7 @@ constructor( startTransitionTo( toState = KeyguardState.LOCKSCREEN, modeOnCanceled = modeOnCanceled, - ownerReason = "listen for aod to awake" + ownerReason = "listen for aod to awake", ) } else if (shouldTransitionToOccluded) { startTransitionTo( 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 fbe31bbf36e6..8f7f2a0a8cbb 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 @@ -44,7 +44,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -84,7 +83,6 @@ class KeyguardInteractor @Inject constructor( private val repository: KeyguardRepository, - powerInteractor: PowerInteractor, bouncerRepository: KeyguardBouncerRepository, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, shadeRepository: ShadeRepository, @@ -216,11 +214,7 @@ constructor( // should actually be quite strange to leave AOD and then go straight to // DREAMING so this should be fine. delay(IS_ABLE_TO_DREAM_DELAY_MS) - isDreaming - .sample(powerInteractor.isAwake) { isDreaming, isAwake -> - isDreaming && isAwake - } - .debounce(50L) + isDreaming.debounce(50L) } else { flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt index a133f06b3f41..3bdc32dce6f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardWakeDirectlyToGoneInteractor.kt @@ -116,9 +116,10 @@ constructor( * - We're wake and unlocking (fingerprint auth occurred while asleep). * - We're allowed to ignore auth and return to GONE, due to timeouts not elapsing. * - We're DREAMING and dismissible. - * - We're already GONE. Technically you're already awake when GONE, but this makes it easier to - * reason about this state (for example, if canWakeDirectlyToGone, don't tell WM to pause the - * top activity - something you should never do while GONE as well). + * - We're already GONE and not transitioning out of GONE. Technically you're already awake when + * GONE, but this makes it easier to reason about this state (for example, if + * canWakeDirectlyToGone, don't tell WM to pause the top activity - something you should never + * do while GONE as well). */ val canWakeDirectlyToGone = combine( @@ -138,7 +139,8 @@ constructor( canIgnoreAuthAndReturnToGone || (currentState == KeyguardState.DREAMING && keyguardInteractor.isKeyguardDismissible.value) || - currentState == KeyguardState.GONE + (currentState == KeyguardState.GONE && + transitionInteractor.getStartedState() == KeyguardState.GONE) } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt index 542fb9b46bef..3eb8522e0338 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/BlurConfig.kt @@ -23,4 +23,10 @@ data class BlurConfig(val minBlurRadiusPx: Float, val maxBlurRadiusPx: Float) { // No-op config that will be used by dagger of other SysUI variants which don't blur the // background surface. @Inject constructor() : this(0.0f, 0.0f) + + companion object { + // Blur the shade much lesser than the background surface so that the surface is + // distinguishable from the background. + @JvmStatic fun Float.maxBlurRadiusToNotificationPanelBlurRadius(): Float = this / 3.0f + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt index e77e9dd9e9ed..eb1afb406d2b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/PrimaryBouncerTransition.kt @@ -30,6 +30,9 @@ interface PrimaryBouncerTransition { /** Radius of blur applied to the window's root view. */ val windowBlurRadius: Flow<Float> + /** Radius of blur applied to the notifications on expanded shade */ + val notificationBlurRadius: Flow<Float> + fun transitionProgressToBlurRadius( starBlurRadius: Float, endBlurRadius: Float, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index f17455788d6e..92bb5e6029cb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCE import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -73,7 +75,28 @@ constructor( val lockscreenAlpha: Flow<Float> = if (WindowBlurFlag.isEnabled) alphaFlow else emptyFlow() - val notificationAlpha: Flow<Float> = alphaFlow + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + alphaFlow + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow<Float>() + } override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt index dbb6a49e7844..e3b55874de6f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt @@ -53,4 +53,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt index d8b617a60129..c937d5c6453d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DozingToPrimaryBouncerTransitionViewModel.kt @@ -64,4 +64,6 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio }, onFinish = { blurConfig.maxBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt index 597df15a2b55..5ab458334a25 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt index c373fd01ba20..44c4c8723dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -23,6 +24,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import com.android.systemui.keyguard.ui.transitions.BlurConfig +import com.android.systemui.keyguard.ui.transitions.BlurConfig.Companion.maxBlurRadiusToNotificationPanelBlurRadius import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -32,6 +34,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow /** * Breaks down LOCKSCREEN->PRIMARY BOUNCER transition into discrete steps for corresponding views to @@ -70,6 +73,29 @@ constructor( val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val notificationAlpha: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = lockscreenAlpha, + flowWhenShadeIsExpanded = transitionAnimation.immediatelyTransitionTo(1f), + ) + } else { + lockscreenAlpha + } + + override val notificationBlurRadius: Flow<Float> = + if (Flags.bouncerUiRevamp()) { + shadeDependentFlows.transitionFlow( + flowWhenShadeIsNotExpanded = emptyFlow(), + flowWhenShadeIsExpanded = + transitionAnimation.immediatelyTransitionTo( + blurConfig.maxBlurRadiusPx.maxBlurRadiusToNotificationPanelBlurRadius() + ), + ) + } else { + emptyFlow() + } + override val deviceEntryParentViewAlpha: Flow<Float> = shadeDependentFlows.transitionFlow( flowWhenShadeIsNotExpanded = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt index 44598107fa4b..4d3e27265cea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(blurConfig: BlurConfig, animationFlow: KeyguardTransitionAnimationFl override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt index fab8008cbfa7..224191b64f5f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToAodTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt index eebdf2ef418e..0f8495f34d22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToDozingTransitionViewModel.kt @@ -80,4 +80,6 @@ constructor( }, onFinish = { blurConfig.minBlurRadiusPx }, ) + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt index 3636b747d5c9..a13eef2388f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGlanceableHubTransitionViewModel.kt @@ -43,4 +43,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 4ed3e6cde230..d1233f220f47 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -166,6 +166,9 @@ constructor( createBouncerWindowBlurFlow(primaryBouncerInteractor::willRunDismissFromKeyguard) } + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) + val scrimAlpha: Flow<ScrimAlpha> = bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, PRIMARY_BOUNCER) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 2edc93cb5617..c53a408a88e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -91,4 +91,7 @@ constructor( }, ), ) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt index 3a54a26858d4..fe1708efea2f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt @@ -42,4 +42,7 @@ constructor(private val blurConfig: BlurConfig, animationFlow: KeyguardTransitio override val windowBlurRadius: Flow<Float> = transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx) + + override val notificationBlurRadius: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0.0f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt index 09544827a51a..a6b9442b1270 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/Media3ActionFactory.kt @@ -31,6 +31,7 @@ import androidx.media3.session.CommandButton import androidx.media3.session.MediaController as Media3Controller import androidx.media3.session.SessionCommand import androidx.media3.session.SessionToken +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -128,7 +129,11 @@ constructor( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -230,17 +235,33 @@ constructor( Player.COMMAND_PLAY_PAUSE -> { if (!controller.isPlaying) { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } else { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { executeAction(packageName, token, Player.COMMAND_PLAY_PAUSE) }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt index 4f9791353b8a..9bf556cf07c2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt @@ -29,6 +29,7 @@ import android.media.session.PlaybackState import android.service.notification.StatusBarNotification import android.util.Log import androidx.media.utils.MediaConstants +import com.android.systemui.Flags import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS import com.android.systemui.media.controls.shared.MediaControlDrawables @@ -69,7 +70,11 @@ fun createActionsFromState( drawable, null, // no action to perform when clicked context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_connecting_status_container) + } else { + context.getDrawable(R.drawable.ic_media_connecting_container) + }, // Specify a rebind id to prevent the spinner from restarting on later binds. com.android.internal.R.drawable.progress_small_material, ) @@ -157,18 +162,34 @@ private fun getStandardAction( return when (action) { PlaybackState.ACTION_PLAY -> { MediaAction( - context.getDrawable(R.drawable.ic_media_play), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button) + } else { + context.getDrawable(R.drawable.ic_media_play) + }, { controller.transportControls.play() }, context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_play_button_container) + } else { + context.getDrawable(R.drawable.ic_media_play_container) + }, ) } PlaybackState.ACTION_PAUSE -> { MediaAction( - context.getDrawable(R.drawable.ic_media_pause), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button) + } else { + context.getDrawable(R.drawable.ic_media_pause) + }, { controller.transportControls.pause() }, context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container), + if (Flags.mediaControlsUiUpdate()) { + context.getDrawable(R.drawable.ic_media_pause_button_container) + } else { + context.getDrawable(R.drawable.ic_media_pause_container) + }, ) } PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt index 21407f3bd6d4..02b403786768 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransition.kt @@ -27,6 +27,7 @@ import android.graphics.drawable.RippleDrawable import com.android.internal.R import com.android.internal.annotations.VisibleForTesting import com.android.settingslib.Utils +import com.android.systemui.Flags import com.android.systemui.media.controls.ui.view.MediaViewHolder import com.android.systemui.monet.ColorScheme import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect @@ -51,7 +52,7 @@ interface ColorTransition { open class AnimatingColorTransition( private val defaultColor: Int, private val extractColor: (ColorScheme) -> Int, - private val applyColor: (Int) -> Unit + private val applyColor: (Int) -> Unit, ) : AnimatorUpdateListener, ColorTransition { private val argbEvaluator = ArgbEvaluator() @@ -105,35 +106,52 @@ internal constructor( private val mediaViewHolder: MediaViewHolder, private val multiRippleController: MultiRippleController, private val turbulenceNoiseController: TurbulenceNoiseController, - animatingColorTransitionFactory: AnimatingColorTransitionFactory + animatingColorTransitionFactory: AnimatingColorTransitionFactory, ) { constructor( context: Context, mediaViewHolder: MediaViewHolder, multiRippleController: MultiRippleController, - turbulenceNoiseController: TurbulenceNoiseController + turbulenceNoiseController: TurbulenceNoiseController, ) : this( context, mediaViewHolder, multiRippleController, turbulenceNoiseController, - ::AnimatingColorTransition + ::AnimatingColorTransition, ) + var loadingEffect: LoadingEffect? = null - val bgColor = context.getColor(com.google.android.material.R.color.material_dynamic_neutral20) + val bgColor = + if (Flags.mediaControlsUiUpdate()) { + context.getColor(R.color.materialColorOnSurface) + } else { + context.getColor(com.google.android.material.R.color.material_dynamic_neutral20) + } + + val textColor = context.getColor(R.color.materialColorInverseOnSurface) + val buttonBgColor = context.getColor(R.color.materialColorPrimary) + val insideButtonColor = context.getColor(R.color.materialColorOnPrimary) + val surfaceColor = animatingColorTransitionFactory(bgColor, ::surfaceFromScheme) { surfaceColor -> val colorList = ColorStateList.valueOf(surfaceColor) - mediaViewHolder.seamlessIcon.imageTintList = colorList - mediaViewHolder.seamlessText.setTextColor(surfaceColor) mediaViewHolder.albumView.backgroundTintList = colorList mediaViewHolder.gutsViewHolder.setSurfaceColor(surfaceColor) + + if (Flags.mediaControlsUiUpdate()) return@animatingColorTransitionFactory + mediaViewHolder.seamlessIcon.imageTintList = colorList + mediaViewHolder.seamlessText.setTextColor(surfaceColor) } val accentPrimary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::accentPrimaryFromScheme + if (Flags.mediaControlsUiUpdate()) { + buttonBgColor + } else { + loadDefaultColor(R.attr.textColorPrimary) + }, + ::accentPrimaryFromScheme, ) { accentPrimary -> val accentColorList = ColorStateList.valueOf(accentPrimary) mediaViewHolder.actionPlayPause.backgroundTintList = accentColorList @@ -145,8 +163,12 @@ internal constructor( val accentSecondary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::accentSecondaryFromScheme + if (Flags.mediaControlsUiUpdate()) { + buttonBgColor + } else { + loadDefaultColor(R.attr.textColorPrimary) + }, + ::accentSecondaryFromScheme, ) { accentSecondary -> val colorList = ColorStateList.valueOf(accentSecondary) (mediaViewHolder.seamlessButton.background as? RippleDrawable)?.let { @@ -157,7 +179,11 @@ internal constructor( val colorSeamless = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), + if (Flags.mediaControlsUiUpdate()) { + buttonBgColor + } else { + loadDefaultColor(R.attr.textColorPrimary) + }, { colorScheme: ColorScheme -> // A1-100 dark in dark theme, A1-200 in light theme if ( @@ -170,13 +196,17 @@ internal constructor( { seamlessColor: Int -> val accentColorList = ColorStateList.valueOf(seamlessColor) mediaViewHolder.seamlessButton.backgroundTintList = accentColorList - } + }, ) val textPrimary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimary), - ::textPrimaryFromScheme + if (Flags.mediaControlsUiUpdate()) { + textColor + } else { + loadDefaultColor(R.attr.textColorPrimary) + }, + ::textPrimaryFromScheme, ) { textPrimary -> mediaViewHolder.titleText.setTextColor(textPrimary) val textColorList = ColorStateList.valueOf(textPrimary) @@ -192,25 +222,41 @@ internal constructor( val textPrimaryInverse = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorPrimaryInverse), - ::textPrimaryInverseFromScheme + if (Flags.mediaControlsUiUpdate()) { + insideButtonColor + } else { + loadDefaultColor(R.attr.textColorPrimaryInverse) + }, + ::textPrimaryInverseFromScheme, ) { textPrimaryInverse -> - mediaViewHolder.actionPlayPause.imageTintList = - ColorStateList.valueOf(textPrimaryInverse) + val colorList = ColorStateList.valueOf(textPrimaryInverse) + mediaViewHolder.actionPlayPause.imageTintList = colorList + + if (!Flags.mediaControlsUiUpdate()) return@animatingColorTransitionFactory + mediaViewHolder.seamlessIcon.imageTintList = colorList + mediaViewHolder.seamlessText.setTextColor(textPrimaryInverse) } val textSecondary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorSecondary), - ::textSecondaryFromScheme + if (Flags.mediaControlsUiUpdate()) { + textColor + } else { + loadDefaultColor(R.attr.textColorSecondary) + }, + ::textSecondaryFromScheme, ) { textSecondary -> mediaViewHolder.artistText.setTextColor(textSecondary) } val textTertiary = animatingColorTransitionFactory( - loadDefaultColor(R.attr.textColorTertiary), - ::textTertiaryFromScheme + if (Flags.mediaControlsUiUpdate()) { + textColor + } else { + loadDefaultColor(R.attr.textColorTertiary) + }, + ::textTertiaryFromScheme, ) { textTertiary -> mediaViewHolder.seekBar.progressBackgroundTintList = ColorStateList.valueOf(textTertiary) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt index 3928a711f840..a2ddc20844e7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt @@ -1016,9 +1016,24 @@ constructor( expandedLayout.load(context, R.xml.media_recommendations_expanded) } } + readjustPlayPauseWidth() refreshState() } + private fun readjustPlayPauseWidth() { + // TODO: move to xml file when flag is removed. + if (Flags.mediaControlsUiUpdate()) { + collapsedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + expandedLayout.constrainWidth( + R.id.actionPlayPause, + context.resources.getDimensionPixelSize(R.dimen.qs_media_action_play_pause_width), + ) + } + } + /** Get a view state based on the width and height set by the scene */ private fun obtainSceneContainerViewState(state: MediaHostState?): TransitionViewState? { logger.logMediaSize("scene container", widthInSceneContainerPx, heightInSceneContainerPx) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt index f60621882ac0..59990ea22c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import com.android.systemui.classifier.domain.interactor.FalsingInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel import dagger.assisted.AssistedFactory @@ -27,11 +28,12 @@ class EditModeButtonViewModel constructor( private val editModeViewModel: EditModeViewModel, private val falsingInteractor: FalsingInteractor, + private val activityStarter: ActivityStarter, ) { fun onButtonClick() { if (!falsingInteractor.isFalseTap(FalsingManager.LOW_PENALTY)) { - editModeViewModel.startEditing() + activityStarter.postQSRunnableDismissingKeyguard { editModeViewModel.startEditing() } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java index 74563fff8775..22b742f2f05b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java @@ -29,7 +29,6 @@ import android.widget.Button; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; -import com.android.systemui.Flags; import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker; import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager; import com.android.systemui.animation.Expandable; @@ -138,9 +137,4 @@ public class HearingDevicesTile extends QSTileImpl<BooleanState> { public CharSequence getTileLabel() { return mContext.getString(R.string.quick_settings_hearing_devices_label); } - - @Override - public boolean isAvailable() { - return Flags.hearingAidsQsTileDialog(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index ec8d30b01eab..e93cec875429 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -41,12 +41,14 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.plugins.qs.TileDetailsViewModel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel; import com.android.systemui.res.R; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.screenrecord.data.model.ScreenRecordModel; @@ -54,6 +56,8 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.function.Consumer; + import javax.inject.Inject; /** @@ -122,17 +126,78 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> @Override protected void handleClick(@Nullable Expandable expandable) { + handleClick(() -> showDialog(expandable)); + } + + private void showDialog(@Nullable Expandable expandable) { + final Dialog dialog = mController.createScreenRecordDialog( + this::onStartRecordingClicked); + + executeWhenUnlockedKeyguard(() -> { + // We animate from the touched view only if we are not on the keyguard, given that if we + // are we will dismiss it which will also collapse the shade. + boolean shouldAnimateFromExpandable = + expandable != null && !mKeyguardStateController.isShowing(); + + if (shouldAnimateFromExpandable) { + DialogTransitionAnimator.Controller controller = + expandable.dialogTransitionController(new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)); + if (controller != null) { + mDialogTransitionAnimator.show(dialog, + controller, /* animateBackgroundBoundsChange= */ true); + } else { + dialog.show(); + } + } else { + dialog.show(); + } + }); + } + + private void onStartRecordingClicked() { + // We dismiss the shade. Since starting the recording will also dismiss the dialog (if + // there is one showing), we disable the exit animation which looks weird when it happens + // at the same time as the shade collapsing. + mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); + mPanelInteractor.collapsePanels(); + } + + private void executeWhenUnlockedKeyguard(Runnable dismissActionCallback) { + ActivityStarter.OnDismissAction dismissAction = () -> { + dismissActionCallback.run(); + + int uid = mUserContextProvider.getUserContext().getUserId(); + mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); + + return false; + }; + + mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, + true /* afterKeyguardDone */); + } + + private void handleClick(Runnable showPromptCallback) { if (mController.isStarting()) { cancelCountdown(); } else if (mController.isRecording()) { stopRecording(); } else { - mUiHandler.post(() -> showPrompt(expandable)); + mUiHandler.post(showPromptCallback); } refreshState(); } @Override + public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { + handleClick(() -> + callback.accept(new ScreenRecordDetailsViewModel()) + ); + return true; + } + + @Override protected void handleUpdateState(BooleanState state, Object arg) { boolean isStarting = mController.isStarting(); boolean isRecording = mController.isRecording(); @@ -178,49 +243,6 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } - private void showPrompt(@Nullable Expandable expandable) { - // We animate from the touched view only if we are not on the keyguard, given that if we - // are we will dismiss it which will also collapse the shade. - boolean shouldAnimateFromExpandable = - expandable != null && !mKeyguardStateController.isShowing(); - - // Create the recording dialog that will collapse the shade only if we start the recording. - Runnable onStartRecordingClicked = () -> { - // We dismiss the shade. Since starting the recording will also dismiss the dialog, we - // disable the exit animation which looks weird when it happens at the same time as the - // shade collapsing. - mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); - mPanelInteractor.collapsePanels(); - }; - - final Dialog dialog = mController.createScreenRecordDialog(onStartRecordingClicked); - - ActivityStarter.OnDismissAction dismissAction = () -> { - if (shouldAnimateFromExpandable) { - DialogTransitionAnimator.Controller controller = - expandable.dialogTransitionController(new DialogCuj( - InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, - INTERACTION_JANK_TAG)); - if (controller != null) { - mDialogTransitionAnimator.show(dialog, - controller, /* animateBackgroundBoundsChange= */ true); - } else { - dialog.show(); - } - } else { - dialog.show(); - } - - int uid = mUserContextProvider.getUserContext().getUserId(); - mMediaProjectionMetricsLogger.notifyPermissionRequestDisplayed(uid); - - return false; - }; - - mKeyguardDismissUtil.executeWhenUnlocked(dismissAction, false /* requiresShadeOpen */, - true /* afterKeyguardDone */); - } - private void cancelCountdown() { Log.d(TAG, "Cancelling countdown"); mController.cancelCountdown(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt new file mode 100644 index 000000000000..42cb1248ccff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.dialog + +import android.view.LayoutInflater +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.plugins.qs.TileDetailsViewModel +import com.android.systemui.res.R + +/** The view model used for the screen record details view in the Quick Settings */ +class ScreenRecordDetailsViewModel() : TileDetailsViewModel() { + @Composable + override fun GetContentView() { + // TODO(b/378514312): Finish implementing this function. + AndroidView( + modifier = Modifier.fillMaxWidth().heightIn(max = VIEW_MAX_HEIGHT), + factory = { context -> + // Inflate with the existing dialog xml layout + LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null) + }, + ) + } + + override fun clickOnSettingsButton() { + // No settings button in this tile. + } + + override fun getTitle(): String { + // TODO(b/388321032): Replace this string with a string in a translatable xml file, + return "Screen recording" + } + + override fun getSubTitle(): String { + // No sub-title in this tile. + return "" + } + + companion object { + private val VIEW_MAX_HEIGHT: Dp = 320.dp + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractor.kt index ec0a4e9db896..33b7feb49b09 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileDataInteractor.kt @@ -17,7 +17,6 @@ package com.android.systemui.qs.tiles.impl.hearingdevices.domain.interactor import android.os.UserHandle -import com.android.systemui.Flags import com.android.systemui.accessibility.hearingaid.HearingDevicesChecker import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger @@ -62,8 +61,7 @@ constructor( .flowOn(backgroundContext) .distinctUntilChanged() - override fun availability(user: UserHandle): Flow<Boolean> = - flowOf(Flags.hearingAidsQsTileDialog()) + override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true) private fun getModel() = HearingDevicesTileModel( diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index c34edc81bfe7..30d1f05771d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -118,7 +118,8 @@ constructor( override fun addCallback(callback: QSTile.Callback?) { callback ?: return callbacks.add(callback) - state?.let(callback::onStateChanged) + state.copyTo(cachedState) + state.let(callback::onStateChanged) } override fun removeCallback(callback: QSTile.Callback?) { @@ -212,9 +213,9 @@ constructor( qsTileViewModel.destroy() } - override fun getState(): QSTile.State = + override fun getState(): QSTile.AdapterState = qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) } - ?: QSTile.State() + ?: QSTile.AdapterState() override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId @@ -241,7 +242,7 @@ constructor( context: Context, viewModelState: QSTileState, config: QSTileConfig, - ): QSTile.State = + ): QSTile.AdapterState = // we have to use QSTile.BooleanState to support different side icons // which are bound to instanceof QSTile.BooleanState in QSTileView. QSTile.AdapterState().apply { diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt index d60f05e685bb..0488962fd076 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -90,22 +90,15 @@ constructor( initialValue = defaultTransitionState, ) - fun changeScene( - toScene: SceneKey, - transitionKey: TransitionKey? = null, - ) { - dataSource.changeScene( - toScene = toScene, - transitionKey = transitionKey, - ) + /** Number of currently active transition animations. */ + val activeTransitionAnimationCount = MutableStateFlow(0) + + fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null) { + dataSource.changeScene(toScene = toScene, transitionKey = transitionKey) } - fun snapToScene( - toScene: SceneKey, - ) { - dataSource.snapToScene( - toScene = toScene, - ) + fun snapToScene(toScene: SceneKey) { + dataSource.snapToScene(toScene = toScene) } /** @@ -116,10 +109,7 @@ constructor( * [overlay] is already shown. */ fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.showOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.showOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -130,10 +120,7 @@ constructor( * if [overlay] is already hidden. */ fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.hideOverlay( - overlay = overlay, - transitionKey = transitionKey, - ) + dataSource.hideOverlay(overlay = overlay, transitionKey = transitionKey) } /** @@ -143,11 +130,7 @@ constructor( * This throws if [from] is not currently shown or if [to] is already shown. */ fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null) { - dataSource.replaceOverlay( - from = from, - to = to, - transitionKey = transitionKey, - ) + dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey) } /** Sets whether the container is visible. */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 0e6fc36fb96a..ba9dc7625769 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update /** * Generic business logic and app state accessors for the scene framework. @@ -165,12 +166,15 @@ constructor( /** Whether the scene container is visible. */ val isVisible: StateFlow<Boolean> = - combine(repository.isVisible, repository.isRemoteUserInputOngoing) { - isVisible, - isRemoteUserInteractionOngoing -> + combine( + repository.isVisible, + repository.isRemoteUserInputOngoing, + repository.activeTransitionAnimationCount, + ) { isVisible, isRemoteUserInteractionOngoing, activeTransitionAnimationCount -> isVisibleInternal( raw = isVisible, isRemoteUserInputOngoing = isRemoteUserInteractionOngoing, + activeTransitionAnimationCount = activeTransitionAnimationCount, ) } .stateIn( @@ -436,8 +440,9 @@ constructor( private fun isVisibleInternal( raw: Boolean = repository.isVisible.value, isRemoteUserInputOngoing: Boolean = repository.isRemoteUserInputOngoing.value, + activeTransitionAnimationCount: Int = repository.activeTransitionAnimationCount.value, ): Boolean { - return raw || isRemoteUserInputOngoing + return raw || isRemoteUserInputOngoing || activeTransitionAnimationCount > 0 } /** @@ -525,4 +530,50 @@ constructor( ): Flow<Map<UserAction, UserActionResult>> { return disabledContentInteractor.filteredUserActions(unfiltered) } + + /** + * Notifies that a transition animation has started. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationStart() { + repository.activeTransitionAnimationCount.update { current -> + (current + 1).also { + check(it < 10) { + "Number of active transition animations is too high. Something must be" + + " calling onTransitionAnimationStart too many times!" + } + } + } + } + + /** + * Notifies that a transition animation has ended. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationEnd() { + decrementActiveTransitionAnimationCount() + } + + /** + * Notifies that a transition animation has been canceled. + * + * The scene container will remain visible while any transition animation is running within it. + */ + fun onTransitionAnimationCancelled() { + decrementActiveTransitionAnimationCount() + } + + private fun decrementActiveTransitionAnimationCount() { + repository.activeTransitionAnimationCount.update { current -> + (current - 1).also { + check(it >= 0) { + "Number of active transition animations is negative. Something must be" + + " calling onTransitionAnimationEnd or onTransitionAnimationCancelled too" + + " many times!" + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 8d8c24eae9e2..3a23a71cf7bf 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -25,6 +25,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.AuthInteractionProperties import com.android.systemui.CoreStartable import com.android.systemui.Flags +import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -146,6 +147,7 @@ constructor( private val vibratorHelper: VibratorHelper, private val msdlPlayer: MSDLPlayer, private val disabledContentInteractor: DisabledContentInteractor, + private val activityTransitionAnimator: ActivityTransitionAnimator, ) : CoreStartable { private val centralSurfaces: CentralSurfaces? get() = centralSurfacesOptLazy.get().getOrNull() @@ -169,6 +171,7 @@ constructor( handleKeyguardEnabledness() notifyKeyguardDismissCancelledCallbacks() refreshLockscreenEnabled() + hydrateActivityTransitionAnimationState() } else { sceneLogger.logFrameworkEnabled( isEnabled = false, @@ -929,6 +932,35 @@ constructor( } } + /** + * Wires the scene framework to activity transition animations that originate from anywhere. A + * subset of these may actually originate from UI inside one of the scenes in the framework. + * + * Telling the scene framework about ongoing activity transition animations is critical so the + * scene framework doesn't make its scene container invisible during a transition. + * + * As it turns out, making the scene container view invisible during a transition animation + * disrupts the animation and causes interaction jank CUJ tracking to ignore reports of the CUJ + * ending or being canceled. + */ + private fun hydrateActivityTransitionAnimationState() { + activityTransitionAnimator.addListener( + object : ActivityTransitionAnimator.Listener { + override fun onTransitionAnimationStart() { + sceneInteractor.onTransitionAnimationStart() + } + + override fun onTransitionAnimationEnd() { + sceneInteractor.onTransitionAnimationEnd() + } + + override fun onTransitionAnimationCancelled() { + sceneInteractor.onTransitionAnimationCancelled() + } + } + ) + } + companion object { private const val TAG = "SceneContainerStartable" } diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index 42d83637ec1a..a48d4d4d3b5f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -316,7 +316,7 @@ internal constructor( val callback = it.callback.get() if (callback != null) { it.executor.execute { - traceSection({ "$callback" }) { action(callback) { latch.countDown() } } + traceSection({ "UserTrackerImpl::$callback" }) { action(callback) { latch.countDown() } } } } else { latch.countDown() diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 19152170757c..19bf4c0bab81 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -109,6 +109,7 @@ import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.binder.KeyguardLongPressViewBinder; +import com.android.systemui.keyguard.ui.transitions.BlurConfig; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; @@ -914,13 +915,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (!com.android.systemui.Flags.bouncerUiRevamp()) return; if (isBouncerShowing && isExpanded()) { - // Blur the shade much lesser than the background surface so that the surface is - // distinguishable from the background. - float shadeBlurEffect = mDepthController.getMaxBlurRadiusPx() / 3; + float shadeBlurEffect = BlurConfig.maxBlurRadiusToNotificationPanelBlurRadius( + mDepthController.getMaxBlurRadiusPx()); mView.setRenderEffect(RenderEffect.createBlurEffect( shadeBlurEffect, shadeBlurEffect, - Shader.TileMode.MIRROR)); + Shader.TileMode.CLAMP)); } else { mView.setRenderEffect(null); } @@ -3959,7 +3959,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } final boolean isTrackpadThreeFingerSwipe = isTrackpadThreeFingerSwipe(event); - if (com.android.systemui.Flags.disableShadeExpandsOnTrackpadTwoFingerSwipe() + if (com.android.systemui.Flags.disableShadeTrackpadTwoFingerSwipe() && !isTrackpadThreeFingerSwipe && isTwoFingerSwipeTrackpadEvent(event) && !isPanelExpanded()) { if (isDown) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 2bcd3fcfed17..10b726b90894 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -92,6 +92,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import javax.inject.Inject; @@ -297,6 +298,9 @@ public class NotificationLockscreenUserManagerImpl implements // The last lock time. Uses currentTimeMillis @VisibleForTesting protected final AtomicLong mLastLockTime = new AtomicLong(-1); + // Whether or not the device is locked + @VisibleForTesting + protected final AtomicBoolean mLocked = new AtomicBoolean(true); protected int mCurrentUserId = 0; @@ -369,6 +373,7 @@ public class NotificationLockscreenUserManagerImpl implements if (!unlocked) { mLastLockTime.set(System.currentTimeMillis()); } + mLocked.set(!unlocked); })); } } @@ -737,7 +742,7 @@ public class NotificationLockscreenUserManagerImpl implements return false; } - if (!mKeyguardManager.isDeviceLocked()) { + if (!mLocked.get()) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 75117936c090..38f7c39203f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -34,6 +34,7 @@ import androidx.dynamicanimation.animation.SpringForce import com.android.app.animation.Interpolators import com.android.app.tracing.coroutines.TrackTracer import com.android.systemui.Dumpable +import com.android.systemui.Flags.spatialModelAppPushback import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -52,7 +53,9 @@ import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.WallpaperController import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import com.android.systemui.window.flag.WindowBlurFlag +import com.android.wm.shell.appzoomout.AppZoomOut import java.io.PrintWriter +import java.util.Optional import javax.inject.Inject import kotlin.math.max import kotlin.math.sign @@ -79,6 +82,7 @@ constructor( private val splitShadeStateController: SplitShadeStateController, private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor, @Application private val applicationScope: CoroutineScope, + private val appZoomOutOptional: Optional<AppZoomOut>, dumpManager: DumpManager, configurationController: ConfigurationController, ) : ShadeExpansionListener, Dumpable { @@ -271,6 +275,13 @@ constructor( private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) { lastAppliedBlur = appliedBlurRadius wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius) + if (spatialModelAppPushback()) { + appZoomOutOptional.ifPresent { appZoomOut -> + appZoomOut.setProgress( + zoomOutFromShadeRadius + ) + } + } listeners.forEach { it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius) it.onBlurRadiusChanged(appliedBlurRadius) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 48cf7a83c324..155049f512d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Bundle; import android.util.AttributeSet; import android.util.IndentingPrintWriter; import android.util.MathUtils; @@ -30,6 +31,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; @@ -1014,12 +1016,24 @@ public class NotificationShelf extends ActivatableNotificationView { public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); if (mInteractive) { + // Add two accessibility actions that both performs expanding the notification shade info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); - AccessibilityNodeInfo.AccessibilityAction unlock - = new AccessibilityNodeInfo.AccessibilityAction( + + AccessibilityAction seeAll = new AccessibilityAction( AccessibilityNodeInfo.ACTION_CLICK, - getContext().getString(R.string.accessibility_overflow_action)); - info.addAction(unlock); + getContext().getString(R.string.accessibility_overflow_action) + ); + info.addAction(seeAll); + } + } + + @Override + public boolean performAccessibilityAction(int action, Bundle args) { + // override ACTION_EXPAND with ACTION_CLICK + if (action == AccessibilityNodeInfo.ACTION_EXPAND) { + return super.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, args); + } else { + return super.performAccessibilityAction(action, args); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt index de08e3891902..86954d569199 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel import android.view.View import com.android.internal.jank.InteractionJankMonitor -import com.android.systemui.Flags import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -64,18 +63,12 @@ constructor( is OngoingCallModel.InCallWithVisibleApp -> OngoingActivityChipModel.Hidden() is OngoingCallModel.InCall -> { val icon = - if ( - Flags.statusBarCallChipNotificationIcon() && - state.notificationIconView != null - ) { + if (state.notificationIconView != null) { StatusBarConnectedDisplays.assertInLegacyMode() OngoingActivityChipModel.ChipIcon.StatusBarView( state.notificationIconView ) - } else if ( - StatusBarConnectedDisplays.isEnabled && - Flags.statusBarCallChipNotificationIcon() - ) { + } else if (StatusBarConnectedDisplays.isEnabled) { OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon( state.notificationKey ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 2f6431b05c8b..ec3a5b271e35 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import javax.inject.Inject @@ -60,7 +61,7 @@ constructor( /** Converts the notification to the [OngoingActivityChipModel] object. */ private fun NotificationChipModel.toActivityChipModel( - headsUpState: PinnedStatus + headsUpState: TopPinnedState ): OngoingActivityChipModel.Shown { StatusBarNotifChips.assertInNewMode() val icon = @@ -87,8 +88,12 @@ constructor( } } - if (headsUpState == PinnedStatus.PinnedByUser) { - // If the user tapped the chip to show the HUN, we want to just show the icon because + val isShowingHeadsUpFromChipTap = + headsUpState is TopPinnedState.Pinned && + headsUpState.status == PinnedStatus.PinnedByUser && + headsUpState.key == this.key + if (isShowingHeadsUpFromChipTap) { + // If the user tapped this chip to show the HUN, we want to just show the icon because // the HUN will show the rest of the information. return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt index 69ef09d8bf5e..b0fa9d842480 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipBinder.kt @@ -25,6 +25,7 @@ import android.widget.DateTimeView import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.UiThread import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView @@ -38,24 +39,24 @@ import com.android.systemui.statusbar.notification.icon.ui.viewbinder.Notificati /** Binder for ongoing activity chip views. */ object OngoingActivityChipBinder { /** Binds the given [chipModel] data to the given [chipView]. */ - fun bind(chipModel: OngoingActivityChipModel, chipView: View, iconViewStore: IconViewStore?) { - val chipContext = chipView.context - val chipDefaultIconView: ImageView = - chipView.requireViewById(R.id.ongoing_activity_chip_icon) - val chipTimeView: ChipChronometer = - chipView.requireViewById(R.id.ongoing_activity_chip_time) - val chipTextView: TextView = chipView.requireViewById(R.id.ongoing_activity_chip_text) - val chipShortTimeDeltaView: DateTimeView = - chipView.requireViewById(R.id.ongoing_activity_chip_short_time_delta) - val chipBackgroundView: ChipBackgroundContainer = - chipView.requireViewById(R.id.ongoing_activity_chip_background) + fun bind( + chipModel: OngoingActivityChipModel, + viewBinding: OngoingActivityChipViewBinding, + iconViewStore: IconViewStore?, + ) { + val chipContext = viewBinding.rootView.context + val chipDefaultIconView = viewBinding.defaultIconView + val chipTimeView = viewBinding.timeView + val chipTextView = viewBinding.textView + val chipShortTimeDeltaView = viewBinding.shortTimeDeltaView + val chipBackgroundView = viewBinding.backgroundView when (chipModel) { is OngoingActivityChipModel.Shown -> { // Data setChipIcon(chipModel, chipBackgroundView, chipDefaultIconView, iconViewStore) setChipMainContent(chipModel, chipTextView, chipTimeView, chipShortTimeDeltaView) - chipView.setOnClickListener(chipModel.onClickListener) + viewBinding.rootView.setOnClickListener(chipModel.onClickListener) updateChipPadding( chipModel, chipBackgroundView, @@ -65,7 +66,7 @@ object OngoingActivityChipBinder { ) // Accessibility - setChipAccessibility(chipModel, chipView, chipBackgroundView) + setChipAccessibility(chipModel, viewBinding.rootView, chipBackgroundView) // Colors val textColor = chipModel.colors.text(chipContext) @@ -83,6 +84,85 @@ object OngoingActivityChipBinder { } } + /** Stores [rootView] and relevant child views in an object for easy reference. */ + fun createBinding(rootView: View): OngoingActivityChipViewBinding { + return OngoingActivityChipViewBinding( + rootView = rootView, + timeView = rootView.requireViewById(R.id.ongoing_activity_chip_time), + textView = rootView.requireViewById(R.id.ongoing_activity_chip_text), + shortTimeDeltaView = + rootView.requireViewById(R.id.ongoing_activity_chip_short_time_delta), + defaultIconView = rootView.requireViewById(R.id.ongoing_activity_chip_icon), + backgroundView = rootView.requireViewById(R.id.ongoing_activity_chip_background), + ) + } + + /** + * Resets any width restrictions that were placed on the primary chip's contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetPrimaryChipWidthRestrictions( + primaryChipViewBinding: OngoingActivityChipViewBinding, + currentPrimaryChipViewModel: OngoingActivityChipModel, + ) { + if (currentPrimaryChipViewModel is OngoingActivityChipModel.Hidden) { + return + } + resetChipMainContentWidthRestrictions( + primaryChipViewBinding, + currentPrimaryChipViewModel as OngoingActivityChipModel.Shown, + ) + } + + /** + * Resets any width restrictions that were placed on the secondary chip and its contents. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + fun resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding: OngoingActivityChipViewBinding, + currentSecondaryChipModel: OngoingActivityChipModel, + ) { + if (currentSecondaryChipModel is OngoingActivityChipModel.Hidden) { + return + } + secondaryChipViewBinding.rootView.resetWidthRestriction() + resetChipMainContentWidthRestrictions( + secondaryChipViewBinding, + currentSecondaryChipModel as OngoingActivityChipModel.Shown, + ) + } + + private fun resetChipMainContentWidthRestrictions( + viewBinding: OngoingActivityChipViewBinding, + model: OngoingActivityChipModel.Shown, + ) { + when (model) { + is OngoingActivityChipModel.Shown.Text -> viewBinding.textView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.Timer -> viewBinding.timeView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.ShortTimeDelta -> + viewBinding.shortTimeDeltaView.resetWidthRestriction() + is OngoingActivityChipModel.Shown.IconOnly, + is OngoingActivityChipModel.Shown.Countdown -> {} + } + } + + /** + * Resets any width restrictions that were placed on the given view. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun View.resetWidthRestriction() { + // View needs to be visible in order to be re-measured + visibility = View.VISIBLE + forceLayout() + } + private fun setChipIcon( chipModel: OngoingActivityChipModel.Shown, backgroundView: ChipBackgroundContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt new file mode 100644 index 000000000000..1814b7430330 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/binder/OngoingActivityChipViewBinding.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 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.chips.ui.binder + +import android.view.View +import android.widget.ImageView +import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer +import com.android.systemui.statusbar.chips.ui.view.ChipChronometer +import com.android.systemui.statusbar.chips.ui.view.ChipDateTimeView +import com.android.systemui.statusbar.chips.ui.view.ChipTextView + +/** Stores bound views for a given chip. */ +data class OngoingActivityChipViewBinding( + val rootView: View, + val timeView: ChipChronometer, + val textView: ChipTextView, + val shortTimeDeltaView: ChipDateTimeView, + val defaultIconView: ImageView, + val backgroundView: ChipBackgroundContainer, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt new file mode 100644 index 000000000000..1be5842bceeb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -0,0 +1,208 @@ +/* + * 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.chips.ui.compose + +import android.content.res.ColorStateList +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.modifiers.neverDecreaseWidth +import com.android.systemui.statusbar.chips.ui.model.ColorsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + val isClickable = model.onClickListener != null + val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView + + // Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible + // height of the chip is determined by the height of the background of the Row below. + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .fillMaxHeight() + .clickable( + enabled = isClickable, + onClick = { + // TODO(b/372657935): Implement click actions. + }, + ), + ) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.clip( + RoundedCornerShape( + dimensionResource(id = R.dimen.ongoing_activity_chip_corner_radius) + ) + ) + .height(dimensionResource(R.dimen.ongoing_appops_chip_height)) + .widthIn( + min = + if (isClickable) { + dimensionResource(id = R.dimen.min_clickable_item_size) + } else { + 0.dp + } + ) + .background(Color(model.colors.background(context).defaultColor)) + .padding( + horizontal = + if (hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding) + } + ), + ) { + model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) } + + val isIconOnly = model is OngoingActivityChipModel.Shown.IconOnly + val isTextOnly = model.icon == null + if (!isIconOnly) { + ChipContent( + viewModel = model, + modifier = + Modifier.padding( + start = + if (isTextOnly || hasEmbeddedIcon) { + 0.dp + } else { + dimensionResource( + id = R.dimen.ongoing_activity_chip_icon_text_padding + ) + }, + end = + if (hasEmbeddedIcon) { + dimensionResource( + id = + R.dimen + .ongoing_activity_chip_text_end_padding_for_embedded_padding_icon + ) + } else { + 0.dp + }, + ), + ) + } + } + } +} + +@Composable +private fun ChipIcon( + viewModel: OngoingActivityChipModel.ChipIcon, + colors: ColorsModel, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + when (viewModel) { + is OngoingActivityChipModel.ChipIcon.StatusBarView -> { + val originalIcon = viewModel.impl + val iconSizePx = + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_embedded_padding_icon_size + ) + AndroidView( + modifier = modifier, + factory = { _ -> + originalIcon.apply { + layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) + imageTintList = ColorStateList.valueOf(colors.text(context)) + } + }, + ) + } + + is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { + Icon( + icon = viewModel.impl, + tint = Color(colors.text(context)), + modifier = + modifier.size(dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size)), + ) + } + + // TODO(b/372657935): Add recommended architecture implementation for + // StatusBarNotificationIcons + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> {} + } +} + +@Composable +private fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) { + val context = LocalContext.current + when (viewModel) { + is OngoingActivityChipModel.Shown.Timer -> { + ChronometerText( + startTimeMillis = viewModel.startTimeMs, + style = MaterialTheme.typography.labelLarge, + color = Color(viewModel.colors.text(context)), + modifier = modifier, + ) + } + + is OngoingActivityChipModel.Shown.Countdown -> { + ChipText( + text = viewModel.secondsUntilStarted.toString(), + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier.neverDecreaseWidth(), + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.Text -> { + ChipText( + text = viewModel.text, + color = Color(viewModel.colors.text(context)), + style = MaterialTheme.typography.labelLarge, + modifier = modifier, + backgroundColor = Color(viewModel.colors.background(context).defaultColor), + ) + } + + is OngoingActivityChipModel.Shown.ShortTimeDelta -> { + // TODO(b/372657935): Implement ShortTimeDelta content in compose. + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt new file mode 100644 index 000000000000..85ea087f531b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel +import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel + +@Composable +fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) { + Row( + // TODO(b/372657935): Remove magic numbers for padding and spacing. + modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal + // space. + if (chips.primary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.primary) + } + if (chips.secondary is OngoingActivityChipModel.Shown) { + OngoingActivityChip(model = chips.secondary) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt index c81e8e211507..956d99e46766 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.chips.ui.model import android.view.View -import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -130,10 +129,6 @@ sealed class OngoingActivityChipModel { */ data class StatusBarView(val impl: StatusBarIconView) : ChipIcon { init { - check(Flags.statusBarCallChipNotificationIcon()) { - "OngoingActivityChipModel.ChipIcon.StatusBarView created even though " + - "Flags.statusBarCallChipNotificationIcon is not enabled" - } StatusBarConnectedDisplays.assertInLegacyMode() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt index ff3061e850d9..7b4b79d7c852 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/view/ChipChronometer.kt @@ -33,10 +33,8 @@ import androidx.annotation.UiThread * that wide. This means the chip may get larger over time (e.g. in the transition from 59:59 to * 1:00:00), but never smaller. * 2) Hiding the text if the time gets too long for the space available. Once the text has been - * hidden, it remains hidden for the duration of the activity. - * - * Note that if the text was too big in portrait mode, resulting in the text being hidden, then the - * text will also be hidden in landscape (even if there is enough space for it in landscape). + * hidden, it remains hidden for the duration of the activity (or until [resetWidthRestriction] + * is called). */ class ChipChronometer @JvmOverloads @@ -51,12 +49,23 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : private var shouldHideText: Boolean = false override fun setBase(base: Long) { - // These variables may have changed during the previous activity, so re-set them before the - // new activity starts. + resetWidthRestriction() + super.setBase(base) + } + + /** + * Resets any width restrictions that were placed on the chronometer. + * + * Should be used when the user's screen bounds changed because there may now be more room in + * the status bar to show additional content. + */ + @UiThread + fun resetWidthRestriction() { minimumTextWidth = 0 shouldHideText = false + // View needs to be visible in order to be re-measured visibility = VISIBLE - super.setBase(base) + forceLayout() } /** Sets whether this view should hide its text or not. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt index 057213fa4b18..3c30f3cbec85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarRootModernization.kt @@ -25,6 +25,9 @@ object StatusBarRootModernization { /** Aconfig flag for removing the fragment */ const val FLAG_NAME = Flags.FLAG_STATUS_BAR_ROOT_MODERNIZATION + /** Shows a "compose->bar" text in the status bar for debug purposes */ + const val SHOW_DISAMBIGUATION = false + /** A token used for dependency declaration */ val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index eff959d0f83b..351cdc8e7f36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.data.repository.LightBarControllerStore import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider import com.android.systemui.statusbar.layout.StatusBarContentInsetsProviderImpl +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModel import com.android.systemui.statusbar.phone.AutoHideController import com.android.systemui.statusbar.phone.AutoHideControllerImpl import com.android.systemui.statusbar.phone.LightBarController @@ -39,6 +40,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.ui.StatusBarUiLayerModule import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore @@ -60,7 +62,14 @@ import dagger.multibindings.IntoMap * ([com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule], * [com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule], etc.). */ -@Module(includes = [StatusBarDataLayerModule::class, SystemBarUtilsProxyImpl.Module::class]) +@Module( + includes = + [ + StatusBarDataLayerModule::class, + StatusBarUiLayerModule::class, + SystemBarUtilsProxyImpl.Module::class, + ] +) interface StatusBarModule { @Binds @@ -169,5 +178,13 @@ interface StatusBarModule { ): StatusBarContentInsetsProvider { return factory.create(context, configurationController, sysUICutoutProvider) } + + @Provides + @SysUISingleton + fun contentInsetsViewModel( + insetsProvider: StatusBarContentInsetsProvider + ): StatusBarContentInsetsViewModel { + return StatusBarContentInsetsViewModel(insetsProvider) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt new file mode 100644 index 000000000000..03c07480ecea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import android.graphics.Rect +import com.android.systemui.statusbar.layout.StatusBarContentInsetsChangedListener +import com.android.systemui.statusbar.layout.StatusBarContentInsetsProvider +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart + +/** A recommended architecture version of [StatusBarContentInsetsProvider]. */ +class StatusBarContentInsetsViewModel( + private val statusBarContentInsetsProvider: StatusBarContentInsetsProvider +) { + /** Emits the status bar content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> = + conflatedCallbackFlow { + val listener = + object : StatusBarContentInsetsChangedListener { + override fun onStatusBarContentInsetsChanged() { + trySend( + statusBarContentInsetsProvider + .getStatusBarContentAreaForCurrentRotation() + ) + } + } + statusBarContentInsetsProvider.addCallback(listener) + awaitClose { statusBarContentInsetsProvider.removeCallback(listener) } + } + .onStart { + emit(statusBarContentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) + } + .distinctUntilChanged() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt new file mode 100644 index 000000000000..d2dccc49ffd7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelStore.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.PerDisplayStore +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.display.data.repository.SingleDisplayStore +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import dagger.Lazy +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope + +/** Provides per-display instances of [StatusBarContentInsetsViewModel]. */ +interface StatusBarContentInsetsViewModelStore : PerDisplayStore<StatusBarContentInsetsViewModel> + +@SysUISingleton +class MultiDisplayStatusBarContentInsetsViewModelStore +@Inject +constructor( + @Background backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, + private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore, +) : + StatusBarContentInsetsViewModelStore, + PerDisplayStoreImpl<StatusBarContentInsetsViewModel>( + backgroundApplicationScope, + displayRepository, + ) { + + override fun createInstanceForDisplay(displayId: Int): StatusBarContentInsetsViewModel? { + val insetsProvider = + statusBarContentInsetsProviderStore.forDisplay(displayId) ?: return null + return StatusBarContentInsetsViewModel(insetsProvider) + } + + override val instanceClass = StatusBarContentInsetsViewModel::class.java +} + +@SysUISingleton +class SingleDisplayStatusBarContentInsetsViewModelStore +@Inject +constructor(statusBarContentInsetsViewModel: StatusBarContentInsetsViewModel) : + StatusBarContentInsetsViewModelStore, + PerDisplayStore<StatusBarContentInsetsViewModel> by SingleDisplayStore( + defaultInstance = statusBarContentInsetsViewModel + ) + +@Module +object StatusBarContentInsetsViewModelStoreModule { + @Provides + @SysUISingleton + @IntoMap + @ClassKey(StatusBarContentInsetsViewModelStore::class) + fun storeAsCoreStartable( + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore> + ): CoreStartable { + return if (StatusBarConnectedDisplays.isEnabled) { + return multiDisplayLazy.get() + } else { + CoreStartable.NOP + } + } + + @Provides + @SysUISingleton + fun store( + singleDisplayLazy: Lazy<SingleDisplayStatusBarContentInsetsViewModelStore>, + multiDisplayLazy: Lazy<MultiDisplayStatusBarContentInsetsViewModelStore>, + ): StatusBarContentInsetsViewModelStore { + return if (StatusBarConnectedDisplays.isEnabled) { + multiDisplayLazy.get() + } else { + singleDisplayLazy.get() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt index 90212ed5b5f7..034a4fd2af72 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinator.kt @@ -36,7 +36,7 @@ class DataStoreCoordinator internal constructor(private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl) : CoreCoordinator { override fun attach(pipeline: NotifPipeline) { - pipeline.addOnAfterRenderListListener { entries, _ -> onAfterRenderList(entries) } + pipeline.addOnAfterRenderListListener { entries -> onAfterRenderList(entries) } } override fun dumpPipeline(d: PipelineDumper) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index c7535ec14d5d..eb5a3703bcfb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -112,20 +112,20 @@ constructor( if (StatusBarNotifChips.isEnabled) { applicationScope.launch { statusBarNotificationChipsInteractor.promotedNotificationChipTapEvent.collect { - showPromotedNotificationHeadsUp(it) + onPromotedNotificationChipTapEvent(it) } } } } /** - * Shows the promoted notification with the given [key] as heads-up. + * Updates the heads-up state based on which promoted notification with the given [key] was + * tapped. * * Must be run on the main thread. */ - private fun showPromotedNotificationHeadsUp(key: String) { + private fun onPromotedNotificationChipTapEvent(key: String) { StatusBarNotifChips.assertInNewMode() - mLogger.logShowPromotedNotificationHeadsUp(key) val entry = notifCollection.getEntry(key) if (entry == null) { @@ -135,22 +135,29 @@ constructor( // TODO(b/364653005): Validate that the given key indeed matches a promoted notification, // not just any notification. + val isCurrentlyHeadsUp = mHeadsUpManager.isHeadsUpEntry(entry.key) val posted = PostedEntry( entry, wasAdded = false, wasUpdated = false, - // Force-set this notification to show heads-up. - shouldHeadsUpEver = true, - shouldHeadsUpAgain = true, + // We want the chip to act as a toggle, so if the chip's notification is currently + // showing as heads up, then we should stop showing it. + shouldHeadsUpEver = !isCurrentlyHeadsUp, + shouldHeadsUpAgain = !isCurrentlyHeadsUp, isPinnedByUser = true, - isHeadsUpEntry = mHeadsUpManager.isHeadsUpEntry(entry.key), + isHeadsUpEntry = isCurrentlyHeadsUp, isBinding = isEntryBinding(entry), ) + if (isCurrentlyHeadsUp) { + mLogger.logHidePromotedNotificationHeadsUp(key) + } else { + mLogger.logShowPromotedNotificationHeadsUp(key) + } mExecutor.execute { mPostedEntries[entry.key] = posted - mNotifPromoter.invalidateList("showPromotedNotificationHeadsUp: ${entry.logKey}") + mNotifPromoter.invalidateList("onPromotedNotificationChipTapEvent: ${entry.logKey}") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt index e443a0418ffd..5141aa35b041 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt @@ -148,6 +148,15 @@ class HeadsUpCoordinatorLogger(private val buffer: LogBuffer, private val verbos ) } + fun logHidePromotedNotificationHeadsUp(key: String) { + buffer.log( + TAG, + LogLevel.DEBUG, + { str1 = key }, + { "requesting promoted entry to hide heads up: $str1" }, + ) + } + fun logPromotedNotificationForHeadsUpNotFound(key: String) { buffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java index 1d37dcf13037..69b069d792e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -74,7 +74,12 @@ public class RankingCoordinator implements Coordinator { mStatusBarStateController.addCallback(mStatusBarStateCallback); pipeline.addPreGroupFilter(mSuspendedFilter); - pipeline.addPreGroupFilter(mDndVisualEffectsFilter); + if (com.android.systemui.Flags.notificationAmbientSuppressionAfterInflation()) { + pipeline.addPreGroupFilter(mDndPreGroupFilter); + pipeline.addFinalizeFilter(mDndVisualEffectsFilter); + } else { + pipeline.addPreGroupFilter(mDndVisualEffectsFilter); + } } public NotifSectioner getAlertingSectioner() { @@ -191,6 +196,16 @@ public class RankingCoordinator implements Coordinator { } }; + private final NotifFilter mDndPreGroupFilter = new NotifFilter("DndPreGroupFilter") { + @Override + public boolean shouldFilterOut(NotificationEntry entry, long now) { + // Entries with both flags set should be suppressed ASAP regardless of dozing state. + // As a result of being doze-independent, they can also be suppressed early in the + // pipeline. + return entry.shouldSuppressNotificationList() && entry.shouldSuppressAmbient(); + } + }; + private final StatusBarStateController.StateListener mStatusBarStateCallback = new StatusBarStateController.StateListener() { private boolean mPrevDozeAmountIsOne = false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index d4d3cdf42fb1..1cb2366a16fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -23,8 +23,7 @@ import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl -import com.android.systemui.statusbar.notification.collection.render.NotifStackController -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT @@ -51,8 +50,7 @@ internal constructor( groupExpansionManagerImpl.attach(pipeline) } - // TODO: b/293167744 - Remove controller param. - private fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = + private fun onAfterRenderList(entries: List<ListEntry>) = traceSection("StackCoordinator.onAfterRenderList") { val notifStats = calculateNotifStats(entries) activeNotificationsInteractor.setNotifStats(notifStats) @@ -84,7 +82,6 @@ internal constructor( } } return NotifStats( - numActiveNotifs = entries.size, hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs, hasClearableAlertingNotifs = hasClearableAlertingNotifs, hasNonClearableSilentNotifs = hasNonClearableSilentNotifs, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java index a34d033afcaa..c58b3febe54b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/init/NotifPipelineInitializer.java @@ -33,7 +33,6 @@ import com.android.systemui.statusbar.notification.collection.ShadeListBuilder; import com.android.systemui.statusbar.notification.collection.coalescer.GroupCoalescer; import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators; import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import com.android.systemui.statusbar.notification.collection.render.RenderStageManager; import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager; import com.android.systemui.statusbar.notification.collection.render.ShadeViewManagerFactory; @@ -89,8 +88,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable { public void initialize( NotificationListener notificationService, NotificationRowBinderImpl rowBinder, - NotificationListContainer listContainer, - NotifStackController stackController) { + NotificationListContainer listContainer) { mDumpManager.registerDumpable("NotifPipeline", this); mNotificationService = notificationService; @@ -102,7 +100,7 @@ public class NotifPipelineInitializer implements Dumpable, PipelineDumpable { mNotifPluggableCoordinators.attach(mPipelineWrapper); // Wire up pipeline - mShadeViewManager = mShadeViewManagerFactory.create(listContainer, stackController); + mShadeViewManager = mShadeViewManagerFactory.create(listContainer); mShadeViewManager.attach(mRenderStageManager); mRenderStageManager.attach(mListBuilder); mListBuilder.attach(mNotifCollection); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java index b5a0f7ae169d..ac450c03b850 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/OnAfterRenderListListener.java @@ -20,7 +20,6 @@ import androidx.annotation.NonNull; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import java.util.List; @@ -31,9 +30,6 @@ public interface OnAfterRenderListListener { * * @param entries The current list of top-level entries. Note that this is a live view into the * current list and will change whenever the pipeline is rerun. - * @param controller An object for setting state on the shade. */ - void onAfterRenderList( - @NonNull List<ListEntry> entries, - @NonNull NotifStackController controller); + void onAfterRenderList(@NonNull List<ListEntry> entries); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt deleted file mode 100644 index a37937a6c495..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt +++ /dev/null @@ -1,49 +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.systemui.statusbar.notification.collection.render - -import javax.inject.Inject - -/** An interface by which the pipeline can make updates to the notification root view. */ -interface NotifStackController { - /** Provides stats about the list of notifications attached to the shade */ - fun setNotifStats(stats: NotifStats) -} - -/** Data provided to the NotificationRootController whenever the pipeline runs */ -data class NotifStats( - // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag. - val numActiveNotifs: Int, - val hasNonClearableAlertingNotifs: Boolean, - val hasClearableAlertingNotifs: Boolean, - val hasNonClearableSilentNotifs: Boolean, - val hasClearableSilentNotifs: Boolean -) { - companion object { - @JvmStatic val empty = NotifStats(0, false, false, false, false) - } -} - -/** - * An implementation of NotifStackController which provides default, no-op implementations of each - * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods, - * rather than forcing us to add no-op implementations in their implementation every time a method - * is added. - */ -open class DefaultNotifStackController @Inject constructor() : NotifStackController { - override fun setNotifStats(stats: NotifStats) {} -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt index 410b78b9d3bf..8284022c7270 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifViewRenderer.kt @@ -37,12 +37,6 @@ interface NotifViewRenderer { fun onRenderList(notifList: List<ListEntry>) /** - * Provides an interface for the pipeline to update the overall shade. This will be called at - * most once for each time [onRenderList] is called. - */ - fun getStackController(): NotifStackController - - /** * Provides an interface for the pipeline to update individual groups. This will be called at * most once for each group in the most recent call to [onRenderList]. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt index 9d3b098fa966..21e68376031c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RenderStageManager.kt @@ -50,7 +50,7 @@ class RenderStageManager @Inject constructor() : PipelineDumpable { traceSection("RenderStageManager.onRenderList") { val viewRenderer = viewRenderer ?: return viewRenderer.onRenderList(notifList) - dispatchOnAfterRenderList(viewRenderer, notifList) + dispatchOnAfterRenderList(notifList) dispatchOnAfterRenderGroups(viewRenderer, notifList) dispatchOnAfterRenderEntries(viewRenderer, notifList) viewRenderer.onDispatchComplete() @@ -85,15 +85,9 @@ class RenderStageManager @Inject constructor() : PipelineDumpable { dump("onAfterRenderEntryListeners", onAfterRenderEntryListeners) } - private fun dispatchOnAfterRenderList( - viewRenderer: NotifViewRenderer, - entries: List<ListEntry>, - ) { + private fun dispatchOnAfterRenderList(entries: List<ListEntry>) { traceSection("RenderStageManager.dispatchOnAfterRenderList") { - val stackController = viewRenderer.getStackController() - onAfterRenderListListeners.forEach { listener -> - listener.onAfterRenderList(entries, stackController) - } + onAfterRenderListListeners.forEach { listener -> listener.onAfterRenderList(entries) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt index 3c838e5b707e..72316bf14c9a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt @@ -41,7 +41,6 @@ class ShadeViewManager constructor( @ShadeDisplayAware context: Context, @Assisted listContainer: NotificationListContainer, - @Assisted private val stackController: NotifStackController, mediaContainerController: MediaContainerController, featureManager: NotificationSectionsFeatureManager, sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider, @@ -83,8 +82,6 @@ constructor( } } - override fun getStackController(): NotifStackController = stackController - override fun getGroupController(group: GroupEntry): NotifGroupController = viewBarn.requireGroupController(group.requireSummary) @@ -95,8 +92,5 @@ constructor( @AssistedFactory interface ShadeViewManagerFactory { - fun create( - listContainer: NotificationListContainer, - stackController: NotifStackController, - ): ShadeViewManager + fun create(listContainer: NotificationListContainer): ShadeViewManager } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.kt new file mode 100644 index 000000000000..d7fd7025a94f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/model/NotifStats.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.notification.data.model + +/** Information about the current list of notifications. */ +data class NotifStats( + val hasNonClearableAlertingNotifs: Boolean, + val hasClearableAlertingNotifs: Boolean, + val hasNonClearableSilentNotifs: Boolean, + val hasClearableSilentNotifs: Boolean, +) { + companion object { + @JvmStatic + val empty = + NotifStats( + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt index 2b9e49372a63..70f06ebe8468 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index 6b93ee1c435e..0c040c855368 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.notification.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 75c7d2d5be98..6140c92369b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import com.android.systemui.statusbar.notification.domain.model.TopPinnedState import com.android.systemui.statusbar.notification.headsup.PinnedStatus import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import javax.inject.Inject @@ -98,21 +99,39 @@ constructor( } } - /** What [PinnedStatus] does the top row have? */ - private val topPinnedStatus: Flow<PinnedStatus> = + /** What [PinnedStatus] and key does the top row have? */ + private val topPinnedState: Flow<TopPinnedState> = headsUpRepository.activeHeadsUpRows.flatMapLatest { rows -> if (rows.isNotEmpty()) { - combine(rows.map { it.pinnedStatus }) { pinnedStatus -> - pinnedStatus.firstOrNull { it.isPinned } ?: PinnedStatus.NotPinned + // For each row, emits a (key, pinnedStatus) pair each time any row's + // `pinnedStatus` changes + val toCombine: List<Flow<Pair<String, PinnedStatus>>> = + rows.map { row -> row.pinnedStatus.map { status -> row.key to status } } + combine(toCombine) { pairs -> + val topPinnedRow: Pair<String, PinnedStatus>? = + pairs.firstOrNull { it.second.isPinned } + if (topPinnedRow != null) { + TopPinnedState.Pinned( + key = topPinnedRow.first, + status = topPinnedRow.second, + ) + } else { + TopPinnedState.NothingPinned + } } } else { - // if the set is empty, there are no flows to combine - flowOf(PinnedStatus.NotPinned) + flowOf(TopPinnedState.NothingPinned) } } /** Are there any pinned heads up rows to display? */ - val hasPinnedRows: Flow<Boolean> = topPinnedStatus.map { it.isPinned } + val hasPinnedRows: Flow<Boolean> = + topPinnedState.map { + when (it) { + is TopPinnedState.Pinned -> it.status.isPinned + is TopPinnedState.NothingPinned -> false + } + } val isHeadsUpOrAnimatingAway: Flow<Boolean> by lazy { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) { @@ -142,13 +161,25 @@ constructor( } } - /** Emits the pinned notification state as it relates to the status bar. */ - val statusBarHeadsUpState: Flow<PinnedStatus> = - combine(topPinnedStatus, canShowHeadsUp) { topPinnedStatus, canShowHeadsUp -> + /** + * Emits the pinned notification state as it relates to the status bar. Includes both the pinned + * status and key of the notification that's pinned (if there is a pinned notification). + */ + val statusBarHeadsUpState: Flow<TopPinnedState> = + combine(topPinnedState, canShowHeadsUp) { topPinnedState, canShowHeadsUp -> if (canShowHeadsUp) { - topPinnedStatus + topPinnedState } else { - PinnedStatus.NotPinned + TopPinnedState.NothingPinned + } + } + + /** Emits the pinned notification status as it relates to the status bar. */ + val statusBarHeadsUpStatus: Flow<PinnedStatus> = + statusBarHeadsUpState.map { + when (it) { + is TopPinnedState.Pinned -> it.status + is TopPinnedState.NothingPinned -> PinnedStatus.NotPinned } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 042389f7fde7..fd5973e0ab3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -25,7 +25,6 @@ import android.graphics.drawable.Icon import android.service.notification.StatusBarNotification import android.util.ArrayMap import com.android.app.tracing.traceSection -import com.android.systemui.Flags import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry @@ -132,12 +131,6 @@ private class ActiveNotificationsStoreBuilder( } private fun NotificationEntry.toModel(): ActiveNotificationModel { - val statusBarChipIcon = - if (Flags.statusBarCallChipNotificationIcon()) { - icons.statusBarChipIcon - } else { - null - } val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { promotedNotificationContentModel @@ -158,7 +151,7 @@ private class ActiveNotificationsStoreBuilder( aodIcon = icons.aodIcon?.sourceIcon, shelfIcon = icons.shelfIcon?.sourceIcon, statusBarIcon = icons.statusBarIcon?.sourceIcon, - statusBarChipIconView = statusBarChipIcon, + statusBarChipIconView = icons.statusBarChipIcon, uid = sbn.uid, packageName = sbn.packageName, contentIntent = sbn.notification.contentIntent, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt new file mode 100644 index 000000000000..51c448adf998 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/model/TopPinnedState.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2025 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.domain.model + +import com.android.systemui.statusbar.notification.headsup.PinnedStatus + +/** A class representing the state of the top pinned row. */ +sealed interface TopPinnedState { + /** Nothing is pinned. */ + data object NothingPinned : TopPinnedState + + /** + * The top pinned row is a notification with the given key and status. + * + * @property status must have [PinnedStatus.isPinned] as true. + */ + data class Pinned(val key: String, val status: PinnedStatus) : TopPinnedState { + init { + check(status.isPinned) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index b56a838a80a5..31375cc4a03a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -162,9 +162,7 @@ constructor( val sbIcon = iconBuilder.createIconView(entry) sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE val sbChipIcon: StatusBarIconView? - if ( - Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled - ) { + if (!StatusBarConnectedDisplays.isEnabled) { sbChipIcon = iconBuilder.createIconView(entry) sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE } else { @@ -186,7 +184,7 @@ constructor( try { setIcon(entry, normalIconDescriptor, sbIcon) - if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) { + if (sbChipIcon != null) { setIcon(entry, normalIconDescriptor, sbChipIcon) } setIcon(entry, sensitiveIconDescriptor, shelfIcon) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt index 2c5d9c2e449b..3c2051f0b153 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsController.kt @@ -20,7 +20,6 @@ import android.service.notification.StatusBarNotification import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer /** @@ -33,7 +32,6 @@ interface NotificationsController { fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index ea6a60bd7a1c..0a9899e88d24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -34,7 +34,6 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica import com.android.systemui.statusbar.notification.collection.init.NotifPipelineInitializer import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer @@ -76,7 +75,6 @@ constructor( override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) { notificationListener.registerAsSystemService() @@ -101,7 +99,7 @@ constructor( notifPipelineInitializer .get() - .initialize(notificationListener, notificationRowBinder, listContainer, stackController) + .initialize(notificationListener, notificationRowBinder, listContainer) targetSdkResolver.initialize(notifPipeline.get()) notificationsMediaManager.setUpWithPresenter(presenter) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt index 148b3f021643..92d96f9e899b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerStub.kt @@ -21,7 +21,6 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.Snoo import com.android.systemui.statusbar.NotificationListener import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.NotificationActivityStarter -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.stack.NotificationListContainer import javax.inject.Inject @@ -35,7 +34,6 @@ constructor(private val notificationListener: NotificationListener) : Notificati override fun initialize( presenter: NotificationPresenter, listContainer: NotificationListContainer, - stackController: NotifStackController, notificationActivityStarter: NotificationActivityStarter, ) { // Always connect the listener even if notification-handling is disabled. Being a listener diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt index 10e67a40ebc9..640d364895ae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorListView.kt @@ -25,7 +25,7 @@ import android.app.NotificationManager.IMPORTANCE_NONE import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED import android.content.Context import android.graphics.drawable.Drawable -import android.text.TextUtils +import android.text.TextUtils.isEmpty import android.transition.AutoTransition import android.transition.Transition import android.transition.TransitionManager @@ -37,13 +37,10 @@ import android.widget.LinearLayout import android.widget.Switch import android.widget.TextView import com.android.settingslib.Utils - import com.android.systemui.res.R import com.android.systemui.util.Assert -/** - * Half-shelf for notification channel controls - */ +/** Half-shelf for notification channel controls */ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { lateinit var controller: ChannelEditorDialogController var appIcon: Drawable? = null @@ -84,23 +81,21 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a val transition = AutoTransition() transition.duration = 200 - transition.addListener(object : Transition.TransitionListener { - override fun onTransitionEnd(p0: Transition?) { - notifySubtreeAccessibilityStateChangedIfNeeded() - } + transition.addListener( + object : Transition.TransitionListener { + override fun onTransitionEnd(p0: Transition?) { + notifySubtreeAccessibilityStateChangedIfNeeded() + } - override fun onTransitionResume(p0: Transition?) { - } + override fun onTransitionResume(p0: Transition?) {} - override fun onTransitionPause(p0: Transition?) { - } + override fun onTransitionPause(p0: Transition?) {} - override fun onTransitionCancel(p0: Transition?) { - } + override fun onTransitionCancel(p0: Transition?) {} - override fun onTransitionStart(p0: Transition?) { + override fun onTransitionStart(p0: Transition?) {} } - }) + ) TransitionManager.beginDelayedTransition(this, transition) // Remove any rows @@ -130,8 +125,9 @@ class ChannelEditorListView(c: Context, attrs: AttributeSet) : LinearLayout(c, a private fun updateAppControlRow(enabled: Boolean) { appControlRow.iconView.setImageDrawable(appIcon) - appControlRow.channelName.text = context.resources - .getString(R.string.notification_channel_dialog_title, appName) + val title = context.resources.getString(R.string.notification_channel_dialog_title, appName) + appControlRow.channelName.text = title + appControlRow.switch.contentDescription = title appControlRow.switch.isChecked = enabled appControlRow.switch.setOnCheckedChangeListener { _, b -> controller.proposeSetAppNotificationsEnabled(b) @@ -164,8 +160,8 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { var gentle = false init { - highlightColor = Utils.getColorAttrDefaultColor( - context, android.R.attr.colorControlHighlight) + highlightColor = + Utils.getColorAttrDefaultColor(context, android.R.attr.colorControlHighlight) } var channel: NotificationChannel? = null @@ -182,17 +178,16 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { switch = requireViewById(R.id.toggle) switch.setOnCheckedChangeListener { _, b -> channel?.let { - controller.proposeEditForChannel(it, - if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) - else IMPORTANCE_NONE) + controller.proposeEditForChannel( + it, + if (b) it.originalImportance.coerceAtLeast(IMPORTANCE_LOW) else IMPORTANCE_NONE, + ) } } setOnClickListener { switch.toggle() } } - /** - * Play an animation that highlights this row - */ + /** Play an animation that highlights this row */ fun playHighlight() { // Use 0 for the start value because our background is given to us by our parent val fadeInLoop = ValueAnimator.ofObject(ArgbEvaluator(), 0, highlightColor) @@ -211,17 +206,21 @@ class ChannelRow(c: Context, attrs: AttributeSet) : LinearLayout(c, attrs) { channelName.text = nc.name ?: "" - nc.group?.let { groupId -> - channelDescription.text = controller.groupNameForId(groupId) - } + nc.group?.let { groupId -> channelDescription.text = controller.groupNameForId(groupId) } - if (nc.group == null || TextUtils.isEmpty(channelDescription.text)) { + if (nc.group == null || isEmpty(channelDescription.text)) { channelDescription.visibility = View.GONE } else { channelDescription.visibility = View.VISIBLE } switch.isChecked = nc.importance != IMPORTANCE_NONE + switch.contentDescription = + if (isEmpty(channelDescription.text)) { + channelName.text + } else { + "${channelName.text} ${channelDescription.text}" + } } private fun updateImportance() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 7e3d0043b91a..95604c113a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -1267,6 +1267,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } if (mExpandedWhenPinned) { return Math.max(getMaxExpandHeight(), getHeadsUpHeight()); + } else if (android.app.Flags.compactHeadsUpNotification() + && getShowingLayout().isHUNCompact()) { + return getHeadsUpHeight(); } else if (atLeastMinHeight) { return Math.max(getCollapsedHeight(), getHeadsUpHeight()); } else { @@ -3680,6 +3683,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return super.disallowSingleClick(event); } + // TODO: b/388470175 - Although this does get triggered when a notification + // is expanded by the system (e.g. the first notication in the shade), it + // will not be when a notification is collapsed by the system (such as when + // the shade is closed). private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt new file mode 100644 index 000000000000..e27ff7d6746b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2025 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.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable + +/** + * A background style for smarter-smart-actions. + * + * TODO(b/383567383) implement final UX + */ +class MagicActionBackgroundDrawable(context: Context) : Drawable() { + + private var _alpha: Int = 255 + private var _colorFilter: ColorFilter? = null + private val paint = + Paint().apply { + color = context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) + } + + override fun draw(canvas: Canvas) { + canvas.drawRect(bounds, paint) + } + + override fun setAlpha(alpha: Int) { + _alpha = alpha + invalidateSelf() + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + _colorFilter = colorFilter + invalidateSelf() + } + + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT +} 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 7c44eae6c0b8..70e27a981b49 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 @@ -16,8 +16,6 @@ package com.android.systemui.statusbar.notification.row; -import static android.app.Flags.notificationsRedesignTemplates; - import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; @@ -481,16 +479,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder logger.logAsyncTaskProgress(entryForLogging, "creating low-priority group summary remote view"); result.mNewMinimizedGroupHeaderView = - builder.makeLowPriorityContentView(/* useRegularSubtext = */ true, - /* highlightExpander = */ notificationsRedesignTemplates()); + builder.makeLowPriorityContentView(true /* useRegularSubtext */); } } setNotifsViewsInflaterFactory(result, row, notifLayoutInflaterFactoryProvider); result.packageContext = packageContext; result.headsUpStatusBarText = builder.getHeadsUpStatusBarText( - /* showingPublic = */ false); + false /* showingPublic */); result.headsUpStatusBarTextPublic = builder.getHeadsUpStatusBarText( - /* showingPublic = */ true); + true /* showingPublic */); return result; }); @@ -1139,8 +1136,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private static RemoteViews createContentView(Notification.Builder builder, boolean isMinimized, boolean useLarge) { if (isMinimized) { - return builder.makeLowPriorityContentView(/* useRegularSubtext = */ false, - /* highlightExpander = */ false); + return builder.makeLowPriorityContentView(false /* useRegularSubtext */); } return builder.createContentView(useLarge); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 786d7d9ea0f3..0d2998174121 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -207,6 +207,8 @@ public class NotificationContentView extends FrameLayout implements Notification private boolean mContentAnimating; private UiEventLogger mUiEventLogger; + private boolean mIsHUNCompact; + public NotificationContentView(Context context, AttributeSet attrs) { super(context, attrs); mHybridGroupManager = new HybridGroupManager(getContext()); @@ -543,6 +545,7 @@ public class NotificationContentView extends FrameLayout implements Notification if (child == null) { mHeadsUpChild = null; mHeadsUpWrapper = null; + mIsHUNCompact = false; if (mTransformationStartVisibleType == VISIBLE_TYPE_HEADSUP) { mTransformationStartVisibleType = VISIBLE_TYPE_NONE; } @@ -556,8 +559,9 @@ public class NotificationContentView extends FrameLayout implements Notification mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child, mContainingNotification); - if (Flags.compactHeadsUpNotification() - && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper) { + mIsHUNCompact = Flags.compactHeadsUpNotification() + && mHeadsUpWrapper instanceof NotificationCompactHeadsUpTemplateViewWrapper; + if (mIsHUNCompact) { logCompactHUNShownEvent(); } @@ -902,6 +906,10 @@ public class NotificationContentView extends FrameLayout implements Notification } } + public boolean isHUNCompact() { + return mIsHUNCompact; + } + private boolean isGroupExpanded() { return mContainingNotification.isGroupExpanded(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index ae9b69c8f6bf..c619b17f1ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint -import android.app.Flags.notificationsRedesignTemplates import android.app.Notification import android.app.Notification.MessagingStyle import android.content.Context @@ -888,10 +887,7 @@ constructor( entryForLogging, "creating low-priority group summary remote view", ) - builder.makeLowPriorityContentView( - /* useRegularSubtext = */ true, - /* highlightExpander = */ notificationsRedesignTemplates(), - ) + builder.makeLowPriorityContentView(true /* useRegularSubtext */) } else null NewRemoteViews( contracted = contracted, @@ -1661,10 +1657,7 @@ constructor( useLarge: Boolean, ): RemoteViews { return if (isMinimized) { - builder.makeLowPriorityContentView( - /* useRegularSubtext = */ false, - /* highlightExpander = */ false, - ) + builder.makeLowPriorityContentView(false /* useRegularSubtext */) } else builder.createContentView(useLarge) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index e477c7430262..8e48065d9d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -568,8 +568,7 @@ public class NotificationChildrenContainer extends ViewGroup builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } - header = builder.makeLowPriorityContentView(true /* useRegularSubtext */, - notificationsRedesignTemplates() /* highlightExpander */); + header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); if (mMinimizedGroupHeader == null) { mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 76591ac4e453..7b55e83a0a99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1245,15 +1245,19 @@ public class NotificationStackScrollLayout @Override public void setHeadsUpTop(float headsUpTop) { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; - mAmbientState.setHeadsUpTop(headsUpTop); - requestChildrenUpdate(); + if (mAmbientState.getHeadsUpTop() != headsUpTop) { + mAmbientState.setHeadsUpTop(headsUpTop); + requestChildrenUpdate(); + } } @Override public void setHeadsUpBottom(float headsUpBottom) { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; - mAmbientState.setHeadsUpBottom(headsUpBottom); - mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom)); + if (mAmbientState.getHeadsUpBottom() != headsUpBottom) { + mAmbientState.setHeadsUpBottom(headsUpBottom); + mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom)); + } } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index b892bebb3120..c717e3b229be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -35,6 +35,8 @@ import static com.android.systemui.statusbar.notification.stack.StackStateAnimat import android.animation.ObjectAnimator; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.RenderEffect; +import android.graphics.Shader; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; @@ -103,9 +105,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider; import com.android.systemui.statusbar.notification.collection.provider.VisibilityLocationProviderDelegator; -import com.android.systemui.statusbar.notification.collection.render.DefaultNotifStackController; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; -import com.android.systemui.statusbar.notification.collection.render.NotifStackController; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.headsup.HeadsUpNotificationViewControllerEmptyImpl; @@ -211,9 +211,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationListContainerImpl mNotificationListContainer = new NotificationListContainerImpl(); - // TODO: b/293167744 - Remove this. - private final NotifStackController mNotifStackController = - new DefaultNotifStackController(); @VisibleForTesting final View.OnAttachStateChangeListener mOnAttachStateChangeListener = @@ -1242,6 +1239,22 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Applies a blur effect to the view. + * + * @param blurRadius Radius of blur + */ + public void setBlurRadius(float blurRadius) { + if (blurRadius > 0.0f) { + mView.setRenderEffect(RenderEffect.createBlurEffect( + blurRadius, + blurRadius, + Shader.TileMode.CLAMP)); + } else { + mView.setRenderEffect(null); + } + } + private void updateAlpha() { if (mView != null) { mView.setAlpha(Math.min(Math.min(mMaxAlphaFromView, mMaxAlphaForKeyguard), @@ -1469,10 +1482,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mNotificationListContainer; } - public NotifStackController getNotifStackController() { - return mNotifStackController; - } - public void resetCheckSnoozeLeavebehind() { mView.resetCheckSnoozeLeavebehind(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 0b2b84e60f4b..3ea4d488357d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -179,6 +179,10 @@ constructor( } } + if (Flags.bouncerUiRevamp()) { + launch { viewModel.blurRadius.collect { controller.setBlurRadius(it) } } + } + if (communalSettingsInteractor.isCommunalFlagEnabled()) { launch { viewModel.glanceableHubAlpha.collect { 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 fc8c70fb8e9a..f0455fc3a22b 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,6 +42,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED +import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel @@ -154,6 +155,7 @@ constructor( private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel, private val primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, + private val primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, aodBurnInViewModel: AodBurnInViewModel, private val communalSceneInteractor: CommunalSceneInteractor, // Lazy because it's only used in the SceneContainer + Dual Shade configuration. @@ -562,7 +564,7 @@ constructor( lockscreenToDreamingTransitionViewModel.lockscreenAlpha, lockscreenToGoneTransitionViewModel.notificationAlpha(viewState), lockscreenToOccludedTransitionViewModel.lockscreenAlpha, - lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, + lockscreenToPrimaryBouncerTransitionViewModel.notificationAlpha, alternateBouncerToPrimaryBouncerTransitionViewModel.notificationAlpha, occludedToAodTransitionViewModel.lockscreenAlpha, occludedToGoneTransitionViewModel.notificationAlpha(viewState), @@ -626,6 +628,12 @@ constructor( .dumpWhileCollecting("keyguardAlpha") } + val blurRadius = + primaryBouncerTransitions + .map { transition -> transition.notificationBlurRadius } + .merge() + .dumpWhileCollecting("blurRadius") + /** * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB or * DREAMING<->GLANCEABLE_HUB transition or idle on the hub. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 6f29f618ee0d..afc5bc67c0a1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -750,7 +750,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp BiometricUnlockSource.Companion.fromBiometricSourceType(biometricSourceType) ); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT - && mUpdateMonitor.isUdfpsSupported()) { + && mUpdateMonitor.isOpticalUdfpsSupported()) { long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 3d6cd7e49dfe..b146b92ed110 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1492,7 +1492,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationsController.initialize( mPresenterLazy.get(), mNotifListContainer, - mStackScrollerController.getNotifStackController(), mNotificationActivityStarterLazy.get()); mWindowRootViewVisibilityInteractor.setUp(mPresenterLazy.get(), mNotificationsController); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index a2b4e7b474a0..880df79d8e6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -40,12 +40,10 @@ import android.service.notification.ZenModeConfig; import android.telecom.TelecomManager; import android.text.format.DateFormat; import android.util.Log; -import android.view.View; import androidx.lifecycle.Observer; import com.android.internal.statusbar.StatusBarIcon; -import com.android.systemui.Flags; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; @@ -60,13 +58,10 @@ import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.res.R; import com.android.systemui.screenrecord.RecordingController; -import com.android.systemui.screenrecord.data.model.ScreenRecordModel; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.policy.BluetoothController; -import com.android.systemui.statusbar.policy.CastController; -import com.android.systemui.statusbar.policy.CastDevice; import com.android.systemui.statusbar.policy.DataSaverController; import com.android.systemui.statusbar.policy.DataSaverController.Listener; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -139,7 +134,6 @@ public class PhoneStatusBarPolicy private final TelecomManager mTelecomManager; private final Handler mHandler; - private final CastController mCast; private final HotspotController mHotspot; private final NextAlarmController mNextAlarmController; private final AlarmManager mAlarmManager; @@ -161,7 +155,6 @@ public class PhoneStatusBarPolicy private final Executor mMainExecutor; private final Executor mUiBgExecutor; private final SensorPrivacyController mSensorPrivacyController; - private final RecordingController mRecordingController; private final RingerModeTracker mRingerModeTracker; private final PrivacyLogger mPrivacyLogger; private final ZenModeInteractor mZenModeInteractor; @@ -180,7 +173,7 @@ public class PhoneStatusBarPolicy public PhoneStatusBarPolicy(StatusBarIconController iconController, CommandQueue commandQueue, BroadcastDispatcher broadcastDispatcher, @Main Executor mainExecutor, @UiBackground Executor uiBgExecutor, @Main Looper looper, - @Main Resources resources, CastController castController, + @Main Resources resources, HotspotController hotspotController, BluetoothController bluetoothController, NextAlarmController nextAlarmController, UserInfoController userInfoController, RotationLockController rotationLockController, DataSaverController dataSaverController, @@ -190,7 +183,7 @@ public class PhoneStatusBarPolicy LocationController locationController, SensorPrivacyController sensorPrivacyController, AlarmManager alarmManager, UserManager userManager, UserTracker userTracker, - DevicePolicyManager devicePolicyManager, RecordingController recordingController, + DevicePolicyManager devicePolicyManager, @Nullable TelecomManager telecomManager, @DisplayId int displayId, @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil, RingerModeTracker ringerModeTracker, @@ -206,7 +199,6 @@ public class PhoneStatusBarPolicy mBroadcastDispatcher = broadcastDispatcher; mHandler = new Handler(looper); mResources = resources; - mCast = castController; mHotspot = hotspotController; mBluetooth = bluetoothController; mNextAlarmController = nextAlarmController; @@ -223,7 +215,6 @@ public class PhoneStatusBarPolicy mLocationController = locationController; mPrivacyItemController = privacyItemController; mSensorPrivacyController = sensorPrivacyController; - mRecordingController = recordingController; mMainExecutor = mainExecutor; mUiBgExecutor = uiBgExecutor; mTelecomManager = telecomManager; @@ -368,11 +359,6 @@ public class PhoneStatusBarPolicy this::onMainActiveModeChanged); } mZenController.addCallback(mZenControllerCallback); - if (!Flags.statusBarScreenSharingChips()) { - // If the flag is enabled, the cast icon is handled in the new screen sharing chips - // instead of here so we don't need to listen for events here. - mCast.addCallback(mCastCallback); - } mHotspot.addCallback(mHotspotCallback); mNextAlarmController.addCallback(mNextAlarmCallback); mDataSaver.addCallback(this); @@ -380,11 +366,6 @@ public class PhoneStatusBarPolicy mPrivacyItemController.addCallback(this); mSensorPrivacyController.addCallback(mSensorPrivacyListener); mLocationController.addCallback(this); - if (!Flags.statusBarScreenSharingChips()) { - // If the flag is enabled, the screen record icon is handled in the new screen sharing - // chips instead of here so we don't need to listen for events here. - mRecordingController.addCallback(this); - } mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(), this::onConnectedDisplayAvailabilityChanged); @@ -583,33 +564,6 @@ public class PhoneStatusBarPolicy } } - private void updateCast() { - if (Flags.statusBarScreenSharingChips()) { - // The cast icon is handled in the new screen sharing chips instead of here. - return; - } - - boolean isCasting = false; - for (CastDevice device : mCast.getCastDevices()) { - if (device.isCasting()) { - isCasting = true; - break; - } - } - if (DEBUG) Log.v(TAG, "updateCast: isCasting: " + isCasting); - mHandler.removeCallbacks(mRemoveCastIconRunnable); - if (isCasting && !mRecordingController.isRecording()) { // screen record has its own icon - mIconController.setIcon(mSlotCast, R.drawable.stat_sys_cast, - mResources.getString(R.string.accessibility_casting)); - mIconController.setIconVisibility(mSlotCast, true); - } else { - // don't turn off the screen-record icon for a few seconds, just to make sure the user - // has seen it - if (DEBUG) Log.v(TAG, "updateCast: hiding icon in 3 sec..."); - mHandler.postDelayed(mRemoveCastIconRunnable, 3000); - } - } - private void updateProfileIcon() { // getLastResumedActivityUserId needs to acquire the AM lock, which may be contended in // some cases. Since it doesn't really matter here whether it's updated in this frame @@ -678,13 +632,6 @@ public class PhoneStatusBarPolicy } }; - private final CastController.Callback mCastCallback = new CastController.Callback() { - @Override - public void onCastDevicesChanged() { - updateCast(); - } - }; - private final NextAlarmController.NextAlarmChangeCallback mNextAlarmCallback = new NextAlarmController.NextAlarmChangeCallback() { @Override @@ -856,85 +803,6 @@ public class PhoneStatusBarPolicy } }; - private Runnable mRemoveCastIconRunnable = new Runnable() { - @Override - public void run() { - if (Flags.statusBarScreenSharingChips()) { - // The cast icon is handled in the new screen sharing chips instead of here. - return; - } - if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW"); - mIconController.setIconVisibility(mSlotCast, false); - } - }; - - // Screen Recording - @Override - public void onCountdown(long millisUntilFinished) { - if (Flags.statusBarScreenSharingChips()) { - // The screen record icon is handled in the new screen sharing chips instead of here. - return; - } - if (DEBUG) Log.d(TAG, "screenrecord: countdown " + millisUntilFinished); - int countdown = - (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(millisUntilFinished); - int resourceId = R.drawable.stat_sys_screen_record; - String description = Integer.toString(countdown); - switch (countdown) { - case 1: - resourceId = R.drawable.stat_sys_screen_record_1; - break; - case 2: - resourceId = R.drawable.stat_sys_screen_record_2; - break; - case 3: - resourceId = R.drawable.stat_sys_screen_record_3; - break; - } - mIconController.setIcon(mSlotScreenRecord, resourceId, description); - mIconController.setIconVisibility(mSlotScreenRecord, true); - // Set as assertive so talkback will announce the countdown - mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord, - View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE); - } - - @Override - public void onCountdownEnd() { - if (Flags.statusBarScreenSharingChips()) { - // The screen record icon is handled in the new screen sharing chips instead of here. - return; - } - if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown"); - mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false)); - // Reset talkback priority - mHandler.post(() -> mIconController.setIconAccessibilityLiveRegion(mSlotScreenRecord, - View.ACCESSIBILITY_LIVE_REGION_NONE)); - } - - @Override - public void onRecordingStart() { - if (Flags.statusBarScreenSharingChips()) { - // The screen record icon is handled in the new screen sharing chips instead of here. - return; - } - if (DEBUG) Log.d(TAG, "screenrecord: showing icon"); - mIconController.setIcon(mSlotScreenRecord, - R.drawable.stat_sys_screen_record, - mResources.getString(R.string.screenrecord_ongoing_screen_only)); - mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, true)); - } - - @Override - public void onRecordingEnd() { - if (Flags.statusBarScreenSharingChips()) { - // The screen record icon is handled in the new screen sharing chips instead of here. - return; - } - // Ensure this is on the main thread - if (DEBUG) Log.d(TAG, "screenrecord: hiding icon"); - mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false)); - } - private void onConnectedDisplayAvailabilityChanged(ConnectedDisplayInteractor.State state) { boolean visible = state != ConnectedDisplayInteractor.State.DISCONNECTED; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java index c31e34c50b06..1f1be261a854 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java @@ -42,7 +42,6 @@ import com.android.app.animation.Interpolators; import com.android.app.animation.InterpolatorsAndroidX; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; -import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; @@ -81,6 +80,7 @@ import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -142,6 +142,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private StatusBarVisibilityModel mLastModifiedVisibility = StatusBarVisibilityModel.createDefaultModel(); private DarkIconManager mDarkIconManager; + private HomeStatusBarViewModel mHomeStatusBarViewModel; + private final HomeStatusBarComponent.Factory mHomeStatusBarComponentFactory; private final CommandQueue mCommandQueue; private final CollapsedStatusBarFragmentLogger mCollapsedStatusBarFragmentLogger; @@ -151,8 +153,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue private final ShadeExpansionStateManager mShadeExpansionStateManager; private final StatusBarIconController mStatusBarIconController; private final CarrierConfigTracker mCarrierConfigTracker; - private final HomeStatusBarViewModel mHomeStatusBarViewModel; private final HomeStatusBarViewBinder mHomeStatusBarViewBinder; + private final HomeStatusBarViewModelFactory mHomeStatusBarViewModelFactory; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; private final DarkIconManager.Factory mDarkIconManagerFactory; private final SecureSettings mSecureSettings; @@ -256,7 +258,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue ShadeExpansionStateManager shadeExpansionStateManager, StatusBarIconController statusBarIconController, DarkIconManager.Factory darkIconManagerFactory, - HomeStatusBarViewModel homeStatusBarViewModel, + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory, HomeStatusBarViewBinder homeStatusBarViewBinder, StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager, KeyguardStateController keyguardStateController, @@ -281,7 +283,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mAnimationScheduler = animationScheduler; mShadeExpansionStateManager = shadeExpansionStateManager; mStatusBarIconController = statusBarIconController; - mHomeStatusBarViewModel = homeStatusBarViewModel; + mHomeStatusBarViewModelFactory = homeStatusBarViewModelFactory; mHomeStatusBarViewBinder = homeStatusBarViewBinder; mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager; mDarkIconManagerFactory = darkIconManagerFactory; @@ -410,6 +412,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue mCarrierConfigTracker.addCallback(mCarrierConfigCallback); mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener); + mHomeStatusBarViewModel = mHomeStatusBarViewModelFactory.create(displayId); mHomeStatusBarViewBinder.bind( view.getContext().getDisplayId(), mStatusBar, @@ -694,20 +697,9 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue boolean showClock = externalModel.getShowClock() && !headsUpVisible; - boolean showPrimaryOngoingActivityChip; - if (Flags.statusBarScreenSharingChips()) { - // If this flag is on, the ongoing activity status comes from - // CollapsedStatusBarViewBinder, which updates the mHasPrimaryOngoingActivity variable. - showPrimaryOngoingActivityChip = mHasPrimaryOngoingActivity; - } else { - // If this flag is off, the only ongoing activity is the ongoing call, and we pull it - // from the controller directly. - showPrimaryOngoingActivityChip = mOngoingCallController.hasOngoingCall(); - } + boolean showPrimaryOngoingActivityChip = mHasPrimaryOngoingActivity; boolean showSecondaryOngoingActivityChip = - Flags.statusBarScreenSharingChips() - && StatusBarNotifChips.isEnabled() - && mHasSecondaryOngoingActivity; + StatusBarNotifChips.isEnabled() && mHasSecondaryOngoingActivity; return new StatusBarVisibilityModel( showClock, @@ -865,10 +857,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue /** * Displays the primary ongoing activity chip. - * - * If Flags.statusBarScreenSharingChips is disabled, this chip will only ever contain the - * ongoing call information, If that flag is enabled, it will support different kinds of ongoing - * activities. See b/332662551. */ private void showPrimaryOngoingActivityChip(boolean animate) { StatusBarRootModernization.assertInLegacyMode(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index c57cede754d3..4eb69babfadb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -18,35 +18,26 @@ package com.android.systemui.statusbar.phone.ongoingcall import android.app.ActivityManager import android.app.IActivityManager -import android.app.Notification -import android.app.Notification.CallStyle.CALL_TYPE_ONGOING import android.app.PendingIntent import android.app.UidObserver import android.content.Context import android.view.View import androidx.annotation.VisibleForTesting import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.CoreStartable import com.android.systemui.Dumpable -import com.android.systemui.Flags -import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel -import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.view.ChipChronometer import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection -import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.notification.shared.CallType @@ -54,13 +45,14 @@ import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingC import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -/** A controller to handle the ongoing call chip in the collapsed status bar. +/** + * A controller to handle the ongoing call chip in the collapsed status bar. + * * @deprecated Use [OngoingCallInteractor] instead, which follows recommended architecture patterns */ @Deprecated("Use OngoingCallInteractor instead") @@ -71,10 +63,7 @@ constructor( @Application private val scope: CoroutineScope, private val context: Context, private val ongoingCallRepository: OngoingCallRepository, - private val notifCollection: CommonNotifCollection, private val activeNotificationsInteractor: ActiveNotificationsInteractor, - private val systemClock: SystemClock, - private val activityStarter: ActivityStarter, @Main private val mainExecutor: Executor, private val iActivityManager: IActivityManager, private val dumpManager: DumpManager, @@ -90,111 +79,29 @@ constructor( private val mListeners: MutableList<OngoingCallListener> = mutableListOf() private val uidObserver = CallAppUidObserver() - private val notifListener = - object : NotifCollectionListener { - // Temporary workaround for b/178406514 for testing purposes. - // - // b/178406514 means that posting an incoming call notif then updating it to an ongoing - // call notif does not work (SysUI never receives the update). This workaround allows us - // to trigger the ongoing call chip when an ongoing call notif is *added* rather than - // *updated*, allowing us to test the chip. - // - // TODO(b/183229367): Remove this function override when b/178406514 is fixed. - override fun onEntryAdded(entry: NotificationEntry) { - onEntryUpdated(entry, true) - } - - override fun onEntryUpdated(entry: NotificationEntry) { - StatusBarUseReposForCallChip.assertInLegacyMode() - // We have a new call notification or our existing call notification has been - // updated. - // TODO(b/183229367): This likely won't work if you take a call from one app then - // switch to a call from another app. - if ( - callNotificationInfo == null && isCallNotification(entry) || - (entry.sbn.key == callNotificationInfo?.key) - ) { - val newOngoingCallInfo = - CallNotificationInfo( - entry.sbn.key, - entry.sbn.notification.getWhen(), - // In this old listener pattern, we don't have access to the - // notification icon. - notificationIconView = null, - entry.sbn.notification.contentIntent, - entry.sbn.uid, - entry.sbn.notification.extras.getInt( - Notification.EXTRA_CALL_TYPE, - -1, - ) == CALL_TYPE_ONGOING, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, - ) - if (newOngoingCallInfo == callNotificationInfo) { - return - } - - callNotificationInfo = newOngoingCallInfo - if (newOngoingCallInfo.isOngoing) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif *is* ongoing -> showing chip. key=$str1" }, - ) - updateChip() - } else { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = newOngoingCallInfo.key }, - { "Call notif not ongoing -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } - - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - if (entry.sbn.key == callNotificationInfo?.key) { - logger.log( - TAG, - LogLevel.DEBUG, - { str1 = entry.sbn.key }, - { "Call notif removed -> hiding chip. key=$str1" }, - ) - removeChip() - } - } - } override fun start() { - if (StatusBarChipsModernization.isEnabled) - return + if (StatusBarChipsModernization.isEnabled) return dumpManager.registerDumpable(this) - if (Flags.statusBarUseReposForCallChip()) { - scope.launch { - // Listening to [ActiveNotificationsInteractor] instead of using - // [NotifCollectionListener#onEntryUpdated] is better for two reasons: - // 1. ActiveNotificationsInteractor automatically filters the notification list to - // just notifications for the current user, which ensures we don't show a call chip - // for User 1's call while User 2 is active (see b/328584859). - // 2. ActiveNotificationsInteractor only emits notifications that are currently - // present in the shade, which means we know we've already inflated the icon that we - // might use for the call chip (see b/354930838). - activeNotificationsInteractor.ongoingCallNotification.collect { - updateInfoFromNotifModel(it) - } + scope.launch { + // Listening to [ActiveNotificationsInteractor] instead of using + // [NotifCollectionListener#onEntryUpdated] is better for two reasons: + // 1. ActiveNotificationsInteractor automatically filters the notification list to + // just notifications for the current user, which ensures we don't show a call chip + // for User 1's call while User 2 is active (see b/328584859). + // 2. ActiveNotificationsInteractor only emits notifications that are currently + // present in the shade, which means we know we've already inflated the icon that we + // might use for the call chip (see b/354930838). + activeNotificationsInteractor.ongoingCallNotification.collect { + updateInfoFromNotifModel(it) } - } else { - notifCollection.addCollectionListener(notifListener) } scope.launch { statusBarModeRepository.defaultDisplay.isInFullscreenMode.collect { isFullscreen = it - updateChipClickListener() updateGestureListening() } } @@ -244,21 +151,12 @@ constructor( logger.log( TAG, LogLevel.DEBUG, - { - bool1 = Flags.statusBarCallChipNotificationIcon() - bool2 = currentInfo.notificationIconView != null - }, - { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }, + { bool1 = currentInfo.notificationIconView != null }, + { "Creating OngoingCallModel.InCall. hasIcon=$bool1" }, ) - val icon = - if (Flags.statusBarCallChipNotificationIcon()) { - currentInfo.notificationIconView - } else { - null - } return OngoingCallModel.InCall( startTimeMs = currentInfo.callStartTime, - notificationIconView = icon, + notificationIconView = currentInfo.notificationIconView, intent = currentInfo.intent, notificationKey = currentInfo.key, ) @@ -288,7 +186,7 @@ constructor( if (notifModel == null) { logger.log(TAG, LogLevel.DEBUG, {}, { "NotifInteractorCallModel: null" }) - removeChip() + removeChipInfo() } else if (notifModel.callType != CallType.Ongoing) { logger.log( TAG, @@ -296,7 +194,7 @@ constructor( { str1 = notifModel.callType.name }, { "Notification Interactor sent ActiveNotificationModel with callType=$str1" }, ) - removeChip() + removeChipInfo() } else { logger.log( TAG, @@ -337,29 +235,10 @@ constructor( val timeView = currentChipView?.getTimeView() if (currentChipView != null && timeView != null) { - if (!Flags.statusBarScreenSharingChips()) { - // If the new status bar screen sharing chips are enabled, then the display logic - // for *all* status bar chips (both the call chip and the screen sharing chips) are - // handled by CollapsedStatusBarViewBinder, *not* this class. We need to disable - // this class from making any display changes because the new chips use the same - // view as the old call chip. - // TODO(b/332662551): We should move this whole controller class to recommended - // architecture so that we don't need to awkwardly disable only some parts of this - // class. - if (currentCallNotificationInfo.hasValidStartTime()) { - timeView.setShouldHideText(false) - timeView.base = - currentCallNotificationInfo.callStartTime - - systemClock.currentTimeMillis() + systemClock.elapsedRealtime() - timeView.start() - } else { - timeView.setShouldHideText(true) - timeView.stop() - } - updateChipClickListener() - } - - // But, this class still needs to do the non-display logic regardless of the flag. + // Current behavior: Displaying the call chip is handled by HomeStatusBarViewBinder, but + // this class is still responsible for the non-display logic. + // Future behavior: if StatusBarChipsModernization flag is enabled, this class is + // completely deprecated and does nothing. uidObserver.registerWithUid(currentCallNotificationInfo.uid) if (!currentCallNotificationInfo.statusBarSwipedAway) { statusBarWindowControllerStore.defaultDisplay @@ -380,33 +259,6 @@ constructor( } } - private fun updateChipClickListener() { - StatusBarChipsModernization.assertInLegacyMode() - - if (Flags.statusBarScreenSharingChips()) { - return - } - - if (callNotificationInfo == null) { - return - } - val currentChipView = chipView - val backgroundView = - currentChipView?.findViewById<View>(R.id.ongoing_activity_chip_background) - val intent = callNotificationInfo?.intent - if (currentChipView != null && backgroundView != null && intent != null) { - currentChipView.setOnClickListener { - activityStarter.postStartActivityDismissingKeyguard( - intent, - ActivityTransitionAnimator.Controller.fromView( - backgroundView, - InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, - ), - ) - } - } - } - /** Returns true if the given [procState] represents a process that's visible to the user. */ private fun isProcessVisibleToUser(procState: Int): Boolean { StatusBarChipsModernization.assertInLegacyMode() @@ -430,13 +282,10 @@ constructor( } } - private fun removeChip() { + private fun removeChipInfo() { StatusBarChipsModernization.assertInLegacyMode() callNotificationInfo = null - if (!Flags.statusBarScreenSharingChips()) { - tearDownChipView() - } statusBarWindowControllerStore.defaultDisplay.setOngoingProcessRequiresStatusBarVisible( false ) @@ -489,13 +338,7 @@ constructor( val isOngoing: Boolean, /** True if the user has swiped away the status bar while in this phone call. */ val statusBarSwipedAway: Boolean, - ) { - /** - * Returns true if the notification information has a valid call start time. See - * b/192379214. - */ - fun hasValidStartTime(): Boolean = callStartTime > 0 - } + ) override fun dump(pw: PrintWriter, args: Array<out String>) { pw.println("Active call notification: $callNotificationInfo") @@ -597,8 +440,4 @@ constructor( } } -private fun isCallNotification(entry: NotificationEntry): Boolean { - return entry.sbn.notification.isStyle(Notification.CallStyle::class.java) -} - private const val TAG = OngoingCallRepository.TAG diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt deleted file mode 100644 index 4bdd90ebff3e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/StatusBarUseReposForCallChip.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.phone.ongoingcall - -import com.android.systemui.Flags -import com.android.systemui.flags.FlagToken -import com.android.systemui.flags.RefactorFlagUtils - -/** Helper for reading or using the status bar use repos for call chip flag state. */ -@Suppress("NOTHING_TO_INLINE") -object StatusBarUseReposForCallChip { - /** The aconfig flag name */ - const val FLAG_NAME = Flags.FLAG_STATUS_BAR_USE_REPOS_FOR_CALL_CHIP - - /** A token used for dependency declaration */ - val token: FlagToken - get() = FlagToken(FLAG_NAME, isEnabled) - - /** Is the refactor enabled */ - @JvmStatic - inline val isEnabled - get() = Flags.statusBarUseReposForCallChip() - - /** - * 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/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 96666d83b39b..c71162a22d2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -56,8 +56,8 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinderImpl -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel -import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModelImpl.HomeStatusBarViewModelFactoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher @@ -148,7 +148,9 @@ abstract class StatusBarPipelineModule { abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable @Binds - abstract fun homeStatusBarViewModel(impl: HomeStatusBarViewModelImpl): HomeStatusBarViewModel + abstract fun homeStatusBarViewModelFactory( + impl: HomeStatusBarViewModelFactoryImpl + ): HomeStatusBarViewModelFactory @Binds abstract fun homeStatusBarViewBinder(impl: HomeStatusBarViewBinderImpl): HomeStatusBarViewBinder diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt index 30c529a9034a..3e7094a0b5e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt @@ -34,6 +34,9 @@ interface CarrierConfigRepository { */ suspend fun startObservingCarrierConfigUpdates() - /** Gets a cached [SystemUiCarrierConfig], or creates a new one which will track the defaults */ - fun getOrCreateConfigForSubId(subId: Int): SystemUiCarrierConfig + /** + * Gets a cached [SystemUiCarrierConfig], or creates a new one which will track the defaults. A + * null [maybeSubId] will return the default carrier config. + */ + fun getOrCreateConfigForSubId(maybeSubId: Int?): SystemUiCarrierConfig } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImpl.kt index 9a97f19f6593..b32037992501 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImpl.kt @@ -20,6 +20,7 @@ import android.content.IntentFilter import android.os.PersistableBundle import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.util.SparseArray import androidx.annotation.VisibleForTesting import androidx.core.util.getOrElse @@ -51,7 +52,7 @@ constructor( private val defaultConfig: PersistableBundle by lazy { CarrierConfigManager.getDefaultConfig() } // Used for logging the default config in the dumpsys private val defaultConfigForLogs: SystemUiCarrierConfig by lazy { - SystemUiCarrierConfig(-1, defaultConfig) + SystemUiCarrierConfig(INVALID_SUBSCRIPTION_ID, defaultConfig) } private val configs = SparseArray<SystemUiCarrierConfig>() @@ -89,7 +90,10 @@ constructor( configToUpdate.processNewCarrierConfig(config) } - override fun getOrCreateConfigForSubId(subId: Int): SystemUiCarrierConfig { + override fun getOrCreateConfigForSubId(maybeSubId: Int?): SystemUiCarrierConfig { + // See CarrierConfigManager#getConfigForSubId(), passing INVALID_SUBSCRIPTION_ID yields + // the default carrier config + val subId = maybeSubId ?: INVALID_SUBSCRIPTION_ID return configs.getOrElse(subId) { val config = SystemUiCarrierConfig(subId, defaultConfig) val carrierConfig = carrierConfigManager?.getConfigForSubId(subId) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 32e9c85bea81..09a626940c79 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -48,8 +48,8 @@ interface MobileConnectionsRepository { */ val activeSubChangedInGroupEvent: Flow<Unit> - /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ - val defaultDataSubId: StateFlow<Int> + /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId]. Null if there is no default */ + val defaultDataSubId: StateFlow<Int?> /** * True if the default network connection is a mobile-like connection and false otherwise. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index fc766915e4ef..252ebe6a32b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -164,7 +164,7 @@ constructor( override fun getIsAnySimSecure(): Boolean = activeRepo.value.getIsAnySimSecure() - override val defaultDataSubId: StateFlow<Int> = + override val defaultDataSubId: StateFlow<Int?> = activeRepo .flatMapLatest { it.defaultDataSubId } .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 936954f3b484..b608e53b4bce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -166,7 +166,7 @@ constructor( private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key } // TODO(b/261029387): add a command for this value - override val defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) + override val defaultDataSubId: MutableStateFlow<Int?> = MutableStateFlow(null) // TODO(b/261029387): not yet supported override val mobileIsDefault: StateFlow<Boolean> = MutableStateFlow(true) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index 589db1690d3f..aa6da61958e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -58,6 +58,7 @@ import com.android.systemui.util.kotlin.pairwise import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import java.io.PrintWriter import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -108,9 +109,8 @@ constructor( ) : MobileConnectionsRepository, Dumpable { // TODO(b/333912012): for now, we are never invalidating the cache. We can do better though - private var subIdRepositoryCache: - MutableMap<Int, WeakReference<FullMobileConnectionRepository>> = - mutableMapOf() + private var subIdRepositoryCache = + ConcurrentHashMap<Int, WeakReference<FullMobileConnectionRepository>>() private val defaultNetworkName = NetworkNameModel.Default( @@ -249,7 +249,7 @@ constructor( tableLogger, LOGGING_PREFIX, columnName = "activeSubId", - initialValue = INVALID_SUBSCRIPTION_ID, + initialValue = null, ) .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) @@ -264,22 +264,31 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), null) - override val defaultDataSubId: StateFlow<Int> = + override val defaultDataSubId: StateFlow<Int?> = broadcastDispatcher .broadcastFlow( IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) ) { intent, _ -> - intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + val subId = + intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + if (subId == INVALID_SUBSCRIPTION_ID) { + null + } else { + subId + } } .distinctUntilChanged() .logDiffsForTable( tableLogger, LOGGING_PREFIX, columnName = "defaultSubId", - initialValue = INVALID_SUBSCRIPTION_ID, + initialValue = null, ) - .onStart { emit(subscriptionManagerProxy.getDefaultDataSubscriptionId()) } - .stateIn(scope, SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) + .onStart { + val subId = subscriptionManagerProxy.getDefaultDataSubscriptionId() + emit(if (subId == INVALID_SUBSCRIPTION_ID) null else subId) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) private val carrierConfigChangedEvent = broadcastDispatcher diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 78731faa6167..be56461a96ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -72,7 +72,7 @@ interface MobileIconsInteractor { val filteredSubscriptions: Flow<List<SubscriptionModel>> /** Subscription ID of the current default data subscription */ - val defaultDataSubId: Flow<Int> + val defaultDataSubId: Flow<Int?> /** * The current list of [MobileIconInteractor]s associated with the current list of diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 2cd01170b5ef..67fdb3ae7681 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -62,7 +63,7 @@ constructor( @Background private val scope: CoroutineScope, ) { @VisibleForTesting - val reuseCache = mutableMapOf<Int, Pair<MobileIconViewModel, CoroutineScope>>() + val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>() val subscriptionIdsFlow: StateFlow<List<Int>> = interactor.filteredSubscriptions diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 0dd7c8499861..8daa8037c367 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -24,7 +24,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators -import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R @@ -45,6 +44,7 @@ import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernizat import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch /** @@ -107,36 +107,35 @@ constructor( } if (NotificationsLiveDataStoreRefactor.isEnabled) { - val displayId = view.display.displayId val lightsOutView: View = view.requireViewById(R.id.notification_lights_out) launch { - viewModel.areNotificationsLightsOut(displayId).collect { show -> + viewModel.areNotificationsLightsOut.collect { show -> animateLightsOutView(lightsOutView, show) } } } - if ( - Flags.statusBarScreenSharingChips() && - !StatusBarNotifChips.isEnabled && - !StatusBarChipsModernization.isEnabled - ) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) + if (!StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) { + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) launch { viewModel.primaryOngoingActivityChip.collect { primaryChipModel -> OngoingActivityChipBinder.bind( primaryChipModel, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { when (primaryChipModel) { is OngoingActivityChipModel.Shown -> - primaryChipView.show(shouldAnimateChange = true) + primaryChipViewBinding.rootView.show( + shouldAnimateChange = true + ) is OngoingActivityChipModel.Hidden -> - primaryChipView.hide( + primaryChipViewBinding.rootView.hide( state = View.GONE, shouldAnimateChange = primaryChipModel.shouldAnimate, ) @@ -162,33 +161,35 @@ constructor( } } - if ( - Flags.statusBarScreenSharingChips() && - StatusBarNotifChips.isEnabled && - !StatusBarChipsModernization.isEnabled - ) { - val primaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_primary) - val secondaryChipView: View = - view.requireViewById(R.id.ongoing_activity_chip_secondary) + if (StatusBarNotifChips.isEnabled && !StatusBarChipsModernization.isEnabled) { + // Create view bindings here so we don't keep re-fetching child views each time + // the chip model changes. + val primaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_primary) + ) + val secondaryChipViewBinding = + OngoingActivityChipBinder.createBinding( + view.requireViewById(R.id.ongoing_activity_chip_secondary) + ) launch { - viewModel.ongoingActivityChips.collect { chips -> + viewModel.ongoingActivityChips.collectLatest { chips -> OngoingActivityChipBinder.bind( chips.primary, - primaryChipView, + primaryChipViewBinding, iconViewStore, ) - // TODO(b/364653005): Don't show the secondary chip if there isn't - // enough space for it. OngoingActivityChipBinder.bind( chips.secondary, - secondaryChipView, + secondaryChipViewBinding, iconViewStore, ) if (StatusBarRootModernization.isEnabled) { - primaryChipView.adjustVisibility(chips.primary.toVisibilityModel()) - secondaryChipView.adjustVisibility( + primaryChipViewBinding.rootView.adjustVisibility( + chips.primary.toVisibilityModel() + ) + secondaryChipViewBinding.rootView.adjustVisibility( chips.secondary.toVisibilityModel() ) } else { @@ -201,6 +202,18 @@ constructor( shouldAnimate = true, ) } + + viewModel.contentArea.collect { _ -> + OngoingActivityChipBinder.resetPrimaryChipWidthRestrictions( + primaryChipViewBinding, + viewModel.ongoingActivityChips.value.primary, + ) + OngoingActivityChipBinder.resetSecondaryChipWidthRestrictions( + secondaryChipViewBinding, + viewModel.ongoingActivityChips.value.secondary, + ) + view.requestLayout() + } } } } @@ -218,7 +231,7 @@ constructor( StatusBarOperatorNameViewBinder.bind( operatorNameView, viewModel.operatorNameViewModel, - viewModel::areaTint, + viewModel.areaTint, ) launch { viewModel.shouldShowOperatorNameView.collect { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt index b7744d34560d..5dd76f4434f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/StatusBarOperatorNameViewBinder.kt @@ -32,19 +32,16 @@ object StatusBarOperatorNameViewBinder { fun bind( operatorFrameView: View, viewModel: StatusBarOperatorNameViewModel, - areaTint: (Int) -> Flow<StatusBarTintColor>, + areaTint: Flow<StatusBarTintColor>, ) { operatorFrameView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - val displayId = operatorFrameView.display.displayId - val operatorNameText = operatorFrameView.requireViewById<TextView>(R.id.operator_name) launch { viewModel.operatorName.collect { operatorNameText.text = it } } launch { - val tint = areaTint(displayId) - tint.collect { statusBarTintColors -> + areaTint.collect { statusBarTintColors -> operatorNameText.setTextColor( statusBarTintColors.tint(operatorNameText.viewBoundsOnScreen()) ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index f286a1a148fa..b1cc208e9b43 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,11 +32,15 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.isVisible import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.theme.PlatformTheme import com.android.keyguard.AlphaOptimizedLinearLayout import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -53,13 +58,14 @@ import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarIco import com.android.systemui.statusbar.pipeline.shared.ui.binder.HomeStatusBarViewBinder import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory import javax.inject.Inject /** Factory to simplify the dependency management for [StatusBarRoot] */ class StatusBarRootFactory @Inject constructor( - private val homeStatusBarViewModel: HomeStatusBarViewModel, + private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val homeStatusBarViewBinder: HomeStatusBarViewBinder, private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, private val darkIconManagerFactory: DarkIconManager.Factory, @@ -70,13 +76,14 @@ constructor( ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) + val displayId = root.context.displayId val darkIconDispatcher = darkIconDispatcherStore.forDisplay(root.context.displayId) ?: return composeView composeView.apply { setContent { StatusBarRoot( parent = root, - statusBarViewModel = homeStatusBarViewModel, + statusBarViewModel = homeStatusBarViewModelFactory.create(displayId), statusBarViewBinder = homeStatusBarViewBinder, notificationIconsBinder = notificationIconsBinder, darkIconManagerFactory = darkIconManagerFactory, @@ -137,10 +144,14 @@ fun StatusBarRoot( Box(Modifier.fillMaxSize()) { // TODO(b/364360986): remove this before rolling the flag forward - Disambiguation(viewModel = statusBarViewModel) + if (StatusBarRootModernization.SHOW_DISAMBIGUATION) { + Disambiguation(viewModel = statusBarViewModel) + } Row(Modifier.fillMaxSize()) { val scope = rememberCoroutineScope() + val visible = + statusBarViewModel.shouldHomeStatusBarBeVisible.collectAsStateWithLifecycle(false) AndroidView( factory = { context -> val inflater = LayoutInflater.from(context) @@ -158,6 +169,45 @@ fun StatusBarRoot( darkIconDispatcher, ) iconController.addIconGroup(darkIconManager) + + if (StatusBarChipsModernization.isEnabled) { + val startSideExceptHeadsUp = + phoneStatusBarView.requireViewById<LinearLayout>( + R.id.status_bar_start_side_except_heads_up + ) + + val composeView = + ComposeView(context).apply { + layoutParams = + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ) + + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + + setContent { + PlatformTheme { + val chips by + statusBarViewModel.ongoingActivityChips + .collectAsStateWithLifecycle() + OngoingActivityChips(chips = chips) + } + } + } + + // Add the composable container for ongoingActivityChips before the + // notification_icon_area to maintain the same ordering for ongoing activity + // chips in the status bar layout. + val notificationIconAreaIndex = + startSideExceptHeadsUp.indexOfChild( + startSideExceptHeadsUp.findViewById(R.id.notification_icon_area) + ) + startSideExceptHeadsUp.addView(composeView, notificationIconAreaIndex) + } + HomeStatusBarIconBlockListBinder.bind( statusIconContainer, darkIconManager, @@ -236,7 +286,12 @@ fun StatusBarRoot( } onViewCreated(phoneStatusBarView) phoneStatusBarView - } + }, + update = { view -> + // Show or hide the entire status bar. This is important so that we aren't + // visible when first inflated + view.isVisible = visible.value + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt index c9cc17389c17..6ff4354fcc46 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.annotation.ColorInt import android.graphics.Rect import android.view.View -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationSt import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.Idle import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.headsup.PinnedStatus @@ -53,7 +53,9 @@ import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteracto import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarIconBlockListInteractor import com.android.systemui.statusbar.pipeline.shared.domain.interactor.HomeStatusBarInteractor import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.VisibilityModel -import javax.inject.Inject +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -78,6 +80,9 @@ import kotlinx.coroutines.flow.stateIn * so that it's all in one place and easily testable outside of the fragment. */ interface HomeStatusBarViewModel { + /** Should the entire status bar be hidden? */ + val shouldHomeStatusBarBeVisible: Flow<Boolean> + /** * True if the device is currently transitioning from lockscreen to occluded and false * otherwise. @@ -118,6 +123,7 @@ interface HomeStatusBarViewModel { val shouldShowOperatorNameView: Flow<Boolean> val isClockVisible: Flow<VisibilityModel> val isNotificationIconContainerVisible: Flow<VisibilityModel> + /** * Pair of (system info visibility, event animation state). The animation state can be used to * respond to the system event chip animations. In all cases, system info visibility correctly @@ -128,6 +134,9 @@ interface HomeStatusBarViewModel { /** Which icons to block from the home status bar */ val iconBlockList: Flow<List<String>> + /** This status bar's current content area for the given rotation in absolute bounds. */ + val contentArea: Flow<Rect> + /** * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where * status bar and navigation icons dim. In this mode, a notification dot appears where the @@ -137,13 +146,13 @@ interface HomeStatusBarViewModel { * whether there are notifications when the device is in * [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE]. */ - fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> + val areNotificationsLightsOut: Flow<Boolean> /** - * Given a displayId, returns a flow of [StatusBarTintColor], a functional interface that will - * allow a view to calculate its correct tint depending on location + * A flow of [StatusBarTintColor], a functional interface that will allow a view to calculate + * its correct tint depending on location */ - fun areaTint(displayId: Int): Flow<StatusBarTintColor> + val areaTint: Flow<StatusBarTintColor> /** Models the current visibility for a specific child view of status bar. */ data class VisibilityModel( @@ -157,17 +166,22 @@ interface HomeStatusBarViewModel { val baseVisibility: VisibilityModel, val animationState: SystemEventAnimationState, ) + + /** Interface for the assisted factory, to allow for providing a fake in tests */ + interface HomeStatusBarViewModelFactory { + fun create(displayId: Int): HomeStatusBarViewModel + } } -@SysUISingleton class HomeStatusBarViewModelImpl -@Inject +@AssistedInject constructor( + @Assisted thisDisplayId: Int, homeStatusBarInteractor: HomeStatusBarInteractor, homeStatusBarIconBlockListInteractor: HomeStatusBarIconBlockListInteractor, - private val lightsOutInteractor: LightsOutInteractor, - private val notificationsInteractor: ActiveNotificationsInteractor, - private val darkIconInteractor: DarkIconInteractor, + lightsOutInteractor: LightsOutInteractor, + notificationsInteractor: ActiveNotificationsInteractor, + darkIconInteractor: DarkIconInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, keyguardInteractor: KeyguardInteractor, @@ -178,6 +192,7 @@ constructor( ongoingActivityChipsViewModel: OngoingActivityChipsViewModel, statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel, animations: SystemStatusEventAnimationInteractor, + statusBarContentInsetsViewModelStore: StatusBarContentInsetsViewModelStore, @Application coroutineScope: CoroutineScope, ) : HomeStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = @@ -211,22 +226,22 @@ constructor( } .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false) - override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = + override val areNotificationsLightsOut: Flow<Boolean> = if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { emptyFlow() } else { combine( notificationsInteractor.areAnyNotificationsPresent, - lightsOutInteractor.isLowProfile(displayId) ?: flowOf(false), + lightsOutInteractor.isLowProfile(thisDisplayId) ?: flowOf(false), ) { hasNotifications, isLowProfile -> hasNotifications && isLowProfile } .distinctUntilChanged() } - override fun areaTint(displayId: Int): Flow<StatusBarTintColor> = + override val areaTint: Flow<StatusBarTintColor> = darkIconInteractor - .darkState(displayId) + .darkState(thisDisplayId) .map { (areas: Collection<Rect>, tint: Int) -> StatusBarTintColor { viewBounds: Rect -> if (DarkIconDispatcher.isInAreas(areas, viewBounds)) { @@ -259,10 +274,12 @@ constructor( isHomeScreenStatusBarAllowedLegacy } - private val shouldHomeStatusBarBeVisible = - combine(isHomeStatusBarAllowed, keyguardInteractor.isSecureCameraActive) { + override val shouldHomeStatusBarBeVisible = + combine( isHomeStatusBarAllowed, - isSecureCameraActive -> + keyguardInteractor.isSecureCameraActive, + headsUpNotificationInteractor.statusBarHeadsUpStatus, + ) { isHomeStatusBarAllowed, isSecureCameraActive, headsUpState -> // When launching the camera over the lockscreen, the status icons would typically // become visible momentarily before animating out, since we're not yet aware that the // launching camera activity is fullscreen. Even once the activity finishes launching, @@ -270,7 +287,7 @@ constructor( // tells us to hide them. // To ensure that this high-visibility animation is smooth, keep the icons hidden during // a camera launch. See b/257292822. - isHomeStatusBarAllowed && !isSecureCameraActive + headsUpState.isPinned || (isHomeStatusBarAllowed && !isSecureCameraActive) } private val isAnyChipVisible = @@ -283,11 +300,12 @@ constructor( override val shouldShowOperatorNameView: Flow<Boolean> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, homeStatusBarInteractor.shouldShowOperatorName, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags, shouldShowOperator -> - val hideForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags, shouldShowOperator + -> + val hideForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem shouldStatusBarBeVisible && !hideForHeadsUp && visibilityViaDisableFlags.isSystemInfoAllowed && @@ -297,10 +315,10 @@ constructor( override val isClockVisible: Flow<VisibilityModel> = combine( shouldHomeStatusBarBeVisible, - headsUpNotificationInteractor.statusBarHeadsUpState, + headsUpNotificationInteractor.statusBarHeadsUpStatus, homeStatusBarInteractor.visibilityViaDisableFlags, - ) { shouldStatusBarBeVisible, headsUpState, visibilityViaDisableFlags -> - val hideClockForHeadsUp = headsUpState == PinnedStatus.PinnedBySystem + ) { shouldStatusBarBeVisible, headsUpStatus, visibilityViaDisableFlags -> + val hideClockForHeadsUp = headsUpStatus == PinnedStatus.PinnedBySystem val showClock = shouldStatusBarBeVisible && visibilityViaDisableFlags.isClockAllowed && @@ -356,6 +374,10 @@ constructor( override val iconBlockList: Flow<List<String>> = homeStatusBarIconBlockListInteractor.iconBlockList + override val contentArea: Flow<Rect> = + statusBarContentInsetsViewModelStore.forDisplay(thisDisplayId)?.contentArea + ?: flowOf(Rect(0, 0, 0, 0)) + @View.Visibility private fun Boolean.toVisibleOrGone(): Int { return if (this) View.VISIBLE else View.GONE @@ -364,6 +386,13 @@ constructor( // Similar to the above, but uses INVISIBLE in place of GONE @View.Visibility private fun Boolean.toVisibleOrInvisible(): Int = if (this) View.VISIBLE else View.INVISIBLE + + /** Inject this to create the display-dependent view model */ + @AssistedFactory + interface HomeStatusBarViewModelFactoryImpl : + HomeStatusBarViewModel.HomeStatusBarViewModelFactory { + override fun create(displayId: Int): HomeStatusBarViewModelImpl + } } /** Lookup the color for a given view in the status bar */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModel.kt index 7ae74c3bfb65..0b83c4e3dea3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/StatusBarOperatorNameViewModel.kt @@ -22,6 +22,7 @@ import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf /** * View model for the operator name (aka carrier name) of the carrier for the default data @@ -34,6 +35,10 @@ class StatusBarOperatorNameViewModel constructor(mobileIconsInteractor: MobileIconsInteractor) { val operatorName: Flow<String?> = mobileIconsInteractor.defaultDataSubId.flatMapLatest { - mobileIconsInteractor.getMobileConnectionInteractorForSubId(it).carrierName + if (it == null) { + flowOf(null) + } else { + mobileIconsInteractor.getMobileConnectionInteractorForSubId(it).carrierName + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt index 56c9e9abbc36..cb26679434ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt @@ -41,6 +41,7 @@ import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.Button +import com.android.systemui.Flags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.shared.system.ActivityManagerWrapper @@ -52,6 +53,7 @@ import com.android.systemui.statusbar.SmartReplyController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions @@ -400,6 +402,15 @@ constructor( .apply { text = action.title + if (Flags.notificationMagicActionsTreatment()) { + if ( + smartActions.fromAssistant && + action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false) + ) { + background = MagicActionBackgroundDrawable(parent.context) + } + } + // We received the Icon from the application - so use the Context of the application // to // reference icon resources. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt new file mode 100644 index 000000000000..8e81d78d60f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/StatusBarUiLayerModule.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 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.ui + +import com.android.systemui.statusbar.layout.ui.viewmodel.StatusBarContentInsetsViewModelStoreModule +import dagger.Module + +@Module(includes = [StatusBarContentInsetsViewModelStoreModule::class]) +object StatusBarUiLayerModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt index 6175ea190697..a98a9e0c16d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModel.kt @@ -60,7 +60,7 @@ constructor( private val showingHeadsUpStatusBar: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { - headsUpNotificationInteractor.statusBarHeadsUpState.map { it.isPinned } + headsUpNotificationInteractor.statusBarHeadsUpStatus.map { it.isPinned } } else { flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index 66a900bd72d8..c8a58400069e 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -218,9 +218,11 @@ private fun TutorialButton( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { + // contentDescription is set to null because the icon is decorative and we don't want to + // repeat the text twice Icon( imageVector = icon, - contentDescription = text, + contentDescription = null, modifier = Modifier.width(30.dp).height(30.dp), tint = iconColor, ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt index 3b0c8a6b46f8..b52c1075e9f0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -54,9 +55,10 @@ constructor( callbackFlow { val producer = VolumeDialogEventModelProducer(this) volumeDialogController.addCallback(producer, bgHandler) + send(VolumeDialogEventModel.SubscribedToEvents) awaitClose { volumeDialogController.removeCallback(producer) } } - .buffer(BUFFER_CAPACITY) + .buffer(capacity = BUFFER_CAPACITY, onBufferOverflow = BufferOverflow.DROP_OLDEST) .shareIn(replay = 0, scope = coroutineScope, started = SharingStarted.WhileSubscribed()) private class VolumeDialogEventModelProducer( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt index 51e79242daaf..26d2414acec1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart /** * Exposes [VolumeDialogController.getState] in the [volumeDialogState]. @@ -65,12 +64,14 @@ constructor( is VolumeDialogEventModel.ShowSafetyWarning -> { setSafetyWarning(VolumeDialogSafetyWarningModel.Visible(event.flags)) } + is VolumeDialogEventModel.SubscribedToEvents -> { + volumeDialogController.getState() + } else -> { // do nothing } } } - .onStart { volumeDialogController.getState() } .launchIn(coroutineScope) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt index 80e423838251..9793d2be6b98 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogEventModel.kt @@ -52,4 +52,11 @@ sealed interface VolumeDialogEventModel { VolumeDialogEventModel data object VolumeChangedFromKey : VolumeDialogEventModel + + /** + * Signals that the + * [com.android.systemui.volume.dialog.domain.interactor.VolumeDialogCallbacksInteractor] is + * ready to process the events. + */ + data object SubscribedToEvents : VolumeDialogEventModel } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index e8d19dd5e0e4..96630ca36b97 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -51,6 +51,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.launch private const val CLOSE_DRAWER_DELAY = 300L +// Ensure roundness and color of button is updated when progress is changed by a minimum fraction. +private const val BUTTON_MIN_VISIBLE_CHANGE = 0.05F @OptIn(ExperimentalCoroutinesApi::class) @VolumeDialogScope @@ -58,12 +60,12 @@ class VolumeDialogRingerViewBinder @Inject constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { private val roundnessSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 800F dampingRatio = 0.6F } private val colorSpringForce = - SpringForce(0F).apply { + SpringForce(1F).apply { stiffness = 3800F dampingRatio = 1F } @@ -257,30 +259,35 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { // We only need to execute on roundness animation end and volume dialog background // progress update once because these changes should be applied once on volume dialog // background and ringer drawer views. - val selectedCornerRadius = (selectedButton.background as GradientDrawable).cornerRadius - if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { - selectedButton.animateTo( - selectedButtonUiModel, - if (uiModel.currentButtonIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } - val unselectedCornerRadius = - (unselectedButton.background as GradientDrawable).cornerRadius - if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { - unselectedButton.animateTo( - unselectedButtonUiModel, - if (previousIndex == count - 1) { - onProgressChanged - } else { - { _, _ -> } - }, - ) - } coroutineScope { + val selectedCornerRadius = + (selectedButton.background as GradientDrawable).cornerRadius + if (selectedCornerRadius.toInt() != selectedButtonUiModel.cornerRadius) { + launch { + selectedButton.animateTo( + selectedButtonUiModel, + if (uiModel.currentButtonIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } + val unselectedCornerRadius = + (unselectedButton.background as GradientDrawable).cornerRadius + if (unselectedCornerRadius.toInt() != unselectedButtonUiModel.cornerRadius) { + launch { + unselectedButton.animateTo( + unselectedButtonUiModel, + if (previousIndex == count - 1) { + onProgressChanged + } else { + { _, _ -> } + }, + ) + } + } launch { delay(CLOSE_DRAWER_DELAY) bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) @@ -383,11 +390,14 @@ constructor(private val viewModel: VolumeDialogRingerDrawerViewModel) { onProgressChanged: (Float, Boolean) -> Unit = { _, _ -> }, ) { val roundnessAnimation = - SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce) - val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce) + SpringAnimation(FloatValueHolder(0F), 1F).setSpring(roundnessSpringForce) + val colorAnimation = SpringAnimation(FloatValueHolder(0F), 1F).setSpring(colorSpringForce) val radius = (background as GradientDrawable).cornerRadius val cornerRadiusDiff = ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius + + roundnessAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE + colorAnimation.minimumVisibleChange = BUTTON_MIN_VISIBLE_CHANGE coroutineScope { launch { colorAnimation.suspendAnimate { value -> diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt index 3988acbea7c2..b86252ddd1b7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt @@ -47,7 +47,7 @@ constructor( ) { val isDisabledByZenMode: Flow<Boolean> = - if (sliderType is VolumeDialogSliderType.Stream) { + if (zenModeInteractor.canBeBlockedByZenMode(sliderType)) { zenModeInteractor.activeModesBlockingStream(AudioStream(sliderType.audioStream)).map { it.mainMode != null } @@ -75,3 +75,8 @@ constructor( } } } + +private fun ZenModeInteractor.canBeBlockedByZenMode(sliderType: VolumeDialogSliderType): Boolean { + return sliderType is VolumeDialogSliderType.Stream && + canBeBlockedByZenMode(AudioStream(sliderType.audioStream)) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt index daf4c8275d20..71fe22ba4b01 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt @@ -26,6 +26,7 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -49,10 +50,10 @@ constructor( isRoutedToBluetooth: Boolean, ): Flow<Drawable> { return combine( - zenModeInteractor.activeModesBlockingStream(AudioStream(stream)), + zenModeInteractor.activeModesBlockingStream(stream), ringerModeForStream(stream), ) { activeModesBlockingStream, ringerMode -> - if (activeModesBlockingStream.mainMode?.icon != null) { + if (activeModesBlockingStream?.mainMode?.icon != null) { return@combine activeModesBlockingStream.mainMode.icon.drawable } else { context.getDrawable( @@ -141,3 +142,16 @@ constructor( } } } + +private fun ZenModeInteractor.activeModesBlockingStream(stream: Int): Flow<ActiveZenModes?> { + return if (AudioStream.supportedStreamTypes.contains(stream)) { + val audioStream = AudioStream(stream) + if (canBeBlockedByZenMode(audioStream)) { + activeModesBlockingStream(audioStream) + } else { + flowOf(null) + } + } else { + flowOf(null) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 46d7d5f680ce..428dc6ecb5b6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -80,7 +80,6 @@ constructor( MutableStateFlow(WindowInsets.Builder().build()) // Root view of the Volume Dialog. val root: MotionLayout = dialog.requireViewById(R.id.volume_dialog_root) - root.alpha = 0f animateVisibility(root, dialog, viewModel.dialogVisibilityModel) diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 4abbbacd800b..bac2c47f51c7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -24,13 +24,15 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE +import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING @@ -97,8 +99,9 @@ import org.mockito.kotlin.clearInvocations class ClockEventControllerTest : SysuiTestCase() { private val kosmos = testKosmos() - private val zenModeRepository = kosmos.fakeZenModeRepository private val testScope = kosmos.testScope + private val zenModeRepository by lazy { kosmos.fakeZenModeRepository } + private val zenModeInteractor by lazy { kosmos.zenModeInteractor } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -106,7 +109,6 @@ class ClockEventControllerTest : SysuiTestCase() { private lateinit var repository: FakeKeyguardRepository private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG)) private lateinit var underTest: ClockEventController - private lateinit var dndModeId: String @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var batteryController: BatteryController @@ -156,17 +158,12 @@ class ClockEventControllerTest : SysuiTestCase() { whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null)) whenever(userTracker.userId).thenReturn(1) - dndModeId = MANUAL_DND_INACTIVE.id - zenModeRepository.addMode(MANUAL_DND_INACTIVE) + repository = kosmos.fakeKeyguardRepository - repository = FakeKeyguardRepository() - - val withDeps = KeyguardInteractorFactory.create(repository = repository) - - withDeps.featureFlags.apply { set(Flags.REGION_SAMPLING, false) } + kosmos.fakeFeatureFlagsClassic.set(Flags.REGION_SAMPLING, false) underTest = ClockEventController( - withDeps.keyguardInteractor, + kosmos.keyguardInteractor, keyguardTransitionInteractor, broadcastDispatcher, batteryController, @@ -177,9 +174,9 @@ class ClockEventControllerTest : SysuiTestCase() { mainExecutor, bgExecutor, clockBuffers, - withDeps.featureFlags, + kosmos.fakeFeatureFlagsClassic, zenModeController, - kosmos.zenModeInteractor, + zenModeInteractor, userTracker, ) underTest.clock = clock @@ -504,7 +501,7 @@ class ClockEventControllerTest : SysuiTestCase() { runCurrent() clearInvocations(events) - zenModeRepository.activateMode(dndModeId) + zenModeRepository.activateMode(MANUAL_DND) runCurrent() verify(events) @@ -512,7 +509,7 @@ class ClockEventControllerTest : SysuiTestCase() { eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name)) ) - zenModeRepository.deactivateMode(dndModeId) + zenModeRepository.deactivateMode(MANUAL_DND) runCurrent() verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name))) diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java index 9d9fb9c23a73..6ad2128759a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java @@ -30,9 +30,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.pm.ActivityInfo; @@ -51,11 +48,8 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Interpolator; import android.window.InputTransferToken; -import androidx.annotation.NonNull; import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; @@ -80,19 +74,17 @@ import java.util.function.Supplier; @RunWith(AndroidTestingRunner.class) @FlakyTest(bugId = 385115361) public class FullscreenMagnificationControllerTest extends SysuiTestCase { - private static final long ANIMATION_DURATION_MS = 100L; private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER; - private static final long ANIMATION_TIMEOUT_MS = - 5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER; private static final String UNIQUE_DISPLAY_ID_PRIMARY = "000"; private static final String UNIQUE_DISPLAY_ID_SECONDARY = "111"; private static final int CORNER_RADIUS_PRIMARY = 10; private static final int CORNER_RADIUS_SECONDARY = 20; + private static final int DISABLED = 0; + private static final int ENABLED = 3; private FullscreenMagnificationController mFullscreenMagnificationController; private SurfaceControlViewHost mSurfaceControlViewHost; - private ValueAnimator mShowHideBorderAnimator; private SurfaceControl.Transaction mTransaction; private TestableWindowManager mWindowManager; @Mock @@ -136,7 +128,6 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mTransaction = new SurfaceControl.Transaction(); - mShowHideBorderAnimator = spy(newNullTargetObjectAnimator()); mFullscreenMagnificationController = new FullscreenMagnificationController( mContext, mContext.getMainThreadHandler(), @@ -146,141 +137,68 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { mContext.getSystemService(WindowManager.class), mIWindowManager, scvhSupplier, - mTransaction, - mShowHideBorderAnimator); + mTransaction); } @After public void tearDown() { - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false)); + getInstrumentation().runOnMainSync(() -> + mFullscreenMagnificationController.cleanUpBorder()); + } + + @Test + public void createShowTargetAnimator_runAnimator_alphaIsEqualToOne() { + View view = new View(mContext); + view.setAlpha(0f); + ValueAnimator animator = mFullscreenMagnificationController.createShowTargetAnimator(view); + animator.end(); + assertThat(view.getAlpha()).isEqualTo(1f); + } + + @Test + public void createHideTargetAnimator_runAnimator_alphaIsEqualToZero() { + View view = new View(mContext); + view.setAlpha(1f); + ValueAnimator animator = mFullscreenMagnificationController.createHideTargetAnimator(view); + animator.end(); + assertThat(view.getAlpha()).isEqualTo(0f); } @Test - public void enableFullscreenMagnification_visibleBorder() + public void enableFullscreenMagnification_stateEnabled() throws InterruptedException, RemoteException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).start(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); + + assertThat(mFullscreenMagnificationController.getState()).isEqualTo(ENABLED); verify(mIWindowManager) .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY)); - assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue(); } @Test - public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh() + public void disableFullscreenMagnification_stateDisabled() throws InterruptedException, RemoteException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch enableAnimationEndLatch = new CountDownLatch(1); - CountDownLatch disableAnimationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - if (isReverse) { - disableAnimationEndLatch.countDown(); - } else { - enableAnimationEndLatch.countDown(); - } - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for enabling animation to be finished") - .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).start(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); - getInstrumentation().runOnMainSync(() -> - // Disable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(false)); + getInstrumentation().runOnMainSync(() -> { + // Disable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(false); + }); + waitForIdleSync(); + assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull(); + mFullscreenMagnificationController.mShowHideBorderAnimator.end(); + waitForIdleSync(); - assertWithMessage("Failed to wait for disabling animation to be finished") - .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).reverse(); + assertThat(mFullscreenMagnificationController.getState()).isEqualTo(DISABLED); verify(mSurfaceControlViewHost).release(); verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class)); } @Test - public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator() - throws InterruptedException { - // Simulate the hiding border animation is running - when(mShowHideBorderAnimator.isRunning()).thenReturn(true); - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - - getInstrumentation().runOnMainSync( - () -> mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - verify(mShowHideBorderAnimator).reverse(); - } - - @Test public void onScreenSizeChanged_activated_borderChangedToExpectedSize() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); + final Rect testWindowBounds = new Rect( mWindowManager.getCurrentWindowMetrics().getBounds()); testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, @@ -304,29 +222,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { @Test public void enableFullscreenMagnification_applyPrimaryCornerRadius() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); - - // Verify the initial corner radius is applied GradientDrawable backgroundDrawable = (GradientDrawable) mSurfaceControlViewHost.getView().getBackground(); assertThat(backgroundDrawable.getCornerRadius()).isEqualTo(CORNER_RADIUS_PRIMARY); @@ -334,28 +231,8 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { @EnableFlags(Flags.FLAG_UPDATE_CORNER_RADIUS_ON_DISPLAY_CHANGED) @Test - public void onDisplayChanged_updateCornerRadiusToSecondary() throws InterruptedException { - CountDownLatch transactionCommittedLatch = new CountDownLatch(1); - CountDownLatch animationEndLatch = new CountDownLatch(1); - mTransaction.addTransactionCommittedListener( - Runnable::run, transactionCommittedLatch::countDown); - mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animationEndLatch.countDown(); - } - }); - - getInstrumentation().runOnMainSync(() -> - //Enable fullscreen magnification - mFullscreenMagnificationController - .onFullscreenMagnificationActivationChanged(true)); - assertWithMessage("Failed to wait for transaction committed") - .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) - .isTrue(); - assertWithMessage("Failed to wait for animation to be finished") - .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) - .isTrue(); + public void onDisplayChanged_applyCornerRadiusToBorder() throws InterruptedException { + enableFullscreenMagnificationAndWaitForTransactionAndAnimation(); ArgumentCaptor<DisplayManager.DisplayListener> displayListenerCaptor = ArgumentCaptor.forClass(DisplayManager.DisplayListener.class); @@ -372,22 +249,34 @@ public class FullscreenMagnificationControllerTest extends SysuiTestCase { .addOverride( com.android.internal.R.dimen.rounded_corner_radius, CORNER_RADIUS_SECONDARY); + getInstrumentation().runOnMainSync(() -> displayListenerCaptor.getValue().onDisplayChanged(Display.DEFAULT_DISPLAY)); waitForIdleSync(); + // Verify the corner radius is updated GradientDrawable backgroundDrawable2 = (GradientDrawable) mSurfaceControlViewHost.getView().getBackground(); assertThat(backgroundDrawable2.getCornerRadius()).isEqualTo(CORNER_RADIUS_SECONDARY); } + private void enableFullscreenMagnificationAndWaitForTransactionAndAnimation() + throws InterruptedException { + CountDownLatch transactionCommittedLatch = new CountDownLatch(1); + mTransaction.addTransactionCommittedListener( + Runnable::run, transactionCommittedLatch::countDown); + + getInstrumentation().runOnMainSync(() -> + //Enable fullscreen magnification + mFullscreenMagnificationController + .onFullscreenMagnificationActivationChanged(true)); - private ValueAnimator newNullTargetObjectAnimator() { - final ValueAnimator animator = - ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); - Interpolator interpolator = new DecelerateInterpolator(2.5f); - animator.setInterpolator(interpolator); - animator.setDuration(ANIMATION_DURATION_MS); - return animator; + assertWithMessage("Failed to wait for transaction committed") + .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) + .isTrue(); + waitForIdleSync(); + assertThat(mFullscreenMagnificationController.mShowHideBorderAnimator).isNotNull(); + mFullscreenMagnificationController.mShowHideBorderAnimator.end(); + waitForIdleSync(); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 856c37934251..9f6ad56335d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -82,7 +82,7 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final SecureSettings secureSettings = TestUtils.mockSecureSettings(); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(mContext); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, secureSettings, mHearingAidDeviceManager); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 33cfb3890e71..1500340c9d89 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -144,7 +144,7 @@ public class MenuViewLayerTest extends SysuiTestCase { private HearingAidDeviceManager mHearingAidDeviceManager; @Mock private PackageManager mMockPackageManager; - private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(mContext); private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt index 37eb148a5ea7..fd751d9cc7c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -466,6 +466,28 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { assertNull(runner.delegate) } + @Test + fun concurrentListenerModification_doesNotThrow() { + // Need a second listener to trigger the concurrent modification. + activityTransitionAnimator.addListener(object : ActivityTransitionAnimator.Listener {}) + `when`(listener.onTransitionAnimationStart()).thenAnswer { + activityTransitionAnimator.removeListener(listener) + listener + } + + val runner = activityTransitionAnimator.createEphemeralRunner(controller) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + + waitForIdleSync() + verify(listener).onTransitionAnimationStart() + } + private fun controllerFactory( cookie: ActivityTransitionAnimator.TransitionCookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt index 5d622eaeb1aa..e61acc4e1d0b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -224,6 +225,30 @@ class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() { } } + @Test + fun testOnActionIconClick_audioSharingMediaDevice_stopBroadcast() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onActionIconClick(inAudioSharingMediaDeviceItem) {} + assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted) + .isEqualTo(false) + } + } + } + + @Test + fun testOnActionIconClick_availableAudioSharingMediaDevice_startBroadcast() { + with(kosmos) { + testScope.runTest { + bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true) + actionInteractorImpl.onActionIconClick(connectedAudioSharingMediaDeviceItem) {} + assertThat(bluetoothTileDialogAudioSharingRepository.audioSharingStarted) + .isEqualTo(true) + } + } + } + private companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 6bfd08025833..4396b0a42ae6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -32,6 +32,9 @@ import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.model.SysUiState import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeDialogContextInteractor @@ -43,9 +46,8 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -93,7 +95,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { private val fakeSystemClock = FakeSystemClock() - private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope private lateinit var icon: Pair<Drawable, String> @@ -104,9 +105,8 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Before fun setUp() { - scheduler = TestCoroutineScheduler() - dispatcher = UnconfinedTestDispatcher(scheduler) - testScope = TestScope(dispatcher) + dispatcher = kosmos.testDispatcher + testScope = kosmos.testScope whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState) @@ -124,23 +124,19 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { kosmos.shadeDialogContextInteractor, ) - whenever( - sysuiDialogFactory.create( - any(SystemUIDialog.Delegate::class.java), - any() - ) - ).thenAnswer { - SystemUIDialog( - mContext, - 0, - SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, - dialogManager, - sysuiState, - fakeBroadcastDispatcher, - dialogTransitionAnimator, - it.getArgument(0), - ) - } + whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any())) + .thenAnswer { + SystemUIDialog( + mContext, + 0, + SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogTransitionAnimator, + it.getArgument(0), + ) + } icon = Pair(drawable, DEVICE_NAME) deviceItem = @@ -194,20 +190,29 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Test fun testDeviceItemViewHolder_cachedDeviceNotBusy() { - deviceItem.isEnabled = true - - val view = - LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) - val viewHolder = - mBluetoothTileDialogDelegate - .Adapter(bluetoothTileDialogCallback) - .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem, bluetoothTileDialogCallback) - val container = view.requireViewById<View>(R.id.bluetooth_device_row) - - assertThat(container).isNotNull() - assertThat(container.isEnabled).isTrue() - assertThat(container.hasOnClickListeners()).isTrue() + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val container = view.requireViewById<View>(R.id.bluetooth_device_row) + + assertThat(container).isNotNull() + assertThat(container.isEnabled).isTrue() + assertThat(container.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) + runCurrent() + container.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW) + assertThat(it.clickedView).isEqualTo(container) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } } @Test @@ -229,9 +234,9 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { sysuiDialogFactory, kosmos.shadeDialogContextInteractor, ) - .Adapter(bluetoothTileDialogCallback) + .Adapter() .DeviceItemViewHolder(view) - viewHolder.bind(deviceItem, bluetoothTileDialogCallback) + viewHolder.bind(deviceItem) val container = view.requireViewById<View>(R.id.bluetooth_device_row) assertThat(container).isNotNull() @@ -240,6 +245,32 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { } @Test + fun testDeviceItemViewHolder_clickActionIcon() { + testScope.runTest { + deviceItem.isEnabled = true + + val view = + LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) + val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view) + viewHolder.bind(deviceItem) + val actionIconView = view.requireViewById<View>(R.id.gear_icon) + + assertThat(actionIconView).isNotNull() + assertThat(actionIconView.hasOnClickListeners()).isTrue() + val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick) + runCurrent() + actionIconView.performClick() + runCurrent() + assertThat(value).isNotNull() + value?.let { + assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON) + assertThat(it.clickedView).isEqualTo(actionIconView) + assertThat(it.deviceItem).isEqualTo(deviceItem) + } + } + } + + @Test fun testOnDeviceUpdated_hideSeeAll_showPairNew() { testScope.runTest { val dialog = mBluetoothTileDialogDelegate.createDialog() diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt index 5bf15137b834..0aa5199cb20e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt @@ -118,6 +118,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { .isEqualTo(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE) assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) + assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_add) assertThat(deviceItem.isActive).isFalse() assertThat(deviceItem.connectionSummary) .isEqualTo( @@ -292,6 +293,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { assertThat(deviceItem.cachedBluetoothDevice).isEqualTo(cachedDevice) assertThat(deviceItem.deviceName).isEqualTo(DEVICE_NAME) assertThat(deviceItem.connectionSummary).isEqualTo(CONNECTION_SUMMARY) + assertThat(deviceItem.actionIconRes).isEqualTo(R.drawable.ic_settings_24dp) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt index a3c518128b47..f31d49094ac4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DataStoreCoordinatorTest.kt @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener -import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -48,7 +47,6 @@ class DataStoreCoordinatorTest : SysuiTestCase() { private val pipeline: NotifPipeline = mock() private val notifLiveDataStoreImpl: NotifLiveDataStoreImpl = mock() - private val stackController: NotifStackController = mock() private val section: NotifSection = mock() @Before @@ -63,7 +61,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { @Test fun testUpdateDataStore_withOneEntry() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf(entry))) verifyNoMoreInteractions(notifLiveDataStoreImpl) } @@ -86,8 +84,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { .setSection(section) .build(), notificationEntry("baz", 1), - ), - stackController, + ) ) val list: List<NotificationEntry> = withArgCaptor { verify(notifLiveDataStoreImpl).setActiveNotifList(capture()) @@ -111,7 +108,7 @@ class DataStoreCoordinatorTest : SysuiTestCase() { @Test fun testUpdateDataStore_withZeroEntries_whenNewPipelineEnabled() { - afterRenderListListener.onAfterRenderList(listOf(), stackController) + afterRenderListListener.onAfterRenderList(listOf()) verify(notifLiveDataStoreImpl).setActiveNotifList(eq(listOf())) verifyNoMoreInteractions(notifLiveDataStoreImpl) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 77bac59b9dcd..97e99b95f80e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -28,8 +28,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderListListener import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl -import com.android.systemui.statusbar.notification.collection.render.NotifStackController -import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.data.model.NotifStats import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow @@ -43,7 +42,6 @@ import org.junit.runner.RunWith import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @SmallTest @@ -61,7 +59,6 @@ class StackCoordinatorTest : SysuiTestCase() { private val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = mock() - private val stackController: NotifStackController = mock() private val section: NotifSection = mock() private val row: ExpandableNotificationRow = mock() @@ -87,25 +84,23 @@ class StackCoordinatorTest : SysuiTestCase() { @Test fun testSetRenderedListOnInteractor() { - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(renderListInteractor).setRenderedList(eq(listOf(entry))) } @Test fun testSetNotificationStats_clearableAlerting() { whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = true, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test @@ -113,35 +108,31 @@ class StackCoordinatorTest : SysuiTestCase() { fun testSetNotificationStats_isSensitiveStateActive_nonClearableAlerting() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test fun testSetNotificationStats_clearableSilent() { whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = true, ) ) - verifyNoMoreInteractions(stackController) } @Test @@ -149,35 +140,31 @@ class StackCoordinatorTest : SysuiTestCase() { fun testSetNotificationStats_isSensitiveStateActive_nonClearableSilent() { whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) whenever(section.bucket).thenReturn(BUCKET_SILENT) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = false, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = true, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } @Test fun testSetNotificationStats_nonClearableRedacted() { entry.setSensitive(true, true) whenever(section.bucket).thenReturn(BUCKET_ALERTING) - afterRenderListListener.onAfterRenderList(listOf(entry), stackController) + afterRenderListListener.onAfterRenderList(listOf(entry)) verify(activeNotificationsInteractor) .setNotifStats( NotifStats( - 1, hasNonClearableAlertingNotifs = true, hasClearableAlertingNotifs = false, hasNonClearableSilentNotifs = false, hasClearableSilentNotifs = false, ) ) - verifyNoMoreInteractions(stackController) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt index 57a12df0cfee..c4ef4f978ff8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt @@ -35,7 +35,6 @@ import android.platform.test.annotations.EnableFlags import androidx.test.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any import com.android.systemui.statusbar.core.StatusBarConnectedDisplays @@ -112,20 +111,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) - fun testCreateIcons_chipNotifIconFlagDisabled_statusBarChipIconIsNull() { - val entry = - notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) - entry?.let { iconManager.createIcons(it) } - testScope.runCurrent() - - assertThat(entry?.icons?.statusBarChipIcon).isNull() - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagDisabled_statusBarChipIconIsNotNull() { + fun testCreateIcons_cdFlagDisabled_statusBarChipIconIsNotNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -135,8 +122,8 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) - fun testCreateIcons_chipNotifIconFlagEnabled_cdFlagEnabled_statusBarChipIconIsNull() { + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + fun testCreateIcons_cdFlagEnabled_statusBarChipIconIsNull() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = true) entry?.let { iconManager.createIcons(it) } @@ -217,7 +204,6 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testCreateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = @@ -233,7 +219,7 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testCreateIcons_cdFlagEnabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) @@ -248,7 +234,6 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testUpdateIcons_cdFlagDisabled_sensitiveImportantConversation() { val entry = @@ -266,7 +251,7 @@ class IconManagerTest : SysuiTestCase() { } @Test - @EnableFlags(FLAG_STATUS_BAR_CALL_CHIP_NOTIFICATION_ICON, StatusBarConnectedDisplays.FLAG_NAME) + @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) fun testUpdateIcons_cdFlagEnabled_sensitiveImportantConversation() { val entry = notificationEntry(hasShortcut = true, hasMessageSenderIcon = true, hasLargeIcon = false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java index 3a99328fa8ed..f9df6c79e140 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone.fragment; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.systemui.Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN; @@ -42,6 +41,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.view.View; +import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.keyguard.KeyguardUpdateMonitor; @@ -77,6 +77,8 @@ import com.android.systemui.statusbar.phone.ui.DarkIconManager; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewBinder; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeHomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel; +import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.HomeStatusBarViewModel.HomeStatusBarViewModelFactory; import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.StatusBarOperatorNameViewModel; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -493,160 +495,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void disable_noOngoingCall_chipHidden() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - - when(mOngoingCallController.hasOngoingCall()).thenReturn(false); - - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility()); - } - - @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void disable_hasOngoingCall_chipDisplayedAndNotificationIconsHidden() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility()); - assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility()); - } - - @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void disable_hasOngoingCallButNotificationIconsDisabled_chipHidden() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - - fragment.disable(DEFAULT_DISPLAY, - StatusBarManager.DISABLE_NOTIFICATION_ICONS, 0, false); - - assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility()); - } - - @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void disable_hasOngoingCallButAlsoHun_chipHidden() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - when(mHeadsUpAppearanceController.shouldHeadsUpStatusBarBeVisible()).thenReturn(true); - - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility()); - } - - @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void disable_ongoingCallEnded_chipHidden() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - - // Ongoing call started - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility()); - - // Ongoing call ended - when(mOngoingCallController.hasOngoingCall()).thenReturn(false); - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility()); - - // Ongoing call started - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility()); - } - - @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void disable_hasOngoingCall_hidesNotifsWithoutAnimation() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - // Enable animations for testing so that we can verify we still aren't animating - fragment.enableAnimationsForTesting(); - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - // Ongoing call started - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - fragment.disable(DEFAULT_DISPLAY, 0, 0, true); - - // Notification area is hidden without delay - assertEquals(0f, getNotificationAreaView().getAlpha(), 0.01); - assertEquals(View.INVISIBLE, getNotificationAreaView().getVisibility()); - } - - @Test - @DisableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, - StatusBarRootModernization.FLAG_NAME, - StatusBarChipsModernization.FLAG_NAME - }) - public void screenSharingChipsDisabled_ignoresNewCallback() { - CollapsedStatusBarFragment fragment = resumeAndGetFragment(); - - // WHEN there *is* an ongoing call via old callback - when(mOngoingCallController.hasOngoingCall()).thenReturn(true); - fragment.disable(DEFAULT_DISPLAY, 0, 0, true); - - // WHEN there's *no* ongoing activity via new callback - mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasPrimaryOngoingActivity= */ false, - /* hasSecondaryOngoingActivity= */ false, - /* shouldAnimate= */ false); - - // THEN the old callback value is used, so the view is shown - assertEquals(View.VISIBLE, getPrimaryOngoingActivityChipView().getVisibility()); - - // WHEN there's *no* ongoing call via old callback - when(mOngoingCallController.hasOngoingCall()).thenReturn(false); - fragment.disable(DEFAULT_DISPLAY, 0, 0, false); - - // WHEN there *are* ongoing activities via new callback - mCollapsedStatusBarViewBinder.getListener().onOngoingActivityStatusChanged( - /* hasPrimaryOngoingActivity= */ true, - /* hasSecondaryOngoingActivity= */ true, - /* shouldAnimate= */ false); - - // THEN the old callback value is used, so the views are hidden - assertEquals(View.GONE, getPrimaryOngoingActivityChipView().getVisibility()); - assertEquals(View.GONE, getSecondaryOngoingActivityChipView().getVisibility()); - } - - @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void noOngoingActivity_chipHidden() { resumeAndGetFragment(); @@ -664,7 +512,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void hasPrimaryOngoingActivity_primaryChipDisplayedAndNotificationIconsHidden() { resumeAndGetFragment(); @@ -680,7 +527,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { @Test @EnableFlags({ - FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME @@ -709,7 +555,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, @@ -727,7 +572,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void hasSecondaryOngoingActivity_flagOn_secondaryChipShownAndNotificationIconsHidden() { resumeAndGetFragment(); @@ -742,7 +587,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, @@ -763,7 +607,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void hasOngoingActivitiesButNotificationIconsDisabled_chipsHidden_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -781,7 +625,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, @@ -802,7 +645,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void hasOngoingActivitiesButAlsoHun_chipsHidden_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -820,7 +663,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, @@ -847,7 +689,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void primaryOngoingActivityEnded_chipHidden_notifsFlagOn() { resumeAndGetFragment(); @@ -870,7 +712,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void secondaryOngoingActivityEnded_chipHidden() { resumeAndGetFragment(); @@ -893,7 +735,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, @@ -916,7 +757,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) public void hasOngoingActivity_hidesNotifsWithoutAnimation_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); @@ -935,13 +776,12 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags(FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS) @DisableFlags({ StatusBarNotifChips.FLAG_NAME, StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME }) - public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOff() { + public void ignoresOngoingCallController_notifsFlagOff() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); // WHEN there *is* an ongoing call via old callback @@ -972,9 +812,9 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { } @Test - @EnableFlags({FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS, StatusBarNotifChips.FLAG_NAME}) + @EnableFlags({StatusBarNotifChips.FLAG_NAME}) @DisableFlags({StatusBarRootModernization.FLAG_NAME, StatusBarChipsModernization.FLAG_NAME}) - public void screenSharingChipsEnabled_ignoresOngoingCallController_notifsFlagOn() { + public void ignoresOngoingCallController_notifsFlagOn() { CollapsedStatusBarFragment fragment = resumeAndGetFragment(); // WHEN there *is* an ongoing call via old callback @@ -1268,6 +1108,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mock(StatusBarOperatorNameViewModel.class)); mCollapsedStatusBarViewBinder = new FakeHomeStatusBarViewBinder(); + HomeStatusBarViewModelFactory homeStatusBarViewModelFactory = + new HomeStatusBarViewModelFactory() { + @NonNull + @Override + public HomeStatusBarViewModel create(int displayId) { + return mCollapsedStatusBarViewModel; + } + }; + return new CollapsedStatusBarFragment( mStatusBarFragmentComponentFactory, mOngoingCallController, @@ -1275,7 +1124,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest { mShadeExpansionStateManager, mStatusBarIconController, mIconManagerFactory, - mCollapsedStatusBarViewModel, + homeStatusBarViewModelFactory, mCollapsedStatusBarViewBinder, mStatusBarHideIconsForBouncerManager, mKeyguardStateController, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index d7456dfb33c3..6c60f55a3904 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -829,7 +829,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.defaultDataSubId) - assertThat(latest).isEqualTo(INVALID_SUBSCRIPTION_ID) + assertThat(latest).isEqualTo(null) val intent2 = Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) @@ -856,6 +856,31 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun defaultDataSubId_filtersOutInvalidSubIds() = + testScope.runTest { + subscriptionManagerProxy.defaultDataSubId = INVALID_SUBSCRIPTION_ID + val latest by collectLastValue(underTest.defaultDataSubId) + + assertThat(latest).isNull() + } + + @Test + fun defaultDataSubId_filtersOutInvalidSubIds_fromValidToInvalid() = + testScope.runTest { + subscriptionManagerProxy.defaultDataSubId = 2 + val latest by collectLastValue(underTest.defaultDataSubId) + + assertThat(latest).isEqualTo(2) + + val intent = + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + assertThat(latest).isNull() + } + + @Test fun defaultDataSubId_fetchesCurrentOnRestart() = testScope.runTest { subscriptionManagerProxy.defaultDataSubId = 2 diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt index 25d1c377ecbd..7ed736158a53 100644 --- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt +++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt @@ -435,6 +435,8 @@ class FakeStatusBarService : IStatusBarService.Stub() { override fun unbundleNotification(key: String) {} + override fun rebundleNotification(key: String) {} + companion object { const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY const val SECONDARY_DISPLAY_ID = 2 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt index 5ac41ec6741c..f380789968f5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt @@ -23,11 +23,11 @@ import com.android.systemui.util.mockito.mock val Kosmos.mockActivityTransitionAnimatorController by Kosmos.Fixture { mock<ActivityTransitionAnimator.Controller>() } -val Kosmos.activityTransitionAnimator by +var Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator( // The main thread is checked in a bunch of places inside the different transitions // animators, so we have to pass the real main executor here. - mainExecutor = testCase.context.mainExecutor, + mainExecutor = testCase.context.mainExecutor ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt index a839f17aad82..c744eacfa3f4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt @@ -33,6 +33,9 @@ class FakeAudioSharingRepository : AudioSharingRepository { var sourceAdded: Boolean = false private set + var audioSharingStarted: Boolean = false + private set + private var profile: LocalBluetoothLeBroadcast? = null override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? @@ -50,7 +53,13 @@ class FakeAudioSharingRepository : AudioSharingRepository { override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} - override suspend fun startAudioSharing() {} + override suspend fun startAudioSharing() { + audioSharingStarted = true + } + + override suspend fun stopAudioSharing() { + audioSharingStarted = false + } fun setAudioSharingAvailable(available: Boolean) { mutableAvailable = available diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 2a7e3e903737..490b89bf6b13 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock @@ -34,6 +35,8 @@ val Kosmos.deviceEntryHapticsInteractor by DeviceEntryHapticsInteractor( biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + keyguardBypassInteractor = keyguardBypassInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt index 3fc60e339543..a64fc2413246 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayRepository.kt @@ -115,3 +115,6 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository { interface FakeDisplayRepositoryModule { @Binds fun bindFake(fake: FakeDisplayRepository): DisplayRepository } + +val DisplayRepository.fake + get() = this as FakeDisplayRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt index 3de809308702..ee21bdc0b4c2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt @@ -24,8 +24,6 @@ import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.util.mockito.mock @@ -55,7 +53,6 @@ object KeyguardInteractorFactory { fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor = mock(), fromOccludedTransitionInteractor: FromOccludedTransitionInteractor = mock(), fromAlternateBouncerTransitionInteractor: FromAlternateBouncerTransitionInteractor = mock(), - powerInteractor: PowerInteractor = PowerInteractorFactory.create().powerInteractor, testScope: CoroutineScope = TestScope(), ): WithDependencies { // Mock these until they are replaced by kosmos @@ -73,10 +70,8 @@ object KeyguardInteractorFactory { bouncerRepository = bouncerRepository, configurationRepository = configurationRepository, shadeRepository = shadeRepository, - powerInteractor = powerInteractor, KeyguardInteractor( repository = repository, - powerInteractor = powerInteractor, bouncerRepository = bouncerRepository, configurationInteractor = ConfigurationInteractorImpl(configurationRepository), shadeRepository = shadeRepository, @@ -99,7 +94,6 @@ object KeyguardInteractorFactory { val bouncerRepository: FakeKeyguardBouncerRepository, val configurationRepository: FakeConfigurationRepository, val shadeRepository: FakeShadeRepository, - val powerInteractor: PowerInteractor, val keyguardInteractor: KeyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt index f5f8ef75065f..869bae236d5c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorKosmos.kt @@ -21,7 +21,6 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope -import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.data.repository.shadeRepository @@ -29,7 +28,6 @@ val Kosmos.keyguardInteractor: KeyguardInteractor by Kosmos.Fixture { KeyguardInteractor( repository = keyguardRepository, - powerInteractor = powerInteractor, bouncerRepository = keyguardBouncerRepository, configurationInteractor = configurationInteractor, shadeRepository = shadeRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt index 15d00d9f6994..edc1cce326c3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/transitions/FakeBouncerTransition.kt @@ -20,4 +20,5 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeBouncerTransition : PrimaryBouncerTransition { override val windowBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) + override val notificationBlurRadius: MutableStateFlow<Float> = MutableStateFlow(0.0f) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index afe48214832f..439df543b9fb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -54,8 +54,8 @@ var Kosmos.brightnessWarningToast: BrightnessWarningToast by * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in * that Kosmos instance */ -fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) { - testScope.runTestWithSnapshots testBody@{ this@runTest.testBody() } +fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = let { kosmos -> + testScope.runTestWithSnapshots { kosmos.testBody() } } fun Kosmos.runCurrent() = testScope.runCurrent() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt index 43eb93e4dd53..9d73ae3f176f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt @@ -197,7 +197,7 @@ class FakeVolumeDialogController(private val audioManager: AudioManager) : Volum } } -private inline fun CopyOnWriteArraySet<VolumeDialogController.Callbacks>.sendEvent( +private inline fun Collection<VolumeDialogController.Callbacks>.sendEvent( event: (callback: VolumeDialogController.Callbacks) -> Unit ) { for (callback in this) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt index 8ae1332c387a..639bb691f455 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelKosmos.kt @@ -18,13 +18,18 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.activityStarter import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel val Kosmos.editModeButtonViewModelFactory by Kosmos.Fixture { object : EditModeButtonViewModel.Factory { override fun create(): EditModeButtonViewModel { - return EditModeButtonViewModel(editModeViewModel, falsingInteractor) + return EditModeButtonViewModel( + editModeViewModel, + falsingInteractor, + activityStarter, + ) } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt index 82b5f6332b23..72c75000ebf4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.scene.domain.startable import com.android.internal.logging.uiEventLogger +import com.android.systemui.animation.activityTransitionAnimator import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor @@ -85,5 +86,6 @@ val Kosmos.sceneContainerStartable by Fixture { vibratorHelper = vibratorHelper, msdlPlayer = msdlPlayer, disabledContentInteractor = disabledContentInteractor, + activityTransitionAnimator = activityTransitionAnimator, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt index 69e215dcba6a..90897faaa6f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProviderKosmos.kt @@ -16,13 +16,28 @@ package com.android.systemui.statusbar.layout +import android.content.applicationContext +import com.android.systemui.SysUICutoutProvider +import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.fake import org.mockito.kotlin.mock val Kosmos.mockStatusBarContentInsetsProvider by Kosmos.Fixture { mock<StatusBarContentInsetsProvider>() } -var Kosmos.statusBarContentInsetsProvider by Kosmos.Fixture { mockStatusBarContentInsetsProvider } +val Kosmos.statusBarContentInsetsProvider by + Kosmos.Fixture { + StatusBarContentInsetsProviderImpl( + applicationContext, + configurationController.fake, + dumpManager, + commandRegistry, + mock<SysUICutoutProvider>(), + ) + } val Kosmos.fakeStatusBarContentInsetsProviderFactory by Kosmos.Fixture { FakeStatusBarContentInsetsProviderFactory() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt new file mode 100644 index 000000000000..889d469489f2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/layout/ui/viewmodel/StatusBarContentInsetsViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 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.layout.ui.viewmodel + +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.data.repository.multiDisplayStatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.layout.statusBarContentInsetsProvider + +val Kosmos.statusBarContentInsetsViewModel by + Kosmos.Fixture { StatusBarContentInsetsViewModel(statusBarContentInsetsProvider) } + +val Kosmos.multiDisplayStatusBarContentInsetsViewModelStore by + Kosmos.Fixture { + MultiDisplayStatusBarContentInsetsViewModelStore( + applicationCoroutineScope, + displayRepository, + multiDisplayStatusBarContentInsetsProviderStore, + ) + } 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 d1619b7959f2..60e092c9709b 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 @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor +import com.android.systemui.window.ui.viewmodel.fakeBouncerTransitions import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) @@ -99,6 +100,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel, primaryBouncerToLockscreenTransitionViewModel = primaryBouncerToLockscreenTransitionViewModel, + primaryBouncerTransitions = fakeBouncerTransitions, aodBurnInViewModel = aodBurnInViewModel, communalSceneInteractor = communalSceneInteractor, headsUpNotificationInteractor = { headsUpNotificationInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeCarrierConfigRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeCarrierConfigRepository.kt index adf6ca1d1710..eebfca79efe3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeCarrierConfigRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeCarrierConfigRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.os.PersistableBundle +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig class FakeCarrierConfigRepository : CarrierConfigRepository { @@ -24,8 +25,12 @@ class FakeCarrierConfigRepository : CarrierConfigRepository { val configsById = mutableMapOf<Int, SystemUiCarrierConfig>() - override fun getOrCreateConfigForSubId(subId: Int): SystemUiCarrierConfig = - configsById.getOrPut(subId) { SystemUiCarrierConfig(subId, createDefaultTestConfig()) } + override fun getOrCreateConfigForSubId(maybeSubId: Int?): SystemUiCarrierConfig { + val subId = maybeSubId ?: INVALID_SUBSCRIPTION_ID + return configsById.getOrPut(subId) { + SystemUiCarrierConfig(subId, createDefaultTestConfig()) + } + } } val CarrierConfigRepository.fake diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 3b8adb4a8307..352f6cf011e1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -55,7 +55,7 @@ class FakeMobileIconsInteractor( override val filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) - override val defaultDataSubId = MutableStateFlow(DEFAULT_DATA_SUB_ID) + override val defaultDataSubId: MutableStateFlow<Int?> = MutableStateFlow(DEFAULT_DATA_SUB_ID) private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt index b38a723f1fa7..db7e31bb2cb6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel +import android.content.testableContext import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos @@ -26,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.events.domain.interactor.systemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModel +import com.android.systemui.statusbar.layout.ui.viewmodel.multiDisplayStatusBarContentInsetsViewModelStore import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.domain.interactor.darkIconInteractor @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.shared.domain.interactor.homeStat var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by Kosmos.Fixture { HomeStatusBarViewModelImpl( + testableContext.displayId, homeStatusBarInteractor, homeStatusBarIconBlockListInteractor, lightsOutInteractor, @@ -51,6 +54,7 @@ var Kosmos.homeStatusBarViewModel: HomeStatusBarViewModel by ongoingActivityChipsViewModel, statusBarPopupChipsViewModel, systemStatusEventAnimationInteractor, + multiDisplayStatusBarContentInsetsViewModelStore, applicationCoroutineScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt index 282f5947636c..1e4701333857 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ConfigurationControllerKosmos.kt @@ -25,3 +25,6 @@ val Kosmos.fakeConfigurationController: FakeConfigurationController by Kosmos.Fixture { FakeConfigurationController() } val Kosmos.statusBarConfigurationController: StatusBarConfigurationController by Kosmos.Fixture { fakeConfigurationController } + +val ConfigurationController.fake + get() = this as FakeConfigurationController diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt index 1ba5ddbf0337..fc0c92e974f7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryKosmos.kt @@ -20,5 +20,5 @@ import com.android.settingslib.notification.data.repository.FakeZenModeRepositor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.zenModeRepository by Fixture { fakeZenModeRepository } +var Kosmos.zenModeRepository by Fixture { fakeZenModeRepository } val Kosmos.fakeZenModeRepository by Fixture { FakeZenModeRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index ed5322ed098e..db19d6ee9077 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -39,7 +39,7 @@ val Kosmos.localMediaRepositoryFactory by val Kosmos.mediaOutputActionsInteractor by Kosmos.Fixture { MediaOutputActionsInteractor(mediaOutputDialogManager) } -val Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() } +var Kosmos.mediaControllerRepository by Kosmos.Fixture { FakeMediaControllerRepository() } val Kosmos.mediaOutputInteractor by Kosmos.Fixture { MediaOutputInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt index 712ec41bbf2d..3f2b47948c1c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/AudioRepositoryKosmos.kt @@ -19,4 +19,4 @@ package com.android.systemui.volume.data.repository import com.android.systemui.kosmos.Kosmos val Kosmos.fakeAudioRepository by Kosmos.Fixture { FakeAudioRepository() } -val Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } +var Kosmos.audioRepository by Kosmos.Fixture { fakeAudioRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt new file mode 100644 index 000000000000..e2431934bc40 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/VolumeDialogKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.volume.dialog + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.dagger.volumeDialogComponentFactory +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor + +val Kosmos.volumeDialog by + Kosmos.Fixture { + VolumeDialog( + context = applicationContext, + visibilityInteractor = volumeDialogVisibilityInteractor, + componentFactory = volumeDialogComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt new file mode 100644 index 000000000000..73e5d8d40985 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/dagger/VolumeDialogComponentKosmos.kt @@ -0,0 +1,41 @@ +/* + * 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.volume.dialog.dagger + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent +import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory +import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder +import com.android.systemui.volume.dialog.ui.binder.volumeDialogViewBinder +import kotlinx.coroutines.CoroutineScope + +val Kosmos.volumeDialogComponentFactory by + Kosmos.Fixture { + object : VolumeDialogComponent.Factory { + override fun create(scope: CoroutineScope): VolumeDialogComponent = + volumeDialogComponent + } + } +val Kosmos.volumeDialogComponent by + Kosmos.Fixture { + object : VolumeDialogComponent { + override fun volumeDialogViewBinder(): VolumeDialogViewBinder = volumeDialogViewBinder + + override fun sliderComponentFactory(): VolumeDialogSliderComponent.Factory = + volumeDialogSliderComponentFactory + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt index 291dfc0430e2..3d5698b193e1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/data/repository/VolumeDialogVisibilityRepositoryKosmos.kt @@ -19,4 +19,4 @@ package com.android.systemui.volume.dialog.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.dialog.data.VolumeDialogVisibilityRepository -val Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() } +var Kosmos.volumeDialogVisibilityRepository by Kosmos.Fixture { VolumeDialogVisibilityRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt index db9c48d9be6f..8f122b57e9d4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt @@ -16,8 +16,6 @@ package com.android.systemui.volume.dialog.domain.interactor -import android.os.Handler -import android.os.looper import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController @@ -27,6 +25,6 @@ val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by VolumeDialogCallbacksInteractor( volumeDialogController = volumeDialogController, coroutineScope = applicationCoroutineScope, - bgHandler = Handler(looper), + bgHandler = null, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt new file mode 100644 index 000000000000..7cbdc3d9f6ee --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/VolumeDialogRingerViewBinderKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ringer + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.volumeDialogRingerDrawerViewModel + +val Kosmos.volumeDialogRingerViewBinder by + Kosmos.Fixture { VolumeDialogRingerViewBinder(volumeDialogRingerDrawerViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt index 44371b4615df..cf357b498621 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/data/repository/VolumeDialogRingerFeedbackRepositoryKosmos.kt @@ -20,5 +20,5 @@ import com.android.systemui.kosmos.Kosmos val Kosmos.fakeVolumeDialogRingerFeedbackRepository by Kosmos.Fixture { FakeVolumeDialogRingerFeedbackRepository() } -val Kosmos.volumeDialogRingerFeedbackRepository by +var Kosmos.volumeDialogRingerFeedbackRepository: VolumeDialogRingerFeedbackRepository by Kosmos.Fixture { fakeVolumeDialogRingerFeedbackRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt index a494d04ec741..4bebf8911613 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/domain/VolumeDialogRingerInteractorKosmos.kt @@ -21,7 +21,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.plugins.volumeDialogController import com.android.systemui.volume.data.repository.audioSystemRepository import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor -import com.android.systemui.volume.dialog.ringer.data.repository.fakeVolumeDialogRingerFeedbackRepository +import com.android.systemui.volume.dialog.ringer.data.repository.volumeDialogRingerFeedbackRepository val Kosmos.volumeDialogRingerInteractor by Kosmos.Fixture { @@ -30,6 +30,6 @@ val Kosmos.volumeDialogRingerInteractor by volumeDialogStateInteractor = volumeDialogStateInteractor, controller = volumeDialogController, audioSystemRepository = audioSystemRepository, - ringerFeedbackRepository = fakeVolumeDialogRingerFeedbackRepository, + ringerFeedbackRepository = volumeDialogRingerFeedbackRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt new file mode 100644 index 000000000000..26b8bca6344b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/domain/VolumeDialogSettingsButtonInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * 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.volume.dialog.settings.domain + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.policy.deviceProvisionedController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor + +val Kosmos.volumeDialogSettingsButtonInteractor by + Kosmos.Fixture { + VolumeDialogSettingsButtonInteractor( + applicationCoroutineScope, + deviceProvisionedController, + volumePanelGlobalStateInteractor, + volumeDialogVisibilityInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt new file mode 100644 index 000000000000..f9e128ddd810 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.settings.ui.binder + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.settings.ui.viewmodel.volumeDialogSettingsButtonViewModel + +val Kosmos.volumeDialogSettingsButtonViewBinder by + Kosmos.Fixture { VolumeDialogSettingsButtonViewBinder(volumeDialogSettingsButtonViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt new file mode 100644 index 000000000000..0ae3b037b50a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModelKosmos.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.settings.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.volume.dialog.settings.domain.volumeDialogSettingsButtonInteractor +import com.android.systemui.volume.mediaDeviceSessionInteractor +import com.android.systemui.volume.mediaOutputInteractor + +val Kosmos.volumeDialogSettingsButtonViewModel by + Kosmos.Fixture { + VolumeDialogSettingsButtonViewModel( + applicationContext, + testScope.testScheduler, + applicationCoroutineScope, + mediaOutputInteractor, + mediaDeviceSessionInteractor, + volumeDialogSettingsButtonInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt new file mode 100644 index 000000000000..cb38cc3576ad --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.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.volume.dialog.sliders.dagger + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.statusbar.policy.data.repository.zenModeRepository +import com.android.systemui.volume.data.repository.audioRepository +import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository +import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor + +private val Kosmos.mutableSliderComponentKosmoses: MutableMap<VolumeDialogSliderType, Kosmos> by + Kosmos.Fixture { mutableMapOf() } + +val Kosmos.volumeDialogSliderComponentFactory by + Kosmos.Fixture { + object : VolumeDialogSliderComponent.Factory { + override fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderComponent = + volumeDialogSliderComponent(sliderType) + } + } + +fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDialogSliderComponent { + return object : VolumeDialogSliderComponent { + + private val localKosmos + get() = + mutableSliderComponentKosmoses.getOrPut(type) { + Kosmos().also { + it.setupVolumeDialogSliderComponent(this@volumeDialogSliderComponent, type) + } + } + + override fun sliderViewBinder(): VolumeDialogSliderViewBinder = + localKosmos.volumeDialogSliderViewBinder + + override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder = + localKosmos.volumeDialogSliderHapticsViewBinder + + override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder = + localKosmos.volumeDialogOverscrollViewBinder + } +} + +private fun Kosmos.setupVolumeDialogSliderComponent( + parentKosmos: Kosmos, + type: VolumeDialogSliderType, +) { + volumeDialogSliderType = type + applicationContext = parentKosmos.applicationContext + testScope = parentKosmos.testScope + testDispatcher = parentKosmos.testDispatcher + + volumeDialogController = parentKosmos.volumeDialogController + mediaControllerInteractor = parentKosmos.mediaControllerInteractor + mediaControllerRepository = parentKosmos.mediaControllerRepository + zenModeRepository = parentKosmos.zenModeRepository + volumeDialogVisibilityRepository = parentKosmos.volumeDialogVisibilityRepository + audioRepository = parentKosmos.audioRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt new file mode 100644 index 000000000000..13d6ca9732d1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogOverscrollViewModel + +val Kosmos.volumeDialogOverscrollViewBinder by + Kosmos.Fixture { VolumeDialogOverscrollViewBinder(volumeDialogOverscrollViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt new file mode 100644 index 000000000000..d6845b1ff7e3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt @@ -0,0 +1,33 @@ +/* + * 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.volume.dialog.sliders.ui + +import com.android.systemui.haptics.msdl.msdlPlayer +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.time.systemClock +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel + +val Kosmos.volumeDialogSliderHapticsViewBinder by + Kosmos.Fixture { + VolumeDialogSliderHapticsViewBinder( + volumeDialogSliderInputEventsViewModel, + vibratorHelper, + msdlPlayer, + systemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt new file mode 100644 index 000000000000..c6db717e004f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.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.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderViewModel + +val Kosmos.volumeDialogSliderViewBinder by + Kosmos.Fixture { + VolumeDialogSliderViewBinder( + volumeDialogSliderViewModel, + volumeDialogSliderInputEventsViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt new file mode 100644 index 000000000000..83527d994e70 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSlidersViewModel + +val Kosmos.volumeDialogSlidersViewBinder by + Kosmos.Fixture { VolumeDialogSlidersViewBinder(volumeDialogSlidersViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt new file mode 100644 index 000000000000..fe2f3d806b6a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogOverscrollViewModelKosmos.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.sliders.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor + +val Kosmos.volumeDialogOverscrollViewModel by + Kosmos.Fixture { + VolumeDialogOverscrollViewModel(applicationContext, volumeDialogSliderInputEventsInteractor) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt new file mode 100644 index 000000000000..09f9f1c6362e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProviderKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor +import com.android.systemui.volume.domain.interactor.audioVolumeInteractor + +val Kosmos.volumeDialogSliderIconProvider by + Kosmos.Fixture { + VolumeDialogSliderIconProvider( + context = applicationContext, + audioVolumeInteractor = audioVolumeInteractor, + zenModeInteractor = zenModeInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.kt new file mode 100644 index 000000000000..2de0e8f76a4b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderInputEventsViewModelKosmos.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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInputEventsInteractor + +val Kosmos.volumeDialogSliderInputEventsViewModel by + Kosmos.Fixture { + VolumeDialogSliderInputEventsViewModel( + applicationCoroutineScope, + volumeDialogSliderInputEventsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt new file mode 100644 index 000000000000..63cd440a8633 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModelKosmos.kt @@ -0,0 +1,34 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.util.time.systemClock +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSliderInteractor + +val Kosmos.volumeDialogSliderViewModel by + Kosmos.Fixture { + VolumeDialogSliderViewModel( + interactor = volumeDialogSliderInteractor, + visibilityInteractor = volumeDialogVisibilityInteractor, + coroutineScope = applicationCoroutineScope, + volumeDialogSliderIconProvider = volumeDialogSliderIconProvider, + systemClock = systemClock, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt new file mode 100644 index 000000000000..5531f7608b69 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModelKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.volume.dialog.sliders.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.volume.dialog.sliders.dagger.volumeDialogSliderComponentFactory +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor + +val Kosmos.volumeDialogSlidersViewModel by + Kosmos.Fixture { + VolumeDialogSlidersViewModel( + applicationCoroutineScope, + volumeDialogSlidersInteractor, + volumeDialogSliderComponentFactory, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt new file mode 100644 index 000000000000..dc09e3233b1e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.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.volume.dialog.ui.binder + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder +import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSlidersViewBinder +import com.android.systemui.volume.dialog.ui.utils.jankListenerFactory +import com.android.systemui.volume.dialog.ui.viewmodel.volumeDialogViewModel +import com.android.systemui.volume.dialog.utils.volumeTracer + +val Kosmos.volumeDialogViewBinder by + Kosmos.Fixture { + VolumeDialogViewBinder( + applicationContext.resources, + volumeDialogViewModel, + jankListenerFactory, + volumeTracer, + volumeDialogRingerViewBinder, + volumeDialogSlidersViewBinder, + volumeDialogSettingsButtonViewBinder, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt new file mode 100644 index 000000000000..35ec5d3cc9af --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ui.utils + +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.jankListenerFactory by Kosmos.Fixture { JankListenerFactory(interactionJankMonitor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt new file mode 100644 index 000000000000..05ef462d4998 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModelKosmos.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.dialog.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.policy.configurationController +import com.android.systemui.statusbar.policy.devicePostureController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogVisibilityInteractor +import com.android.systemui.volume.dialog.sliders.domain.interactor.volumeDialogSlidersInteractor + +val Kosmos.volumeDialogViewModel by + Kosmos.Fixture { + VolumeDialogViewModel( + applicationContext, + volumeDialogVisibilityInteractor, + volumeDialogSlidersInteractor, + volumeDialogStateInteractor, + devicePostureController, + configurationController, + ) + } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt index bd2173cd2393..c9fae70353f1 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/BuildScope.kt @@ -100,7 +100,7 @@ interface BuildScope : HasNetwork, StateScope { * observation of new emissions. It will however *not* cancel any running effects from previous * emissions. To achieve this behavior, use [launchScope] or [asyncScope] to create a child * build scope: - * ``` kotlin + * ``` * val job = launchScope { * events.observe { x -> * launchEffect { longRunningEffect(x) } @@ -141,7 +141,7 @@ interface BuildScope : HasNetwork, StateScope { * * By default, [builder] is only running while the returned [Events] is being * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ``` kotlin + * ``` * events { ... }.apply { observe() } * ``` */ @@ -158,7 +158,7 @@ interface BuildScope : HasNetwork, StateScope { * * By default, [builder] is only running while the returned [Events] is being * [observed][observe]. If you want it to run at all times, simply add a no-op observer: - * ``` kotlin + * ``` * events { ... }.apply { observe() } * ``` * @@ -196,7 +196,7 @@ interface BuildScope : HasNetwork, StateScope { * outside of the current Kairos transaction; when [transform] returns, the returned value is * emitted from the result [Events] in a new transaction. * - * ``` kotlin + * ``` * fun <A, B> Events<A>.mapAsyncLatest(transform: suspend (A) -> B): Events<B> = * mapLatestBuild { a -> asyncEvent { transform(a) } }.flatten() * ``` @@ -571,7 +571,7 @@ interface BuildScope : HasNetwork, StateScope { /** * Shorthand for: - * ``` kotlin + * ``` * flow.toEvents().holdState(initialValue) * ``` */ @@ -579,7 +579,7 @@ interface BuildScope : HasNetwork, StateScope { /** * Shorthand for: - * ``` kotlin + * ``` * flow.scan(initialValue, operation).toEvents().holdState(initialValue) * ``` */ @@ -588,7 +588,7 @@ interface BuildScope : HasNetwork, StateScope { /** * Shorthand for: - * ``` kotlin + * ``` * flow.scan(initialValue) { a, f -> f(a) }.toEvents().holdState(initialValue) * ``` */ @@ -665,7 +665,7 @@ interface BuildScope : HasNetwork, StateScope { * be used to make further modifications to the Kairos network, and/or perform side-effects via * [effect]. * - * ``` kotlin + * ``` * fun <A> State<A>.observeBuild(block: BuildScope.(A) -> Unit = {}): Job = launchScope { * block(sample()) * changes.observeBuild(block) @@ -698,7 +698,7 @@ interface BuildScope : HasNetwork, StateScope { * outside of the current Kairos transaction; when it completes, the returned [Events] emits in a * new transaction. * - * ``` kotlin + * ``` * fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = * events { emit(block()) }.apply { observe() } * ``` @@ -719,7 +719,7 @@ fun <A> BuildScope.asyncEvent(block: suspend () -> A): Events<A> = * executed if this [BuildScope] is still active by that time. It can be deactivated due to a * -Latest combinator, for example. * - * ``` kotlin + * ``` * fun BuildScope.effect( * context: CoroutineContext = EmptyCoroutineContext, * block: EffectScope.() -> Unit, @@ -740,7 +740,7 @@ fun BuildScope.effect( * done because the current [BuildScope] might be deactivated within this transaction, perhaps due * to a -Latest combinator. If this happens, then the coroutine will never actually be started. * - * ``` kotlin + * ``` * fun BuildScope.launchEffect(block: suspend KairosScope.() -> Unit): Job = * effect { effectCoroutineScope.launch { block() } } * ``` @@ -757,7 +757,7 @@ fun BuildScope.launchEffect(block: suspend KairosCoroutineScope.() -> Unit): Job * to a -Latest combinator. If this happens, then the coroutine will never actually be started. * * Shorthand for: - * ``` kotlin + * ``` * fun <R> BuildScope.asyncEffect(block: suspend KairosScope.() -> R): Deferred<R> = * CompletableDeferred<R>.apply { * effect { effectCoroutineScope.launch { complete(block()) } } @@ -789,7 +789,7 @@ fun BuildScope.launchScope(block: BuildSpec<*>): Job = asyncScope(block).second * * By default, [builder] is only running while the returned [Events] is being * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: - * ``` kotlin + * ``` * events { ... }.apply { observe() } * ``` * @@ -813,7 +813,7 @@ fun <In, Out> BuildScope.coalescingEvents( * * By default, [builder] is only running while the returned [Events] is being * [observed][BuildScope.observe]. If you want it to run at all times, simply add a no-op observer: - * ``` kotlin + * ``` * events { ... }.apply { observe() } * ``` * diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt index 8f468c153743..1a13773d71bf 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Events.kt @@ -105,7 +105,7 @@ class EventsLoop<A> : Events<A>() { * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> Lazy<Events<A>>.defer() = deferredEvents { value } * ``` * @@ -122,7 +122,7 @@ class EventsLoop<A> : Events<A>() { * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> DeferredValue<Events<A>>.defer() = deferredEvents { get() } * ``` * @@ -160,7 +160,7 @@ fun <A, B> Events<A>.mapMaybe(transform: TransactionScope.(A) -> Maybe<B>): Even * Returns an [Events] that contains only the non-null results of applying [transform] to each value * of the original [Events]. * - * ``` kotlin + * ``` * fun <A> Events<A>.mapNotNull(transform: TransactionScope.(A) -> B?): Events<B> = * mapMaybe { if (it == null) absent else present(it) } * ``` @@ -201,7 +201,7 @@ fun <A, B> Events<A>.mapCheap(transform: TransactionScope.(A) -> B): Events<B> = * Returns an [Events] that invokes [action] before each value of the original [Events] is emitted. * Useful for logging and debugging. * - * ``` kotlin + * ``` * fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = * map { it.also { action(it) } } * ``` @@ -220,7 +220,7 @@ fun <A> Events<A>.onEach(action: TransactionScope.(A) -> Unit): Events<A> = map * Splits an [Events] of pairs into a pair of [Events], where each returned [Events] emits half of * the original. * - * ``` kotlin + * ``` * fun <A, B> Events<Pair<A, B>>.unzip(): Pair<Events<A>, Events<B>> { * val lefts = map { it.first } * val rights = map { it.second } diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt index 8ca5ac8652db..d412b849e441 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Filter.kt @@ -29,7 +29,7 @@ fun <A> Events<A>.filter(state: State<Boolean>): Events<A> = filter { state.samp /** * Returns an [Events] containing only values of the original [Events] that are not null. * - * ``` kotlin + * ``` * fun <A> Events<A?>.filterNotNull(): Events<A> = mapNotNull { it } * ``` * @@ -41,7 +41,7 @@ fun <A> Events<A?>.filterNotNull(): Events<A> = mapCheap { it.toMaybe() }.filter /** * Returns an [Events] containing only values of the original [Events] that are instances of [A]. * - * ``` kotlin + * ``` * inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = * mapNotNull { it as? A } * ``` @@ -55,7 +55,7 @@ inline fun <reified A> Events<*>.filterIsInstance(): Events<A> = /** * Returns an [Events] containing only values of the original [Events] that are present. * - * ``` kotlin + * ``` * fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = mapMaybe { it } * ``` * @@ -69,7 +69,7 @@ fun <A> Events<Maybe<A>>.filterPresent(): Events<A> = * Returns an [Events] containing only values of the original [Events] that satisfy the given * [predicate]. * - * ``` kotlin + * ``` * fun <A> Events<A>.filter(predicate: TransactionScope.(A) -> Boolean): Events<A> = * mapMaybe { if (predicate(it)) present(it) else absent } * ``` diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt index 45da34ac9ae6..27fc1b4cf54c 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/GroupBy.kt @@ -63,7 +63,7 @@ fun <K, A> Events<Map<K, A>>.groupByKey(numKeys: Int? = null): GroupedEvents<K, * downstream [Events]. The downstream [Events] are associated with a [key][K], which is derived * from each emission of the original [Events] via [extractKey]. * - * ``` kotlin + * ``` * fun <K, A> Events<A>.groupBy( * numKeys: Int? = null, * extractKey: TransactionScope.(A) -> K, @@ -108,7 +108,7 @@ class GroupedEvents<in K, out A> internal constructor(internal val impl: DemuxIm * Using this is equivalent to `upstream.filter(predicate) to upstream.filter { !predicate(it) }` * but is more efficient; specifically, [partition] will only invoke [predicate] once per element. * - * ``` kotlin + * ``` * fun <A> Events<A>.partition( * predicate: TransactionScope.(A) -> Boolean * ): Pair<Events<A>, Events<A>> = @@ -133,7 +133,7 @@ fun <A> Events<A>.partition( * [First]s and once for [Second]s, but is slightly more efficient; specifically, the * [filterIsInstance] check is only performed once per element. * - * ``` kotlin + * ``` * fun <A, B> Events<Either<A, B>>.partitionEither(): Pair<Events<A>, Events<B>> = * map { it.asThese() }.partitionThese() * ``` diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt index d88ae3b81349..02941bdded30 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Incremental.kt @@ -62,7 +62,7 @@ fun <K, V> incrementalOf(value: Map<K, V>): Incremental<K, V> { * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> Lazy<Incremental<K, V>>.defer() = deferredIncremental { value } * ``` */ @@ -78,7 +78,7 @@ fun <K, V> Lazy<Incremental<K, V>>.defer(): Incremental<K, V> = deferInline { va * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> DeferredValue<Incremental<K, V>>.defer() = deferredIncremental { get() } * ``` */ diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt index de9dca43b5d5..cc4ce53ca40a 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Merge.kt @@ -33,7 +33,7 @@ import com.android.systemui.kairos.util.map * function is used to combine coincident emissions to produce the result value to be emitted by the * merged [Events]. * - * ``` kotlin + * ``` * fun <A> Events<A>.mergeWith( * other: Events<A>, * transformCoincidence: TransactionScope.(A, A) -> A = { a, _ -> a }, @@ -62,7 +62,7 @@ fun <A> Events<A>.mergeWith( * Merges the given [Events] into a single [Events] that emits events from all. All coincident * emissions are collected into the emitted [List], preserving the input ordering. * - * ``` kotlin + * ``` * fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().merge() * ``` * @@ -76,7 +76,7 @@ fun <A> merge(vararg events: Events<A>): Events<List<A>> = events.asIterable().m * Merges the given [Events] into a single [Events] that emits events from all. In the case of * coincident emissions, the emission from the left-most [Events] is emitted. * - * ``` kotlin + * ``` * fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mergeLeft() * ``` * @@ -92,7 +92,7 @@ fun <A> mergeLeft(vararg events: Events<A>): Events<A> = events.asIterable().mer * function is used to combine coincident emissions to produce the result value to be emitted by the * merged [Events]. * - * ``` kotlin + * ``` * fun <A> merge(vararg events: Events<A>, transformCoincidence: (A, A) -> A): Events<A> = * merge(*events).map { l -> l.reduce(transformCoincidence) } * ``` @@ -117,7 +117,7 @@ fun <A> Iterable<Events<A>>.merge(): Events<List<A>> = * coincident emissions, the emission from the left-most [Events] is emitted. * * Semantically equivalent to the following definition: - * ``` kotlin + * ``` * fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = * merge().mapCheap { it.first() } * ``` @@ -135,7 +135,7 @@ fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = * Creates a new [Events] that emits events from all given [Events]. All simultaneous emissions are * collected into the emitted [List], preserving the input ordering. * - * ``` kotlin + * ``` * fun <A> Sequence<Events<A>>.merge(): Events<List<A>> = asIterable().merge() * ``` * @@ -148,7 +148,7 @@ fun <A> Iterable<Events<A>>.mergeLeft(): Events<A> = * collected into the emitted [Map], and are given the same key of the associated [Events] in the * input [Map]. * - * ``` kotlin + * ``` * fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = * asSequence() * .map { (k, events) -> events.map { a -> k to a } } @@ -173,7 +173,7 @@ fun <K, A> Map<K, Events<A>>.merge(): Events<Map<K, A>> = * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * * Conceptually this is equivalent to: - * ``` kotlin + * ``` * fun <K, V> State<Map<K, V>>.mergeEventsIncrementally(): Events<Map<K, V>> = * map { it.merge() }.switchEvents() * ``` @@ -218,7 +218,7 @@ fun <K, V> Incremental<K, Events<V>>.mergeEventsIncrementally(): Events<Map<K, V * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * * Conceptually this is equivalent to: - * ``` kotlin + * ``` * fun <K, V> State<Map<K, V>>.mergeEventsIncrementallyPromptly(): Events<Map<K, V>> = * map { it.merge() }.switchEventsPromptly() * ``` diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt index 22ca83c6a15a..e8b005ec8788 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/State.kt @@ -76,7 +76,7 @@ fun <A> stateOf(value: A): State<A> { * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> Lazy<State<A>>.defer() = deferredState { value } * ``` */ @@ -91,7 +91,7 @@ fun <A> stateOf(value: A): State<A> { * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> DeferredValue<State<A>>.defer() = deferredState { get() } * ``` */ @@ -150,7 +150,7 @@ fun <A, B> State<A>.mapCheapUnsafe(transform: KairosScope.(A) -> B): State<B> { * Splits a [State] of pairs into a pair of [Events][State], where each returned [State] holds half * of the original. * - * ``` kotlin + * ``` * fun <A, B> State<Pair<A, B>>.unzip(): Pair<State<A>, State<B>> { * val first = map { it.first } * val second = map { it.second } @@ -186,7 +186,7 @@ fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { /** * Returns a [State] that behaves like the current value of the original [State]. * - * ``` kotlin + * ``` * fun <A> State<State<A>>.flatten() = flatMap { it } * ``` * @@ -201,7 +201,7 @@ fun <A, B> State<A>.flatMap(transform: KairosScope.(A) -> State<B>): State<B> { * recent value is used. * * Effectively equivalent to: - * ``` kotlin + * ``` * ConflatedMutableEvents(kairosNetwork).holdState(initialValue) * ``` */ @@ -328,7 +328,7 @@ private inline fun <A> deferInline(crossinline block: InitScope.() -> State<A>): * Like [changes] but also includes the old value of this [State]. * * Shorthand for: - * ``` kotlin + * ``` * stateChanges.map { WithPrev(previousValue = sample(), newValue = it) } * ``` */ diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt index faeffe84e2e8..8020896d228a 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/StateScope.kt @@ -116,7 +116,7 @@ interface StateScope : TransactionScope { * [Events] emitted from this, following the patch rules outlined in * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * ``` kotlin + * ``` * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally( * initialEvents: DeferredValue<Map<K, Events<V>>>, * ): Events<Map<K, V>> = @@ -135,7 +135,7 @@ interface StateScope : TransactionScope { * [Events] emitted from this, following the patch rules outlined in * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * ``` kotlin + * ``` * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly( * initialEvents: DeferredValue<Map<K, Events<V>>>, * ): Events<Map<K, V>> = @@ -155,7 +155,7 @@ interface StateScope : TransactionScope { * [Events] emitted from this, following the patch rules outlined in * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * ``` kotlin + * ``` * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementally( * initialEvents: Map<K, Events<V>>, * ): Events<Map<K, V>> = @@ -174,7 +174,7 @@ interface StateScope : TransactionScope { * [Events] emitted from this, following the patch rules outlined in * [Map.applyPatch][com.android.systemui.kairos.util.applyPatch]. * - * ``` kotlin + * ``` * fun <K, V> Events<MapPatch<K, Events<V>>>.mergeEventsIncrementallyPromptly( * initialEvents: Map<K, Events<V>>, * ): Events<Map<K, V>> = @@ -220,7 +220,7 @@ interface StateScope : TransactionScope { * [mapLatestStateful], accumulation is not stopped with each subsequent emission of the * original [Events]. * - * ``` kotlin + * ``` * fun <A, B> Events<A>.mapStateful(transform: StateScope.(A) -> B): Events<B> = * map { statefully { transform(it) } }.applyStatefuls() * ``` @@ -234,7 +234,7 @@ interface StateScope : TransactionScope { * * Unlike [applyLatestStateful], state accumulation is not stopped with each state change. * - * ``` kotlin + * ``` * fun <A> State<Stateful<A>>.applyStatefuls(): State<A> = * changes * .applyStatefuls() @@ -252,7 +252,7 @@ interface StateScope : TransactionScope { * Returns an [Events] that acts like the most recent [Events] to be emitted from the original * [Events]. * - * ``` kotlin + * ``` * fun <A> Events<Events<A>>.flatten() = holdState(emptyEvents).switchEvents() * ``` * @@ -267,7 +267,7 @@ interface StateScope : TransactionScope { * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. * - * ``` kotlin + * ``` * fun <A, B> Events<A>.mapLatestStateful(transform: StateScope.(A) -> B): Events<B> = * map { statefully { transform(it) } }.applyLatestStateful() * ``` @@ -282,7 +282,7 @@ interface StateScope : TransactionScope { * [transform] can perform state accumulation via its [StateScope] receiver. With each * invocation of [transform], state accumulation from previous invocation is stopped. * - * ``` kotlin + * ``` * fun <A, B> Events<A>.flatMapLatestStateful( * transform: StateScope.(A) -> Events<B> * ): Events<B> = @@ -495,7 +495,7 @@ interface StateScope : TransactionScope { * * The optional [numKeys] argument is an optimization used to initialize the internal storage. * - * ``` kotlin + * ``` * fun <K, A, B> Events<MapPatch<K, A>>.mapLatestStatefulForKey( * numKeys: Int? = null, * transform: StateScope.(A) -> B, @@ -516,7 +516,7 @@ interface StateScope : TransactionScope { * If the original [Events] is emitting an event at this exact time, then it will be the only * even emitted from the result [Events]. * - * ``` kotlin + * ``` * fun <A> Events<A>.nextOnly(): Events<A> = * EventsLoop<A>().apply { * loopback = map { emptyEvents }.holdState(this@nextOnly).switchEvents() @@ -535,7 +535,7 @@ interface StateScope : TransactionScope { /** * Returns an [Events] that skips the next emission of the original [Events]. * - * ``` kotlin + * ``` * fun <A> Events<A>.skipNext(): Events<A> = * nextOnly().map { this@skipNext }.holdState(emptyEvents).switchEvents() * ``` @@ -554,7 +554,7 @@ interface StateScope : TransactionScope { * If the original [Events] emits at the same time as [stop], then the returned [Events] will * emit that value. * - * ``` kotlin + * ``` * fun <A> Events<A>.takeUntil(stop: Events<*>): Events<A> = * stop.map { emptyEvents }.nextOnly().holdState(this).switchEvents() * ``` @@ -586,7 +586,7 @@ interface StateScope : TransactionScope { * Returns an [Events] that emits values from the original [Events] up to and including a value * is emitted that satisfies [predicate]. * - * ``` kotlin + * ``` * fun <A> Events<A>.takeUntil(predicate: TransactionScope.(A) -> Boolean): Events<A> = * takeUntil(filter(predicate)) * ``` @@ -602,7 +602,7 @@ interface StateScope : TransactionScope { * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. * - * ``` kotlin + * ``` * fun <A, B> Events<A>.foldState( * initialValue: B, * transform: TransactionScope.(A, B) -> B, @@ -630,7 +630,7 @@ interface StateScope : TransactionScope { * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. * - * ``` kotlin + * ``` * fun <A, B> Events<A>.foldStateDeferred( * initialValue: DeferredValue<B>, * transform: TransactionScope.(A, B) -> B, @@ -663,7 +663,7 @@ interface StateScope : TransactionScope { * have been processed; this keeps the value of the [State] consistent during the entire Kairos * transaction. * - * ``` kotlin + * ``` * fun <A> Events<Stateful<A>>.holdLatestStateful(init: Stateful<A>): State<A> { * val (changes, initApplied) = applyLatestStateful(init) * return changes.holdStateDeferred(initApplied) @@ -724,7 +724,7 @@ interface StateScope : TransactionScope { * Returns an [Events] that wraps each emission of the original [Events] into an [IndexedValue], * containing the emitted value and its index (starting from zero). * - * ``` kotlin + * ``` * fun <A> Events<A>.withIndex(): Events<IndexedValue<A>> { * val index = fold(0) { _, oldIdx -> oldIdx + 1 } * return sample(index) { a, idx -> IndexedValue(idx, a) } @@ -740,7 +740,7 @@ interface StateScope : TransactionScope { * Returns an [Events] containing the results of applying [transform] to each value of the * original [Events] and its index (starting from zero). * - * ``` kotlin + * ``` * fun <A> Events<A>.mapIndexed(transform: TransactionScope.(Int, A) -> B): Events<B> { * val index = foldState(0) { _, i -> i + 1 } * return sample(index) { a, idx -> transform(idx, a) } @@ -755,7 +755,7 @@ interface StateScope : TransactionScope { /** * Returns an [Events] where all subsequent repetitions of the same value are filtered out. * - * ``` kotlin + * ``` * fun <A> Events<A>.distinctUntilChanged(): Events<A> { * val state: State<Any?> = holdState(Any()) * return filter { it != state.sample() } @@ -774,7 +774,7 @@ interface StateScope : TransactionScope { * Note that the returned [Events] will not emit anything until [other] has emitted at least one * value. * - * ``` kotlin + * ``` * fun <A, B, C> Events<A>.sample( * other: Events<B>, * transform: TransactionScope.(A, B) -> C, @@ -796,7 +796,7 @@ interface StateScope : TransactionScope { * Returns a [State] that samples the [Transactional] held by the given [State] within the same * transaction that the state changes. * - * ``` kotlin + * ``` * fun <A> State<Transactional<A>>.sampleTransactionals(): State<A> = * changes * .sampleTransactionals() @@ -815,7 +815,7 @@ interface StateScope : TransactionScope { * Note that this is less efficient than [State.map], which should be preferred if [transform] * does not need access to [TransactionScope]. * - * ``` kotlin + * ``` * fun <A, B> State<A>.mapTransactionally(transform: TransactionScope.(A) -> B): State<B> = * map { transactionally { transform(it) } }.sampleTransactionals() * ``` @@ -830,7 +830,7 @@ interface StateScope : TransactionScope { * Note that this is less efficient than [combine], which should be preferred if [transform] * does not need access to [TransactionScope]. * - * ``` kotlin + * ``` * fun <A, B, Z> combineTransactionally( * stateA: State<A>, * stateB: State<B>, @@ -895,7 +895,7 @@ interface StateScope : TransactionScope { * Note that this is less efficient than [flatMap], which should be preferred if [transform] * does not need access to [TransactionScope]. * - * ``` kotlin + * ``` * fun <A, B> State<A>.flatMapTransactionally( * transform: TransactionScope.(A) -> State<B> * ): State<B> = map { transactionally { transform(it) } }.sampleTransactionals().flatten() @@ -950,7 +950,7 @@ interface StateScope : TransactionScope { * Returns an [Incremental] that reflects the state of the original [Incremental], but also adds * / removes entries based on the state of the original's values. * - * ``` kotlin + * ``` * fun <K, V> Incremental<K, State<Maybe<V>>>.applyStateIncrementally(): Incremental<K, V> = * mapValues { (_, v) -> v.changes } * .mergeEventsIncrementallyPromptly() @@ -971,7 +971,7 @@ interface StateScope : TransactionScope { * / removes entries based on the [State] returned from applying [transform] to the original's * entries. * - * ``` kotlin + * ``` * fun <K, V, U> Incremental<K, V>.mapIncrementalState( * transform: KairosScope.(Map.Entry<K, V>) -> State<Maybe<U>> * ): Incremental<K, U> = mapValues { transform(it) }.applyStateIncrementally() @@ -986,7 +986,7 @@ interface StateScope : TransactionScope { * / removes entries based on the [State] returned from applying [transform] to the original's * entries, such that entries are added when that state is `true`, and removed when `false`. * - * ``` kotlin + * ``` * fun <K, V> Incremental<K, V>.filterIncrementally( * transform: KairosScope.(Map.Entry<K, V>) -> State<Boolean> * ): Incremental<K, V> = mapIncrementalState { entry -> @@ -1004,7 +1004,7 @@ interface StateScope : TransactionScope { * Returns an [Incremental] that samples the [Transactionals][Transactional] held by the * original within the same transaction that the incremental [updates]. * - * ``` kotlin + * ``` * fun <K, V> Incremental<K, Transactional<V>>.sampleTransactionals(): Incremental<K, V> = * updates * .map { patch -> patch.mapValues { (k, mv) -> mv.map { it.sample() } } } @@ -1027,7 +1027,7 @@ interface StateScope : TransactionScope { * Note that this is less efficient than [mapValues], which should be preferred if [transform] * does not need access to [TransactionScope]. * - * ``` kotlin + * ``` * fun <K, V, U> Incremental<K, V>.mapValuesTransactionally( * transform: TransactionScope.(Map.Entry<K, V>) -> U * ): Incremental<K, U> = diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt index cf98821fdadb..5050511a1371 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/Transactional.kt @@ -51,7 +51,7 @@ fun <A> transactionalOf(value: A): Transactional<A> = * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> DeferredValue<Transactional<A>>.defer() = deferredTransactional { get() } * ``` */ @@ -67,7 +67,7 @@ fun <A> DeferredValue<Transactional<A>>.defer(): Transactional<A> = deferInline * * Useful for recursive definitions. * - * ``` kotlin + * ``` * fun <A> Lazy<Transactional<A>>.defer() = deferredTransactional { value } * ``` */ diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt index e193a4957bd0..3dbc6f00c623 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/internal/store/MapK.kt @@ -21,28 +21,28 @@ package com.android.systemui.kairos.internal.store * * Let's say you want to write a class that is generic over both a map, and the type of data within * the map: - * ``` kotlin + * ``` * class Foo<TMap, TKey, TValue> { * val container: TMap<TKey, TElement> // disallowed! * } * ``` * * You can use `MapK` to represent the "higher-kinded" type variable `TMap`: - * ``` kotlin + * ``` * class Foo<TMap, TKey, TValue> { * val container: MapK<TMap, TKey, TValue> // OK! * } * ``` * * Note that Kotlin will not let you use the generic type without parameters as `TMap`: - * ``` kotlin + * ``` * val fooHk: MapK<HashMap, Int, String> // not allowed: HashMap requires two type parameters * ``` * * To work around this, you need to declare a special type-witness object. This object is only used * at compile time and can be stripped out by a minifier because it's never used at runtime. * - * ``` kotlin + * ``` * class Foo<A, B> : MapK<FooWitness, A, B> { ... } * object FooWitness * diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt index 8fe41bc20dfa..dde5d822f052 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/MapPatch.kt @@ -47,7 +47,7 @@ fun <K, V> Map<K, V>.applyPatch(patch: MapPatch<K, V>): Map<K, V> { * Returns a [MapPatch] that, when applied, includes all of the values from the original [Map]. * * Shorthand for: - * ``` kotlin + * ``` * mapValues { (key, value) -> Maybe.present(value) } * ``` */ diff --git a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt index 4754bc443329..4373705597ed 100644 --- a/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt +++ b/packages/SystemUI/utils/kairos/src/com/android/systemui/kairos/util/Maybe.kt @@ -69,7 +69,7 @@ object MaybeScope { * * This can be used instead of Kotlin's built-in nullability (`?.` and `?:`) operators when dealing * with complex combinations of nullables: - * ``` kotlin + * ``` * val aMaybe: Maybe<Any> = ... * val bMaybe: Maybe<Any> = ... * val result: String = maybe { diff --git a/packages/Vcn/service-b/Android.bp b/packages/Vcn/service-b/Android.bp index 1370b0678cc5..97574e6e35e3 100644 --- a/packages/Vcn/service-b/Android.bp +++ b/packages/Vcn/service-b/Android.bp @@ -39,9 +39,7 @@ java_library { name: "connectivity-utils-service-vcn-internal", sdk_version: "module_current", min_sdk_version: "30", - srcs: [ - ":framework-connectivity-shared-srcs", - ], + srcs: ["service-utils/**/*.java"], libs: [ "framework-annotations-lib", "unsupportedappusage", diff --git a/packages/Vcn/service-b/service-utils/android/util/LocalLog.java b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java new file mode 100644 index 000000000000..5955d930aab1 --- /dev/null +++ b/packages/Vcn/service-b/service-utils/android/util/LocalLog.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 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.util; + +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.SystemClock; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +/** + * @hide + */ +// Exported to Mainline modules; cannot use annotations +// @android.ravenwood.annotation.RavenwoodKeepWholeClass +// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public final class LocalLog { + + private final Deque<String> mLog; + private final int mMaxLines; + + /** + * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log + * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is + * useful when logging behavior that modifies device time zone or system clock. + */ + private final boolean mUseLocalTimestamps; + + @UnsupportedAppUsage + public LocalLog(int maxLines) { + this(maxLines, true /* useLocalTimestamps */); + } + + public LocalLog(int maxLines, boolean useLocalTimestamps) { + mMaxLines = Math.max(0, maxLines); + mLog = new ArrayDeque<>(mMaxLines); + mUseLocalTimestamps = useLocalTimestamps; + } + + @UnsupportedAppUsage + public void log(String msg) { + if (mMaxLines <= 0) { + return; + } + final String logLine; + if (mUseLocalTimestamps) { + logLine = LocalDateTime.now() + " - " + msg; + } else { + logLine = Duration.ofMillis(SystemClock.elapsedRealtime()) + + " / " + Instant.now() + " - " + msg; + } + append(logLine); + } + + private synchronized void append(String logLine) { + while (mLog.size() >= mMaxLines) { + mLog.remove(); + } + mLog.add(logLine); + } + + @UnsupportedAppUsage + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + dump(pw); + } + + public synchronized void dump(PrintWriter pw) { + dump("", pw); + } + + /** + * Dumps the content of local log to print writer with each log entry predeced with indent + * + * @param indent indent that precedes each log entry + * @param pw printer writer to write into + */ + public synchronized void dump(String indent, PrintWriter pw) { + Iterator<String> itr = mLog.iterator(); + while (itr.hasNext()) { + pw.printf("%s%s\n", indent, itr.next()); + } + } + + public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + reverseDump(pw); + } + + public synchronized void reverseDump(PrintWriter pw) { + Iterator<String> itr = mLog.descendingIterator(); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } + + // @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public synchronized void clear() { + mLog.clear(); + } + + public static class ReadOnlyLocalLog { + private final LocalLog mLog; + ReadOnlyLocalLog(LocalLog log) { + mLog = log; + } + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.dump(pw); + } + public void dump(PrintWriter pw) { + mLog.dump(pw); + } + public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.reverseDump(pw); + } + public void reverseDump(PrintWriter pw) { + mLog.reverseDump(pw); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public ReadOnlyLocalLog readOnlyLocalLog() { + return new ReadOnlyLocalLog(this); + } +}
\ No newline at end of file diff --git a/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java new file mode 100644 index 000000000000..7db62f8e9ffc --- /dev/null +++ b/packages/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 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.util; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.annotations.VisibleForTesting; + + /** + * An AlarmListener that sends the specified message to a Handler and keeps the system awake until + * the message is processed. + * + * This is useful when using the AlarmManager direct callback interface to wake up the system and + * request that an object whose API consists of messages (such as a StateMachine) perform some + * action. + * + * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send + * the message, but does not guarantee that the system will be awake until the target object has + * processed it. This is because as soon as the onAlarmListener sends the message and returns, the + * AlarmManager releases its wakelock and the system is free to go to sleep again. + */ +// TODO: b/374174952 This is an exact copy of +// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public class WakeupMessage implements AlarmManager.OnAlarmListener { + private final AlarmManager mAlarmManager; + + @VisibleForTesting + protected final Handler mHandler; + @VisibleForTesting + protected final String mCmdName; + @VisibleForTesting + protected final int mCmd, mArg1, mArg2; + @VisibleForTesting + protected final Object mObj; + private final Runnable mRunnable; + private boolean mScheduled; + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2, Object obj) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = cmd; + mArg1 = arg1; + mArg2 = arg2; + mObj = obj; + mRunnable = null; + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) { + this(context, handler, cmdName, cmd, arg1, 0, null); + } + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2) { + this(context, handler, cmdName, cmd, arg1, arg2, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) { + this(context, handler, cmdName, cmd, 0, 0, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = 0; + mArg1 = 0; + mArg2 = 0; + mObj = null; + mRunnable = runnable; + } + + private static AlarmManager getAlarmManager(Context context) { + return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + /** + * Schedule the message to be delivered at the time in milliseconds of the + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup + * the device when it goes off. If schedule is called multiple times without the message being + * dispatched then the alarm is rescheduled to the new time. + */ + public synchronized void schedule(long when) { + mAlarmManager.setExact( + AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler); + mScheduled = true; + } + + /** + * Cancel all pending messages. This includes alarms that may have been fired, but have not been + * run on the handler yet. + */ + public synchronized void cancel() { + if (mScheduled) { + mAlarmManager.cancel(this); + mScheduled = false; + } + } + + @Override + public void onAlarm() { + // Once this method is called the alarm has already been fired and removed from + // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now + // be marked as unscheduled so that it can be rescheduled in the message handler. + final boolean stillScheduled; + synchronized (this) { + stillScheduled = mScheduled; + mScheduled = false; + } + if (stillScheduled) { + Message msg; + if (mRunnable == null) { + msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj); + } else { + msg = Message.obtain(mHandler, mRunnable); + } + mHandler.dispatchMessage(msg); + msg.recycle(); + } + } +}
\ No newline at end of file diff --git a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt index 36307277b4b9..6ec39d953266 100644 --- a/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt +++ b/packages/Vcn/service-b/service-vcn-platform-jarjar-rules.txt @@ -1,5 +1,2 @@ -rule android.util.IndentingPrintWriter android.net.vcn.module.repackaged.android.util.IndentingPrintWriter rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog -rule com.android.internal.util.IndentingPrintWriter android.net.vcn.module.repackaged.com.android.internal.util.IndentingPrintWriter -rule com.android.internal.util.MessageUtils android.net.vcn.module.repackaged.com.android.internal.util.MessageUtils rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage
\ No newline at end of file diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 59043a8356ae..8e998426685b 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -345,14 +345,41 @@ java_library { ], } +// We define our own version of platform_compat_config's here, because: +// - The original version (e.g. "framework-platform-compat-config) is built from +// the output file of the device side jar, rather than the host jar, meaning +// they're slow to build because they depend on D8/R8 output. +// - The original services one ("services-platform-compat-config") is built from services.jar, +// which includes service.permission, which is very slow to rebuild because of kotlin. +// +// Because we're re-defining the same compat-IDs that are defined elsewhere, +// they should all have `include_in_merged_xml: false`. Otherwise, generating +// merged_compat_config.xml would fail due to duplicate IDs. +// +// These module names must end with "compat-config" because these will be used as the filename, +// and at runtime, we only loads files that match `*compat-config.xml`. +platform_compat_config { + name: "ravenwood-framework-platform-compat-config", + src: ":framework-minus-apex-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + +platform_compat_config { + name: "ravenwood-services.core-platform-compat-config", + src: ":services.core-for-host", + include_in_merged_xml: false, + visibility: ["//visibility:private"], +} + filegroup { name: "ravenwood-data", device_common_srcs: [ ":system-build.prop", ":framework-res", ":ravenwood-empty-res", - ":framework-platform-compat-config", - ":services-platform-compat-config", + ":ravenwood-framework-platform-compat-config", + ":ravenwood-services.core-platform-compat-config", "texts/ravenwood-build.prop", ], device_first_srcs: [ @@ -616,6 +643,10 @@ android_ravenwood_libgroup { "android.test.mock.ravenwood", "ravenwood-helper-runtime", "hoststubgen-helper-runtime.ravenwood", + + // Note, when we include other services.* jars, we'll need to add + // platform_compat_config for that module too. + // See ravenwood-services.core-platform-compat-config above. "services.core.ravenwood-jarjar", "services.fakes.ravenwood-jarjar", diff --git a/ravenwood/CleanSpec.mk b/ravenwood/CleanSpec.mk new file mode 100644 index 000000000000..50d2fab40326 --- /dev/null +++ b/ravenwood/CleanSpec.mk @@ -0,0 +1,45 @@ +# Copyright (C) 2024 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# If you don't need to do a full clean build but would like to touch +# a file or delete some intermediate files, add a clean step to the end +# of the list. These steps will only be run once, if they haven't been +# run before. +# +# E.g.: +# $(call add-clean-step, touch -c external/sqlite/sqlite3.h) +# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates) +# +# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with +# files that are missing or have been moved. +# +# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory. +# Use $(OUT_DIR) to refer to the "out" directory. +# +# If you need to re-do something that's already mentioned, just copy +# the command and add it to the bottom of the list. E.g., if a change +# that you made last week required touching a file and a change you +# made today requires touching the same file, just copy the old +# touch step and add it to the end of the list. +# +# ***************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THE BANNER +# ***************************************************************** + +$(call add-clean-step, rm -rf $(OUT_DIR)/host/linux-x86/testcases/ravenwood-runtime) + +# ****************************************************************** +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST ABOVE THIS BANNER +# ****************************************************************** diff --git a/ravenwood/tools/hoststubgen/scripts/dump-jar b/ravenwood/tools/hoststubgen/scripts/dump-jar index 87652451359d..998357b70dff 100755 --- a/ravenwood/tools/hoststubgen/scripts/dump-jar +++ b/ravenwood/tools/hoststubgen/scripts/dump-jar @@ -89,7 +89,7 @@ filter_output() { # - Some other transient lines # - Sometimes the javap shows mysterious warnings, so remove them too. # - # `/PATTERN-1/,/PATTERN-1/{//!d}` is a trick to delete lines between two patterns, without + # `/PATTERN-1/,/PATTERN-2/{//!d}` is a trick to delete lines between two patterns, without # the start and the end lines. sed -e 's/#[0-9][0-9]*/#x/g' \ -e 's/^\( *\)[0-9][0-9]*:/\1x:/' \ diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt index 6d8d7b768b91..cc704b2b32ed 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt @@ -27,7 +27,7 @@ import com.android.hoststubgen.filters.ImplicitOutputFilter import com.android.hoststubgen.filters.KeepNativeFilter import com.android.hoststubgen.filters.OutputFilter import com.android.hoststubgen.filters.SanitizationFilter -import com.android.hoststubgen.filters.TextFileFilterPolicyParser +import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder import com.android.hoststubgen.filters.printAsTextPolicy import com.android.hoststubgen.utils.ClassFilter import com.android.hoststubgen.visitors.BaseAdapter @@ -179,9 +179,9 @@ class HostStubGen(val options: HostStubGenOptions) { // Next, "text based" filter, which allows to override polices without touching // the target code. if (options.policyOverrideFiles.isNotEmpty()) { - val parser = TextFileFilterPolicyParser(allClasses, filter) - options.policyOverrideFiles.forEach(parser::parse) - filter = parser.createOutputFilter() + val builder = TextFileFilterPolicyBuilder(allClasses, filter) + options.policyOverrideFiles.forEach(builder::parse) + filter = builder.createOutputFilter() } // Apply the implicit filter. diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index 7462a8ce12c5..be1b6ca93869 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -23,10 +23,12 @@ import com.android.hoststubgen.asm.toJvmClassName import com.android.hoststubgen.log import com.android.hoststubgen.normalizeTextLine import com.android.hoststubgen.whitespaceRegex -import java.io.File +import org.objectweb.asm.tree.ClassNode +import java.io.BufferedReader +import java.io.FileReader import java.io.PrintWriter +import java.io.Reader import java.util.regex.Pattern -import org.objectweb.asm.tree.ClassNode /** * Print a class node as a "keep" policy. @@ -48,7 +50,7 @@ fun printAsTextPolicy(pw: PrintWriter, cn: ClassNode) { private const val FILTER_REASON = "file-override" -private enum class SpecialClass { +enum class SpecialClass { NotSpecial, Aidl, FeatureFlags, @@ -56,10 +58,58 @@ private enum class SpecialClass { RFile, } -class TextFileFilterPolicyParser( +/** + * This receives [TextFileFilterPolicyBuilder] parsing result. + */ +interface PolicyFileProcessor { + /** "package" directive. */ + fun onPackage(name: String, policy: FilterPolicyWithReason) + + /** "rename" directive. */ + fun onRename(pattern: Pattern, prefix: String) + + /** "class" directive. */ + fun onSimpleClassStart(className: String) + fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) + fun onSimpleClassEnd(className: String) + + fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason) + fun onRedirectionClass(fromClassName: String, toClassName: String) + fun onClassLoadHook(className: String, callback: String) + fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason) + + /** "field" directive. */ + fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) + + /** "method" directive. */ + fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) + fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) + fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) +} + +class TextFileFilterPolicyBuilder( private val classes: ClassNodes, fallback: OutputFilter ) { + private val parser = TextFileFilterPolicyParser() + private val subclassFilter = SubclassFilter(classes, fallback) private val packageFilter = PackageFilter(subclassFilter) private val imf = InMemoryOutputFilter(classes, packageFilter) @@ -71,30 +121,19 @@ class TextFileFilterPolicyParser( private val methodReplaceSpec = mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>() - private lateinit var currentClassName: String - /** - * Read a given "policy" file and return as an [OutputFilter] + * Parse a given policy file. This method can be called multiple times to read from + * multiple files. To get the resulting filter, use [createOutputFilter] */ fun parse(file: String) { - log.i("Loading offloaded annotations from $file ...") - log.withIndent { - var lineNo = 0 - try { - File(file).forEachLine { - lineNo++ - val line = normalizeTextLine(it) - if (line.isEmpty()) { - return@forEachLine // skip empty lines. - } - parseLine(line) - } - } catch (e: ParseException) { - throw e.withSourceInfo(file, lineNo) - } - } + // We may parse multiple files, but we reuse the same parser, because the parser + // will make sure there'll be no dupplicating "special class" policies. + parser.parse(FileReader(file), file, Processor()) } + /** + * Generate the resulting [OutputFilter]. + */ fun createOutputFilter(): OutputFilter { var ret: OutputFilter = imf if (typeRenameSpec.isNotEmpty()) { @@ -112,14 +151,200 @@ class TextFileFilterPolicyParser( return ret } + private inner class Processor : PolicyFileProcessor { + override fun onPackage(name: String, policy: FilterPolicyWithReason) { + packageFilter.addPolicy(name, policy) + } + + override fun onRename(pattern: Pattern, prefix: String) { + typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + pattern, prefix + ) + } + + override fun onSimpleClassStart(className: String) { + } + + override fun onSimpleClassEnd(className: String) { + } + + override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) { + imf.setPolicyForClass(className, policy) + } + + override fun onSubClassPolicy( + superClassName: String, + policy: FilterPolicyWithReason, + ) { + log.i("class extends $superClassName") + subclassFilter.addPolicy( superClassName, policy) + } + + override fun onRedirectionClass(fromClassName: String, toClassName: String) { + imf.setRedirectionClass(fromClassName, toClassName) + } + + override fun onClassLoadHook(className: String, callback: String) { + imf.setClassLoadHook(className, callback) + } + + override fun onSpecialClassPolicy( + type: SpecialClass, + policy: FilterPolicyWithReason, + ) { + log.i("class special $type $policy") + when (type) { + SpecialClass.NotSpecial -> {} // Shouldn't happen + + SpecialClass.Aidl -> { + aidlPolicy = policy + } + + SpecialClass.FeatureFlags -> { + featureFlagsPolicy = policy + } + + SpecialClass.Sysprops -> { + syspropsPolicy = policy + } + + SpecialClass.RFile -> { + rFilePolicy = policy + } + } + } + + override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) { + imf.setPolicyForField(className, fieldName, policy) + } + + override fun onSimpleMethodPolicy( + className: String, + methodName: String, + methodDesc: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + } + + override fun onMethodInClassReplace( + className: String, + methodName: String, + methodDesc: String, + targetName: String, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + + // Make sure to keep the target method. + imf.setPolicyForMethod( + className, + targetName, + methodDesc, + FilterPolicy.Keep.withReason(FILTER_REASON) + ) + // Set up the rename. + imf.setRenameTo(className, targetName, methodDesc, methodName) + } + + override fun onMethodOutClassReplace( + className: String, + methodName: String, + methodDesc: String, + replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec, + policy: FilterPolicyWithReason, + ) { + imf.setPolicyForMethod(className, methodName, methodDesc, policy) + methodReplaceSpec.add(replaceSpec) + } + } +} + +/** + * Parses a filer policy text file. + */ +class TextFileFilterPolicyParser { + private lateinit var processor: PolicyFileProcessor + private var currentClassName: String? = null + + private var aidlPolicy: FilterPolicyWithReason? = null + private var featureFlagsPolicy: FilterPolicyWithReason? = null + private var syspropsPolicy: FilterPolicyWithReason? = null + private var rFilePolicy: FilterPolicyWithReason? = null + + /** Name of the file that's currently being processed. */ + var filename: String? = null + private set + + /** 1-based line number in the current file */ + var lineNumber = -1 + private set + + /** + * Parse a given "policy" file. + */ + fun parse(reader: Reader, inputName: String, processor: PolicyFileProcessor) { + filename = inputName + + log.i("Parsing text policy file $inputName ...") + this.processor = processor + BufferedReader(reader).use { rd -> + lineNumber = 0 + try { + while (true) { + var line = rd.readLine() + if (line == null) { + break + } + lineNumber++ + line = normalizeTextLine(line) // Remove comment and trim. + if (line.isEmpty()) { + continue + } + parseLine(line) + } + finishLastClass() + } catch (e: ParseException) { + throw e.withSourceInfo(inputName, lineNumber) + } + } + } + + private fun finishLastClass() { + currentClassName?.let { className -> + processor.onSimpleClassEnd(className) + currentClassName = null + } + } + + private fun ensureInClass(directive: String): String { + return currentClassName ?: + throw ParseException("Directive '$directive' must follow a 'class' directive") + } + private fun parseLine(line: String) { val fields = line.split(whitespaceRegex).toTypedArray() when (fields[0].lowercase()) { - "p", "package" -> parsePackage(fields) - "c", "class" -> parseClass(fields) - "f", "field" -> parseField(fields) - "m", "method" -> parseMethod(fields) - "r", "rename" -> parseRename(fields) + "p", "package" -> { + finishLastClass() + parsePackage(fields) + } + "c", "class" -> { + finishLastClass() + parseClass(fields) + } + "f", "field" -> { + ensureInClass("field") + parseField(fields) + } + "m", "method" -> { + ensureInClass("method") + parseMethod(fields) + } + "r", "rename" -> { + finishLastClass() + parseRename(fields) + } else -> throw ParseException("Unknown directive \"${fields[0]}\"") } } @@ -184,20 +409,20 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithClasses) { throw ParseException("Package can't have policy '$policy'") } - packageFilter.addPolicy(name, policy.withReason(FILTER_REASON)) + processor.onPackage(name, policy.withReason(FILTER_REASON)) } private fun parseClass(fields: Array<String>) { if (fields.size < 3) { throw ParseException("Class ('c') expects 2 fields.") } - currentClassName = fields[1] + val className = fields[1] // superClass is set when the class name starts with a "*". - val superClass = resolveExtendingClass(currentClassName) + val superClass = resolveExtendingClass(className) // :aidl, etc? - val classType = resolveSpecialClass(currentClassName) + val classType = resolveSpecialClass(className) if (fields[2].startsWith("!")) { if (classType != SpecialClass.NotSpecial) { @@ -208,7 +433,8 @@ class TextFileFilterPolicyParser( } // It's a redirection class. val toClass = fields[2].substring(1) - imf.setRedirectionClass(currentClassName, toClass) + + processor.onRedirectionClass(className, toClass) } else if (fields[2].startsWith("~")) { if (classType != SpecialClass.NotSpecial) { // We could support it, but not needed at least for now. @@ -218,7 +444,8 @@ class TextFileFilterPolicyParser( } // It's a class-load hook val callback = fields[2].substring(1) - imf.setClassLoadHook(currentClassName, callback) + + processor.onClassLoadHook(className, callback) } else { val policy = parsePolicy(fields[2]) if (!policy.isUsableWithClasses) { @@ -229,26 +456,27 @@ class TextFileFilterPolicyParser( SpecialClass.NotSpecial -> { // TODO: Duplicate check, etc if (superClass == null) { - imf.setPolicyForClass( - currentClassName, policy.withReason(FILTER_REASON) - ) + currentClassName = className + processor.onSimpleClassStart(className) + processor.onSimpleClassPolicy(className, policy.withReason(FILTER_REASON)) } else { - subclassFilter.addPolicy( + processor.onSubClassPolicy( superClass, - policy.withReason("extends $superClass") + policy.withReason("extends $superClass"), ) } } - SpecialClass.Aidl -> { if (aidlPolicy != null) { throw ParseException( "Policy for AIDL classes already defined" ) } - aidlPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class AIDL)" ) + processor.onSpecialClassPolicy(classType, p) + aidlPolicy = p } SpecialClass.FeatureFlags -> { @@ -257,9 +485,11 @@ class TextFileFilterPolicyParser( "Policy for feature flags already defined" ) } - featureFlagsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class feature flags)" ) + processor.onSpecialClassPolicy(classType, p) + featureFlagsPolicy = p } SpecialClass.Sysprops -> { @@ -268,9 +498,11 @@ class TextFileFilterPolicyParser( "Policy for sysprops already defined" ) } - syspropsPolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class sysprops)" ) + processor.onSpecialClassPolicy(classType, p) + syspropsPolicy = p } SpecialClass.RFile -> { @@ -279,9 +511,11 @@ class TextFileFilterPolicyParser( "Policy for R file already defined" ) } - rFilePolicy = policy.withReason( + val p = policy.withReason( "$FILTER_REASON (special-class R file)" ) + processor.onSpecialClassPolicy(classType, p) + rFilePolicy = p } } } @@ -296,17 +530,16 @@ class TextFileFilterPolicyParser( if (!policy.isUsableWithFields) { throw ParseException("Field can't have policy '$policy'") } - require(this::currentClassName.isInitialized) // TODO: Duplicate check, etc - imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON)) + processor.onField(currentClassName!!, name, policy.withReason(FILTER_REASON)) } private fun parseMethod(fields: Array<String>) { if (fields.size < 3 || fields.size > 4) { throw ParseException("Method ('m') expects 3 or 4 fields.") } - val name = fields[1] + val methodName = fields[1] val signature: String val policyStr: String if (fields.size <= 3) { @@ -323,44 +556,48 @@ class TextFileFilterPolicyParser( throw ParseException("Method can't have policy '$policy'") } - require(this::currentClassName.isInitialized) + val className = currentClassName!! - imf.setPolicyForMethod( - currentClassName, name, signature, - policy.withReason(FILTER_REASON) - ) - if (policy == FilterPolicy.Substitute) { - val fromName = policyStr.substring(1) + val policyWithReason = policy.withReason(FILTER_REASON) + if (policy != FilterPolicy.Substitute) { + processor.onSimpleMethodPolicy(className, methodName, signature, policyWithReason) + } else { + val targetName = policyStr.substring(1) - if (fromName == name) { + if (targetName == methodName) { throw ParseException( "Substitution must have a different name" ) } - // Set the policy for the "from" method. - imf.setPolicyForMethod( - currentClassName, fromName, signature, - FilterPolicy.Keep.withReason(FILTER_REASON) - ) - - val classAndMethod = splitWithLastPeriod(fromName) + val classAndMethod = splitWithLastPeriod(targetName) if (classAndMethod != null) { // If the substitution target contains a ".", then // it's a method call redirect. - methodReplaceSpec.add( - TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( - currentClassName.toJvmClassName(), - name, + val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec( + currentClassName!!.toJvmClassName(), + methodName, signature, classAndMethod.first.toJvmClassName(), classAndMethod.second, ) + processor.onMethodOutClassReplace( + className, + methodName, + signature, + spec, + policyWithReason, ) } else { // It's an in-class replace. // ("@RavenwoodReplace" equivalent) - imf.setRenameTo(currentClassName, fromName, signature, name) + processor.onMethodInClassReplace( + className, + methodName, + signature, + targetName, + policyWithReason, + ) } } } @@ -378,7 +615,7 @@ class TextFileFilterPolicyParser( // applied. (Which is needed for services.jar) val prefix = fields[2].trimStart('/') - typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec( + processor.onRename( pattern, prefix ) } diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh index b389a67a8e4c..8408a18142eb 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/diff-and-update-golden.sh @@ -34,10 +34,11 @@ source "${0%/*}"/../common.sh SCRIPT_NAME="${0##*/}" -GOLDEN_DIR=golden-output +GOLDEN_DIR=${GOLDEN_DIR:-golden-output} mkdir -p $GOLDEN_DIR -DIFF_CMD=${DIFF:-diff -u --ignore-blank-lines --ignore-space-change} +# TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. +DIFF_CMD=${DIFF_CMD:-diff -u --ignore-blank-lines --ignore-space-change --ignore-matching-lines='^\(Constant.pool:\|{\)$'} update=0 three_way=0 @@ -62,7 +63,7 @@ done shift $(($OPTIND - 1)) # Build the dump files, which are the input of this test. -run m dump-jar tiny-framework-dump-test +run ${BUILD_CMD:=m} dump-jar tiny-framework-dump-test # Get the path to the generate text files. (not the golden files.) diff --git a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py index 88fa492addb8..c35d6d106c8d 100755 --- a/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py +++ b/ravenwood/tools/hoststubgen/test-tiny-framework/tiny-framework-dump-test.py @@ -28,8 +28,11 @@ GOLDEN_DIRS = [ # Run diff. def run_diff(file1, file2): + # TODO(b/388562869) We shouldn't need `--ignore-matching-lines`, but the golden files may not have the "Constant pool:" lines. command = ['diff', '-u', '--ignore-blank-lines', - '--ignore-space-change', file1, file2] + '--ignore-space-change', + '--ignore-matching-lines=^\(Constant.pool:\|{\)$', + file1, file2] print(' '.join(command)) result = subprocess.run(command, stderr=sys.stdout) diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 6cd1f721d215..b08aa5b37b33 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -542,7 +542,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub MagnificationController magnificationController, @Nullable AccessibilityInputFilter inputFilter, ProxyManager proxyManager, - PermissionEnforcer permissionEnforcer) { + PermissionEnforcer permissionEnforcer, + HearingDevicePhoneCallNotificationController hearingDeviceNotificationController) { super(permissionEnforcer); mContext = context; mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); @@ -571,8 +572,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mVisibleBgUserIds = null; mInputManager = context.getSystemService(InputManager.class); if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { - mHearingDeviceNotificationController = new HearingDevicePhoneCallNotificationController( - context); + mHearingDeviceNotificationController = hearingDeviceNotificationController; } else { mHearingDeviceNotificationController = null; } @@ -4385,13 +4385,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void launchAccessibilityFrameworkFeature(int displayId, ComponentName assignedTarget) { if (assignedTarget.equals(ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME)) { - //import com.android.systemui.Flags; - if (com.android.systemui.Flags.hearingAidsQsTileDialog()) { - launchHearingDevicesDialog(); - } else { - launchAccessibilitySubSettings(displayId, - ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME); - } + launchHearingDevicesDialog(); } } diff --git a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java index 6ced096e8778..d8fbde4115d9 100644 --- a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java +++ b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java @@ -16,7 +16,7 @@ package com.android.server.backup; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; +import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import android.annotation.Nullable; @@ -302,7 +302,7 @@ public class BackupAgentConnectionManager { // that the package being backed up doesn't get stuck in restricted mode until the // backup time-out elapses. for (int token : mOperationStorage.operationTokensForPackage(packageName)) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:" + Integer.toHexString(token)); diff --git a/services/backup/java/com/android/server/backup/BackupManagerConstants.java b/services/backup/java/com/android/server/backup/BackupManagerConstants.java index b753d01562ca..6aa5e2c05457 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerConstants.java +++ b/services/backup/java/com/android/server/backup/BackupManagerConstants.java @@ -16,8 +16,6 @@ package com.android.server.backup; -import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING; - import android.app.AlarmManager; import android.app.job.JobInfo; import android.content.ContentResolver; @@ -168,85 +166,55 @@ public class BackupManagerConstants extends KeyValueSettingObserver { // group the calls of these methods in a block syncrhonized on // a reference of this object. public synchronized long getKeyValueBackupIntervalMilliseconds() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getKeyValueBackupIntervalMilliseconds(...) returns " - + mKeyValueBackupIntervalMilliseconds); - } + Slog.d(TAG, "getKeyValueBackupIntervalMilliseconds(...) returns " + + mKeyValueBackupIntervalMilliseconds); return mKeyValueBackupIntervalMilliseconds; } public synchronized long getKeyValueBackupFuzzMilliseconds() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getKeyValueBackupFuzzMilliseconds(...) returns " - + mKeyValueBackupFuzzMilliseconds); - } + Slog.d(TAG, "getKeyValueBackupFuzzMilliseconds(...) returns " + + mKeyValueBackupFuzzMilliseconds); return mKeyValueBackupFuzzMilliseconds; } public synchronized boolean getKeyValueBackupRequireCharging() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getKeyValueBackupRequireCharging(...) returns " - + mKeyValueBackupRequireCharging); - } + Slog.d(TAG, + "getKeyValueBackupRequireCharging(...) returns " + mKeyValueBackupRequireCharging); return mKeyValueBackupRequireCharging; } public synchronized int getKeyValueBackupRequiredNetworkType() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getKeyValueBackupRequiredNetworkType(...) returns " - + mKeyValueBackupRequiredNetworkType); - } + Slog.d(TAG, "getKeyValueBackupRequiredNetworkType(...) returns " + + mKeyValueBackupRequiredNetworkType); return mKeyValueBackupRequiredNetworkType; } public synchronized long getFullBackupIntervalMilliseconds() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getFullBackupIntervalMilliseconds(...) returns " - + mFullBackupIntervalMilliseconds); - } + Slog.d(TAG, "getFullBackupIntervalMilliseconds(...) returns " + + mFullBackupIntervalMilliseconds); return mFullBackupIntervalMilliseconds; } public synchronized boolean getFullBackupRequireCharging() { - if (DEBUG_SCHEDULING) { - Slog.v(TAG, "getFullBackupRequireCharging(...) returns " + mFullBackupRequireCharging); - } + Slog.d(TAG, "getFullBackupRequireCharging(...) returns " + mFullBackupRequireCharging); return mFullBackupRequireCharging; } public synchronized int getFullBackupRequiredNetworkType() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getFullBackupRequiredNetworkType(...) returns " - + mFullBackupRequiredNetworkType); - } + Slog.d(TAG, + "getFullBackupRequiredNetworkType(...) returns " + mFullBackupRequiredNetworkType); return mFullBackupRequiredNetworkType; } /** Returns an array of package names that should be notified whenever a backup finishes. */ public synchronized String[] getBackupFinishedNotificationReceivers() { - if (DEBUG_SCHEDULING) { - Slog.v( - TAG, - "getBackupFinishedNotificationReceivers(...) returns " - + TextUtils.join(", ", mBackupFinishedNotificationReceivers)); - } + Slog.d(TAG, "getBackupFinishedNotificationReceivers(...) returns " + TextUtils.join(", ", + mBackupFinishedNotificationReceivers)); return mBackupFinishedNotificationReceivers; } public synchronized long getWakelockTimeoutMillis() { - Slog.v(TAG, "wakelock timeout: " + mWakelockTimeoutMillis); + Slog.d(TAG, "wakelock timeout: " + mWakelockTimeoutMillis); return mWakelockTimeoutMillis; } } diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 3f6ede95eaf9..5edf08cc1d80 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -22,9 +22,9 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; @@ -60,6 +60,7 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -91,11 +92,9 @@ import java.util.Set; * privileged callers (currently {@link DevicePolicyManager}). If called on {@link * UserHandle#USER_SYSTEM}, backup is disabled for all users. */ -public class BackupManagerService extends IBackupManager.Stub { +public class BackupManagerService extends IBackupManager.Stub implements BackupManagerInternal { public static final String TAG = "BackupManagerService"; - public static final boolean DEBUG = true; - public static final boolean MORE_DEBUG = false; - public static final boolean DEBUG_SCHEDULING = true; + public static final boolean DEBUG = false; @VisibleForTesting static final String DUMP_RUNNING_USERS_MESSAGE = "Backup Manager is running for users:"; @@ -186,12 +185,9 @@ public class BackupManagerService extends IBackupManager.Stub { mUserRemovedReceiver, new IntentFilter(Intent.ACTION_USER_REMOVED)); UserHandle mainUser = getUserManager().getMainUser(); mDefaultBackupUserId = mainUser == null ? UserHandle.USER_SYSTEM : mainUser.getIdentifier(); - if (DEBUG) { - Slog.d(TAG, "Default backup user id = " + mDefaultBackupUserId); - } + Slog.d(TAG, "Default backup user id = " + mDefaultBackupUserId); } - // TODO: Remove this when we implement DI by injecting in the construtor. @VisibleForTesting Handler getBackupHandler() { return mHandler; @@ -637,51 +633,28 @@ public class BackupManagerService extends IBackupManager.Stub { } @Override - public void agentConnectedForUser(int userId, String packageName, IBinder agent) - throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentConnected(userId, packageName, agent); + public void agentConnectedForUser(String packageName, @UserIdInt int userId, IBinder agent) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentConnected(String packageName, IBinder agent) throws RemoteException { - agentConnectedForUser(binderGetCallingUserId(), packageName, agent); - } - - /** - * Callback: a requested backup agent has been instantiated. This should only be called from the - * {@link ActivityManager}. - */ - public void agentConnected(@UserIdInt int userId, String packageName, IBinder agentBinder) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentConnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentConnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName, - agentBinder); + agent); } } @Override - public void agentDisconnectedForUser(int userId, String packageName) throws RemoteException { - if (isUserReadyForBackup(userId)) { - agentDisconnected(userId, packageName); + public void agentDisconnectedForUser(String packageName, @UserIdInt int userId) { + if (!isUserReadyForBackup(userId)) { + return; } - } - @Override - public void agentDisconnected(String packageName) throws RemoteException { - agentDisconnectedForUser(binderGetCallingUserId(), packageName); - } - - /** - * Callback: a backup agent has failed to come up, or has unexpectedly quit. This should only be - * called from the {@link ActivityManager}. - */ - public void agentDisconnected(@UserIdInt int userId, String packageName) { - UserBackupManagerService userBackupManagerService = - getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()"); + UserBackupManagerService userBackupManagerService = getServiceForUserIfCallerHasPermission( + userId, "agentDisconnected()"); if (userBackupManagerService != null) { userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected( @@ -1688,7 +1661,7 @@ public class BackupManagerService extends IBackupManager.Stub { * @param userId User id on which the backup operation is being requested. * @param message A message to include in the exception if it is thrown. */ - void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { + private void enforceCallingPermissionOnUserId(@UserIdInt int userId, String message) { if (binderGetCallingUserId() != userId) { mContext.enforceCallingOrSelfPermission( Manifest.permission.INTERACT_ACROSS_USERS_FULL, message); @@ -1697,6 +1670,8 @@ public class BackupManagerService extends IBackupManager.Stub { /** Implementation to receive lifecycle event callbacks for system services. */ public static class Lifecycle extends SystemService { + private final BackupManagerService mBackupManagerService; + public Lifecycle(Context context) { this(context, new BackupManagerService(context)); } @@ -1704,12 +1679,14 @@ public class BackupManagerService extends IBackupManager.Stub { @VisibleForTesting Lifecycle(Context context, BackupManagerService backupManagerService) { super(context); + mBackupManagerService = backupManagerService; sInstance = backupManagerService; + LocalServices.addService(BackupManagerInternal.class, mBackupManagerService); } @Override public void onStart() { - publishService(Context.BACKUP_SERVICE, BackupManagerService.sInstance); + publishService(Context.BACKUP_SERVICE, mBackupManagerService); } @Override @@ -1717,17 +1694,17 @@ public class BackupManagerService extends IBackupManager.Stub { // Starts the backup service for this user if backup is active for this user. Offloads // work onto the handler thread {@link #mHandlerThread} to keep unlock time low since // backup is not essential for device functioning. - sInstance.postToHandler( + mBackupManagerService.postToHandler( () -> { - sInstance.updateDefaultBackupUserIdIfNeeded(); - sInstance.startServiceForUser(user.getUserIdentifier()); - sInstance.mHasFirstUserUnlockedSinceBoot = true; + mBackupManagerService.updateDefaultBackupUserIdIfNeeded(); + mBackupManagerService.startServiceForUser(user.getUserIdentifier()); + mBackupManagerService.mHasFirstUserUnlockedSinceBoot = true; }); } @Override public void onUserStopping(@NonNull TargetUser user) { - sInstance.onStopUser(user.getUserIdentifier()); + mBackupManagerService.onStopUser(user.getUserIdentifier()); } @VisibleForTesting diff --git a/services/backup/java/com/android/server/backup/BackupWakeLock.java b/services/backup/java/com/android/server/backup/BackupWakeLock.java new file mode 100644 index 000000000000..d839e7a1583b --- /dev/null +++ b/services/backup/java/com/android/server/backup/BackupWakeLock.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2025 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.backup; + +import static com.android.server.backup.BackupManagerService.TAG; + +import android.annotation.Nullable; +import android.os.PowerManager; +import android.os.WorkSource; +import android.util.Slog; + +/** + * Wrapper over {@link PowerManager.WakeLock} to prevent double-free exceptions on release() + * after quit(). + * + * <p>There should be a single instance of this class per {@link UserBackupManagerService}. + */ +public class BackupWakeLock { + private final PowerManager.WakeLock mPowerManagerWakeLock; + private boolean mHasQuit = false; + private final String mUserIdMessage; + private final BackupManagerConstants mBackupManagerConstants; + + public BackupWakeLock(PowerManager.WakeLock powerManagerWakeLock, int userId, + BackupManagerConstants backupManagerConstants) { + mPowerManagerWakeLock = powerManagerWakeLock; + mUserIdMessage = "[UserID:" + userId + "] "; + mBackupManagerConstants = backupManagerConstants; + } + + /** Acquires the {@link PowerManager.WakeLock} if hasn't been quit. */ + public synchronized void acquire() { + if (mHasQuit) { + Slog.d(TAG, mUserIdMessage + "Ignore wakelock acquire after quit: " + + mPowerManagerWakeLock.getTag()); + return; + } + // Set a timeout for the wakelock. Otherwise if we fail internally and never call + // release(), the device might stay awake and drain battery indefinitely. + mPowerManagerWakeLock.acquire(mBackupManagerConstants.getWakelockTimeoutMillis()); + Slog.d(TAG, mUserIdMessage + "Acquired wakelock:" + mPowerManagerWakeLock.getTag()); + } + + /** Releases the {@link PowerManager.WakeLock} if hasn't been quit. */ + public synchronized void release() { + if (mHasQuit) { + Slog.d(TAG, mUserIdMessage + "Ignore wakelock release after quit: " + + mPowerManagerWakeLock.getTag()); + return; + } + + if (!mPowerManagerWakeLock.isHeld()) { + Slog.w(TAG, mUserIdMessage + "Wakelock not held: " + mPowerManagerWakeLock.getTag()); + return; + } + + mPowerManagerWakeLock.release(); + Slog.d(TAG, mUserIdMessage + "Released wakelock:" + mPowerManagerWakeLock.getTag()); + } + + /** + * Returns true if the {@link PowerManager.WakeLock} has been acquired but not yet released. + */ + public synchronized boolean isHeld() { + return mPowerManagerWakeLock.isHeld(); + } + + /** Release the {@link PowerManager.WakeLock} till it isn't held. */ + public synchronized void quit() { + while (mPowerManagerWakeLock.isHeld()) { + Slog.d(TAG, mUserIdMessage + "Releasing wakelock: " + mPowerManagerWakeLock.getTag()); + mPowerManagerWakeLock.release(); + } + mHasQuit = true; + } + + /** Calls {@link PowerManager.WakeLock#setWorkSource} on the underlying wake lock. */ + public void setWorkSource(@Nullable WorkSource workSource) { + mPowerManagerWakeLock.setWorkSource(workSource); + } +} diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java index b343ec8e709b..9f4407d99e18 100644 --- a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java +++ b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java @@ -180,9 +180,7 @@ public class KeyValueAdbBackupEngine { Slog.e(TAG, "Key-value backup failed on package " + packageName); return false; } - if (DEBUG) { - Slog.i(TAG, "Key-value backup success for package " + packageName); - } + Slog.i(TAG, "Key-value backup success for package " + packageName); return true; } catch (RemoteException e) { Slog.e(TAG, "Error invoking agent for backup on " + packageName + ". " + e); @@ -210,9 +208,7 @@ public class KeyValueAdbBackupEngine { AppMetadataBackupWriter writer = new AppMetadataBackupWriter(output, mPackageManager); - if (DEBUG) { - Slog.d(TAG, "Writing manifest for " + mPackage.packageName); - } + Slog.d(TAG, "Writing manifest for " + mPackage.packageName); writer.backupManifest( mPackage, @@ -223,9 +219,7 @@ public class KeyValueAdbBackupEngine { /* withApk */ false); mManifestFile.delete(); - if (DEBUG) { - Slog.d(TAG, "Writing key-value package payload" + mPackage.packageName); - } + Slog.d(TAG, "Writing key-value package payload" + mPackage.packageName); FullBackup.backupToTar(mPackage.packageName, FullBackup.KEY_VALUE_DATA_TOKEN, null, mDataDir.getAbsolutePath(), mBackupDataName.getAbsolutePath(), @@ -283,9 +277,7 @@ public class KeyValueAdbBackupEngine { if (!mBackupManagerService.waitUntilOperationComplete(token)) { Slog.e(TAG, "Full backup failed on package " + mCurrentPackage.packageName); } else { - if (DEBUG) { - Slog.d(TAG, "Full package backup success: " + mCurrentPackage.packageName); - } + Slog.d(TAG, "Full package backup success: " + mCurrentPackage.packageName); } } catch (IOException e) { Slog.e(TAG, "Error backing up " + mCurrentPackage.packageName + ": " + e); diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java index 3184bd87601a..b68a0e42908b 100644 --- a/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java +++ b/services/backup/java/com/android/server/backup/KeyValueAdbRestoreEngine.java @@ -89,10 +89,8 @@ public class KeyValueAdbRestoreEngine implements Runnable { ParcelFileDescriptor newState = ParcelFileDescriptor.open(newStateName, MODE_READ_WRITE | MODE_CREATE | MODE_TRUNCATE); - if (DEBUG) { - Slog.i(TAG, "Starting restore of package " + pkg + " for version code " + Slog.i(TAG, "Starting restore of package " + pkg + " for version code " + info.version); - } agent.doRestore(backupData, info.version, newState, mToken, mBackupManagerService.getBackupManagerBinder()); } catch (IOException e) { diff --git a/services/backup/java/com/android/server/backup/KeyValueBackupJob.java b/services/backup/java/com/android/server/backup/KeyValueBackupJob.java index 9a788be2f46d..30fdb65fc3cf 100644 --- a/services/backup/java/com/android/server/backup/KeyValueBackupJob.java +++ b/services/backup/java/com/android/server/backup/KeyValueBackupJob.java @@ -16,7 +16,6 @@ package com.android.server.backup; -import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.app.AlarmManager; @@ -92,9 +91,7 @@ public class KeyValueBackupJob extends JobService { if (delay <= 0) { delay = interval + new Random().nextInt((int) fuzz); } - if (DEBUG_SCHEDULING) { - Slog.v(TAG, "Scheduling k/v pass in " + (delay / 1000 / 60) + " minutes"); - } + Slog.d(TAG, "Scheduling k/v pass in " + (delay / 1000 / 60) + " minutes"); JobInfo.Builder builder = new JobInfo.Builder(getJobIdForUserId(userId), sKeyValueJobService) diff --git a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java index 52108bf1568d..e17063ab7803 100644 --- a/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java +++ b/services/backup/java/com/android/server/backup/PackageManagerBackupAgent.java @@ -240,7 +240,7 @@ public class PackageManagerBackupAgent extends BackupAgent { try { if (!mExisting.contains(ANCESTRAL_RECORD_KEY)) { // The old state does not store info on ancestral record - Slog.v( + Slog.d( TAG, "No ancestral record version in the old state. Storing " + "ancestral record version key"); @@ -249,7 +249,7 @@ public class PackageManagerBackupAgent extends BackupAgent { upgradingAncestralRecordVersion = true; } else if (mStoredAncestralRecordVersion != ANCESTRAL_RECORD_VERSION) { // The current ancestral record version has changed from the old state - Slog.v( + Slog.d( TAG, "Ancestral record version has changed from old state. Storing" + "ancestral record version key"); diff --git a/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java b/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java index edc2379ff641..3a6f228a7f6f 100644 --- a/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java +++ b/services/backup/java/com/android/server/backup/ProcessedPackagesJournal.java @@ -46,7 +46,6 @@ import java.util.Set; final class ProcessedPackagesJournal { private static final String TAG = "ProcessedPackagesJournal"; private static final String JOURNAL_FILE_NAME = "processed"; - private static final boolean DEBUG = BackupManagerService.DEBUG; // using HashSet instead of ArraySet since we expect 100-500 elements range @GuardedBy("mProcessedPackages") @@ -136,9 +135,7 @@ final class ProcessedPackagesJournal { new BufferedInputStream(new FileInputStream(journalFile)))) { while (true) { String packageName = oldJournal.readUTF(); - if (DEBUG) { - Slog.v(TAG, " + " + packageName); - } + Slog.d(TAG, " + " + packageName); mProcessedPackages.add(packageName); } } catch (EOFException e) { diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java index a792db0f7c78..d33bfecf1ba9 100644 --- a/services/backup/java/com/android/server/backup/TransportManager.java +++ b/services/backup/java/com/android/server/backup/TransportManager.java @@ -62,7 +62,7 @@ import java.util.function.Predicate; /** Handles in-memory bookkeeping of all BackupTransport objects. */ public class TransportManager { private static final String TAG = "BackupTransportManager"; - private static final boolean MORE_DEBUG = false; + private static final boolean DEBUG = false; @VisibleForTesting public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST"; @@ -155,7 +155,7 @@ public class TransportManager { enabled = mPackageManager.getApplicationEnabledSetting(packageName); } catch (IllegalArgumentException ex) { // packageName doesn't exist: likely due to a race with it being uninstalled. - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName + " was changed, but no longer exists.")); } @@ -163,7 +163,7 @@ public class TransportManager { } switch (enabled) { case COMPONENT_ENABLED_STATE_ENABLED: { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName + " was enabled.")); } @@ -174,7 +174,7 @@ public class TransportManager { // Package is set to its default enabled state (as specified in its manifest). // Unless explicitly specified in manifest, the default enabled state // is 'enabled'. Here, we assume that default state always means enabled. - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName + " was put in default enabled state.")); } @@ -182,7 +182,7 @@ public class TransportManager { return; } case COMPONENT_ENABLED_STATE_DISABLED: { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName + " was disabled.")); } @@ -190,7 +190,7 @@ public class TransportManager { return; } case COMPONENT_ENABLED_STATE_DISABLED_USER: { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Package " + packageName + " was disabled by user.")); } diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index ac1f50f85d64..5af2346650ed 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -19,8 +19,6 @@ package com.android.server.backup; import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.internal.BackupHandler.MSG_BACKUP_OPERATION_TIMEOUT; import static com.android.server.backup.internal.BackupHandler.MSG_FULL_CONFIRMATION_TIMEOUT; @@ -88,7 +86,6 @@ import android.os.RemoteException; import android.os.SELinux; import android.os.SystemClock; import android.os.UserHandle; -import android.os.WorkSource; import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; @@ -174,88 +171,6 @@ import java.util.concurrent.atomic.AtomicInteger; /** System service that performs backup/restore operations. */ public class UserBackupManagerService { - /** - * Wrapper over {@link PowerManager.WakeLock} to prevent double-free exceptions on release() - * after quit(). - */ - public static class BackupWakeLock { - private final PowerManager.WakeLock mPowerManagerWakeLock; - private boolean mHasQuit = false; - private final int mUserId; - private final BackupManagerConstants mBackupManagerConstants; - - public BackupWakeLock(PowerManager.WakeLock powerManagerWakeLock, int userId, - BackupManagerConstants backupManagerConstants) { - mPowerManagerWakeLock = powerManagerWakeLock; - mUserId = userId; - mBackupManagerConstants = backupManagerConstants; - } - - /** Acquires the {@link PowerManager.WakeLock} if hasn't been quit. */ - public synchronized void acquire() { - if (mHasQuit) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, - "Ignore wakelock acquire after quit: " - + mPowerManagerWakeLock.getTag())); - return; - } - // Set a timeout for the wakelock. Otherwise if we fail internally and never call - // release(), the device might stay awake and drain battery indefinitely. - mPowerManagerWakeLock.acquire(mBackupManagerConstants.getWakelockTimeoutMillis()); - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, "Acquired wakelock:" + mPowerManagerWakeLock.getTag())); - } - - /** Releases the {@link PowerManager.WakeLock} if hasn't been quit. */ - public synchronized void release() { - if (mHasQuit) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, - "Ignore wakelock release after quit: " - + mPowerManagerWakeLock.getTag())); - return; - } - - if (!mPowerManagerWakeLock.isHeld()) { - Slog.w(TAG, addUserIdToLogMessage(mUserId, - "Wakelock not held: " + mPowerManagerWakeLock.getTag())); - return; - } - - mPowerManagerWakeLock.release(); - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, "Released wakelock:" + mPowerManagerWakeLock.getTag())); - } - - /** - * Returns true if the {@link PowerManager.WakeLock} has been acquired but not yet released. - */ - public synchronized boolean isHeld() { - return mPowerManagerWakeLock.isHeld(); - } - - /** Release the {@link PowerManager.WakeLock} till it isn't held. */ - public synchronized void quit() { - while (mPowerManagerWakeLock.isHeld()) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, "Releasing wakelock: " + mPowerManagerWakeLock.getTag())); - mPowerManagerWakeLock.release(); - } - mHasQuit = true; - } - } - // Persistently track the need to do a full init. private static final String INIT_SENTINEL_FILE_NAME = "_need_init_"; @@ -436,11 +351,7 @@ public class UserBackupManagerService { currentTransport = null; } - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage(userId, "Starting with transport " + currentTransport)); - } + Slog.d(TAG, addUserIdToLogMessage(userId, "Starting with transport " + currentTransport)); TransportManager transportManager = new TransportManager(userId, context, transportWhitelist, currentTransport); @@ -450,11 +361,7 @@ public class UserBackupManagerService { HandlerThread userBackupThread = new HandlerThread("backup-" + userId, Process.THREAD_PRIORITY_BACKGROUND); userBackupThread.start(); - if (DEBUG) { - Slog.d( - TAG, - addUserIdToLogMessage(userId, "Started thread " + userBackupThread.getName())); - } + Slog.d(TAG, addUserIdToLogMessage(userId, "Started thread " + userBackupThread.getName())); return createAndInitializeService( userId, @@ -491,7 +398,7 @@ public class UserBackupManagerService { // if so delete expired events and do not print them to dumpsys BackupManagerMonitorDumpsysUtils backupManagerMonitorDumpsysUtils = new BackupManagerMonitorDumpsysUtils(); - if (backupManagerMonitorDumpsysUtils.deleteExpiredBMMEvents() && DEBUG){ + if (backupManagerMonitorDumpsysUtils.deleteExpiredBMMEvents()) { Slog.d(TAG, "BMM Events recorded for dumpsys have expired"); } return new UserBackupManagerService( @@ -766,20 +673,10 @@ public class UserBackupManagerService { mSetupComplete = setupComplete; } - public BackupWakeLock getWakelock() { + public BackupWakeLock getWakeLock() { return mWakelock; } - /** - * Sets the {@link WorkSource} of the {@link PowerManager.WakeLock} returned by {@link - * #getWakelock()}. - */ - @VisibleForTesting - public void setWorkSource(@Nullable WorkSource workSource) { - // TODO: This is for testing, unfortunately WakeLock is final and WorkSource is not exposed - mWakelock.mPowerManagerWakeLock.setWorkSource(workSource); - } - public Handler getBackupHandler() { return mBackupHandler; } @@ -937,7 +834,7 @@ public class UserBackupManagerService { } private void initPackageTracking() { - if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "` tracking")); + if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "` tracking")); // Remember our ancestral dataset mTokenFile = new File(mBaseStateDir, "ancestral"); @@ -959,7 +856,7 @@ public class UserBackupManagerService { } } catch (FileNotFoundException fnf) { // Probably innocuous - Slog.v(TAG, addUserIdToLogMessage(mUserId, "No ancestral data")); + Slog.d(TAG, addUserIdToLogMessage(mUserId, "No ancestral data")); } catch (IOException e) { Slog.w(TAG, addUserIdToLogMessage(mUserId, "Unable to read token file"), e); } @@ -1038,16 +935,12 @@ public class UserBackupManagerService { pkg.applicationInfo)) { schedule.add(new FullBackupEntry(pkgName, lastBackup)); } else { - if (DEBUG) { - Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName + Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName + " no longer eligible for full backup")); - } } } catch (NameNotFoundException e) { - if (DEBUG) { - Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName + Slog.i(TAG, addUserIdToLogMessage(mUserId, "Package " + pkgName + " not installed; dropping from full backup")); - } } } @@ -1058,7 +951,7 @@ public class UserBackupManagerService { && mScheduledBackupEligibility.appIsEligibleForBackup( app.applicationInfo)) { if (!foundApps.contains(app.packageName)) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1169,7 +1062,7 @@ public class UserBackupManagerService { if (!packageNames.isEmpty()) { String msg = "Stale backup journals: Scheduled " + packageNames.size() + " package(s) total"; - if (MORE_DEBUG) { + if (DEBUG) { msg += ": " + packageNames; } Slog.i(TAG, addUserIdToLogMessage(mUserId, msg)); @@ -1209,7 +1102,7 @@ public class UserBackupManagerService { public void recordInitPending( boolean isPending, String transportName, String transportDirName) { synchronized (mQueueLock) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1276,20 +1169,11 @@ public class UserBackupManagerService { } private void onTransportRegistered(String transportName, String transportDirName) { - if (DEBUG) { - long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime; - Slog.d( - TAG, - addUserIdToLogMessage( - mUserId, - "Transport " - + transportName - + " registered " - + timeMs - + "ms after first request (delay = " - + INITIALIZATION_DELAY_MILLIS - + "ms)")); - } + long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime; + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "Transport " + transportName + " registered " + timeMs + + "ms after first request (delay = " + INITIALIZATION_DELAY_MILLIS + + "ms)")); File stateDir = new File(mBaseStateDir, transportDirName); stateDir.mkdirs(); @@ -1313,7 +1197,7 @@ public class UserBackupManagerService { */ private BroadcastReceiver mPackageTrackingReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Received broadcast " + intent)); } @@ -1344,7 +1228,7 @@ public class UserBackupManagerService { intent.getStringArrayExtra( Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1416,13 +1300,8 @@ public class UserBackupManagerService { mBackupHandler.post( () -> mTransportManager.onPackageAdded(packageName)); } catch (NameNotFoundException e) { - if (DEBUG) { - Slog.w( - TAG, - addUserIdToLogMessage( - mUserId, - "Can't resolve new app " + packageName)); - } + Slog.w(TAG, addUserIdToLogMessage(mUserId, + "Can't resolve new app " + packageName)); } } @@ -1454,7 +1333,7 @@ public class UserBackupManagerService { // Look for apps that define the android:backupAgent attribute List<PackageInfo> targetApps = allAgentPackages(); if (packageNames != null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -1464,7 +1343,7 @@ public class UserBackupManagerService { addPackageParticipantsLockedInner(packageName, targetApps); } } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, addUserIdToLogMessage(mUserId, "addPackageParticipantsLocked: all")); } addPackageParticipantsLockedInner(null, targetApps); @@ -1473,7 +1352,7 @@ public class UserBackupManagerService { private void addPackageParticipantsLockedInner(String packageName, List<PackageInfo> targetPkgs) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -1489,10 +1368,10 @@ public class UserBackupManagerService { mBackupParticipants.put(uid, set); } set.add(pkg.packageName); - if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Agent found; added")); + if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Agent found; added")); // Schedule a backup for it on general principles - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1512,7 +1391,7 @@ public class UserBackupManagerService { return; } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -1528,7 +1407,7 @@ public class UserBackupManagerService { if (set != null && set.contains(pkg)) { removePackageFromSetLocked(set, pkg); if (set.isEmpty()) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -1549,7 +1428,7 @@ public class UserBackupManagerService { // Note that we deliberately leave it 'known' in the "ever backed up" // bookkeeping so that its current-dataset data will be retrieved // if the app is subsequently reinstalled - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage(mUserId, " removing participant " + packageName)); @@ -1627,15 +1506,13 @@ public class UserBackupManagerService { af.writeInt(-1); } else { af.writeInt(mAncestralPackages.size()); - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, "Ancestral packages: " + mAncestralPackages.size())); - } + Slog.d( + TAG, + addUserIdToLogMessage( + mUserId, "Ancestral packages: " + mAncestralPackages.size())); for (String pkgName : mAncestralPackages) { af.writeUTF(pkgName); - if (MORE_DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, " " + pkgName)); + if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, " " + pkgName)); } } } catch (IOException e) { @@ -1686,7 +1563,7 @@ public class UserBackupManagerService { } if (!shouldClearData) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1761,7 +1638,7 @@ public class UserBackupManagerService { long token = mAncestralToken; synchronized (mQueueLock) { if (mCurrentToken != 0 && mProcessedPackagesJournal.hasBeenProcessed(packageName)) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1770,7 +1647,7 @@ public class UserBackupManagerService { token = mCurrentToken; } } - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, addUserIdToLogMessage(mUserId, "getAvailableRestoreToken() == " + token)); } return token; @@ -1885,7 +1762,7 @@ public class UserBackupManagerService { EventLog.writeEvent(EventLogTags.BACKUP_REQUESTED, packages.length, kvBackupList.size(), fullBackupList.size()); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -1909,7 +1786,7 @@ public class UserBackupManagerService { /** Cancel all running backups. */ public void cancelBackups() { mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "cancelBackups"); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, addUserIdToLogMessage(mUserId, "cancelBackups() called.")); } final long oldToken = Binder.clearCallingIdentity(); @@ -1946,7 +1823,7 @@ public class UserBackupManagerService { + operationType)); return; } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -2025,12 +1902,8 @@ public class UserBackupManagerService { FullBackupJob.schedule(mUserId, mContext, latency, /* userBackupManagerService */ this); } else { - if (DEBUG_SCHEDULING) { - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, "Full backup queue empty; not scheduling")); - } + Slog.i(TAG, + addUserIdToLogMessage(mUserId, "Full backup queue empty; not scheduling")); } } } @@ -2098,13 +1971,8 @@ public class UserBackupManagerService { File stateDir = new File(mBaseStateDir, transportDirName); File pmState = new File(stateDir, PACKAGE_MANAGER_SENTINEL); if (pmState.length() <= 0) { - if (DEBUG) { - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, - "Full backup requested but dataset not yet initialized")); - } + Slog.i(TAG, addUserIdToLogMessage(mUserId, + "Full backup requested but dataset not yet initialized")); return false; } } catch (Exception e) { @@ -2143,7 +2011,7 @@ public class UserBackupManagerService { // Backups are globally disabled, so don't proceed. We also don't reschedule // the job driving automatic backups; that job will be scheduled again when // the user enables backup. - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, addUserIdToLogMessage(mUserId, "beginFullBackup but enabled=" + mEnabled + " setupComplete=" + mSetupComplete + "; ignoring")); } @@ -2155,22 +2023,14 @@ public class UserBackupManagerService { final PowerSaveState result = mPowerManager.getPowerSaveState(ServiceType.FULL_BACKUP); if (result.batterySaverEnabled) { - if (DEBUG) { - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, "Deferring scheduled full backups in battery saver mode")); - } + Slog.i(TAG, addUserIdToLogMessage(mUserId, + "Deferring scheduled full backups in battery saver mode")); FullBackupJob.schedule(mUserId, mContext, keyValueBackupInterval, /* userBackupManagerService */ this); return false; } - if (DEBUG_SCHEDULING) { - Slog.i( - TAG, - addUserIdToLogMessage(mUserId, "Beginning scheduled full backup operation")); - } + Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning scheduled full backup operation")); // Great; we're able to run full backup jobs now. See if we have any work to do. synchronized (mQueueLock) { @@ -2193,12 +2053,8 @@ public class UserBackupManagerService { // have emptied the queue. if (mFullBackupQueue.size() == 0) { // no work to do so just bow out - if (DEBUG) { - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, "Backup queue empty; doing nothing")); - } + Slog.i(TAG, + addUserIdToLogMessage(mUserId, "Backup queue empty; doing nothing")); runBackup = false; break; } @@ -2207,7 +2063,7 @@ public class UserBackupManagerService { String transportName = mTransportManager.getCurrentTransportName(); if (!fullBackupAllowable(transportName)) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -2226,7 +2082,7 @@ public class UserBackupManagerService { runBackup = (timeSinceRun >= fullBackupInterval); if (!runBackup) { // It's too early to back up the next thing in the queue, so bow out - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -2245,7 +2101,7 @@ public class UserBackupManagerService { // The head app isn't supposed to get full-data backups [any more]; // so we cull it and force a loop around to consider the new head // app. - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, addUserIdToLogMessage( @@ -2269,17 +2125,11 @@ public class UserBackupManagerService { final long nextEligible = System.currentTimeMillis() + BUSY_BACKOFF_MIN_MILLIS + mTokenGenerator.nextInt(BUSY_BACKOFF_FUZZ); - if (DEBUG_SCHEDULING) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, - "Full backup time but " - + entry.packageName - + " is busy; deferring to " - + sdf.format(new Date(nextEligible)))); - } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Slog.i(TAG, addUserIdToLogMessage(mUserId, + "Full backup time but " + entry.packageName + + " is busy; deferring to " + sdf.format( + new Date(nextEligible)))); // This relocates the app's entry from the head of the queue to // its order-appropriate position further down, so upon looping // a new candidate will be considered at the head. @@ -2319,14 +2169,9 @@ public class UserBackupManagerService { } if (!runBackup) { - if (DEBUG_SCHEDULING) { - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, - "Nothing pending full backup or failed to start the " - + "operation; rescheduling +" + latency)); - } + Slog.i(TAG, addUserIdToLogMessage(mUserId, + "Nothing pending full backup or failed to start the " + + "operation; rescheduling +" + latency)); final long deferTime = latency; // pin for the closure FullBackupJob.schedule(mUserId, mContext, deferTime, /* userBackupManagerService */ this); @@ -2360,12 +2205,7 @@ public class UserBackupManagerService { } } if (pftbt != null) { - if (DEBUG_SCHEDULING) { - Slog.i( - TAG, - addUserIdToLogMessage( - mUserId, "Telling running backup to stop")); - } + Slog.i(TAG, addUserIdToLogMessage(mUserId, "Telling running backup to stop")); pftbt.handleCancel(true); } } @@ -2376,7 +2216,7 @@ public class UserBackupManagerService { /** Used by both incremental and full restore to restore widget data. */ public void restoreWidgetData(String packageName, byte[] widgetData) { // Apply the restored widget state and generate the ID update for the app - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, addUserIdToLogMessage(mUserId, "Incorporating restored widget data")); } AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, mUserId); @@ -2412,7 +2252,7 @@ public class UserBackupManagerService { // one already there, then overwrite it, but no harm done. BackupRequest req = new BackupRequest(packageName); if (mPendingBackups.put(packageName, req) == null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage( @@ -2495,7 +2335,7 @@ public class UserBackupManagerService { public void initializeTransports(String[] transportNames, IBackupObserver observer) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "initializeTransport"); - Slog.v( + Slog.d( TAG, addUserIdToLogMessage( mUserId, "initializeTransport(): " + Arrays.asList(transportNames))); @@ -2517,7 +2357,7 @@ public class UserBackupManagerService { public void setAncestralSerialNumber(long ancestralSerialNumber) { mContext.enforceCallingPermission(android.Manifest.permission.BACKUP, "setAncestralSerialNumber"); - Slog.v( + Slog.d( TAG, addUserIdToLogMessage( mUserId, "Setting ancestral work profile id to " + ancestralSerialNumber)); @@ -2571,13 +2411,8 @@ public class UserBackupManagerService { /** Clear the given package's backup data from the current transport. */ public void clearBackupData(String transportName, String packageName) { - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, - "clearBackupData() of " + packageName + " on " + transportName)); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "clearBackupData() of " + packageName + " on " + transportName)); PackageInfo info; try { @@ -2601,7 +2436,7 @@ public class UserBackupManagerService { } else { // a caller with full permission can ask to back up any participating app // !!! TODO: allow data-clear of ANY app? - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -2612,7 +2447,7 @@ public class UserBackupManagerService { if (apps.contains(packageName)) { // found it; fire off the clear request - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage(mUserId, "Found the app - running clear process")); @@ -2657,25 +2492,19 @@ public class UserBackupManagerService { final PowerSaveState result = mPowerManager.getPowerSaveState(ServiceType.KEYVALUE_BACKUP); if (result.batterySaverEnabled) { - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, "Not running backup while in battery save mode")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "Not running backup while in battery save mode")); // Try again in several hours. KeyValueBackupJob.schedule(mUserId, mContext, /* userBackupManagerService */ this); } else { - if (DEBUG) { - Slog.v(TAG, addUserIdToLogMessage(mUserId, "Scheduling immediate backup pass")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, "Scheduling immediate backup pass")); synchronized (getQueueLock()) { if (getPendingInits().size() > 0) { // If there are pending init operations, we process those and then settle // into the usual periodic backup schedule. - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -2749,26 +2578,11 @@ public class UserBackupManagerService { return; } - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, - "Requesting backup: apks=" - + includeApks - + " obb=" - + includeObbs - + " shared=" - + includeShared - + " all=" - + doAllApps - + " system=" - + includeSystem - + " includekeyvalue=" - + doKeyValue - + " pkgs=" - + Arrays.toString(pkgList))); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "Requesting backup: apks=" + includeApks + " obb=" + includeObbs + " shared=" + + includeShared + " all=" + doAllApps + " system=" + includeSystem + + " includekeyvalue=" + doKeyValue + " pkgs=" + Arrays.toString( + pkgList))); Slog.i(TAG, addUserIdToLogMessage(mUserId, "Beginning adb backup...")); BackupEligibilityRules eligibilityRules = getEligibilityRulesForOperation( @@ -2782,11 +2596,7 @@ public class UserBackupManagerService { } // start up the confirmation UI - if (DEBUG) { - Slog.d( - TAG, - addUserIdToLogMessage(mUserId, "Starting backup confirmation UI")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, "Starting backup confirmation UI")); if (!startConfirmationUi(token, FullBackup.FULL_BACKUP_INTENT_ACTION)) { Slog.e( TAG, @@ -2804,9 +2614,7 @@ public class UserBackupManagerService { startConfirmationTimeout(token, params); // wait for the backup to be performed - if (DEBUG) { - Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for backup completion...")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for backup completion...")); waitForCompletion(params); } finally { try { @@ -2841,9 +2649,7 @@ public class UserBackupManagerService { mUserId, "Full backup not currently possible -- key/value backup not yet run?")); } else { - if (DEBUG) { - Slog.d(TAG, addUserIdToLogMessage(mUserId, "fullTransportBackup()")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, "fullTransportBackup()")); final long oldId = Binder.clearCallingIdentity(); try { @@ -2886,9 +2692,7 @@ public class UserBackupManagerService { } } - if (DEBUG) { - Slog.d(TAG, addUserIdToLogMessage(mUserId, "Done with full transport backup.")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, "Done with full transport backup.")); } /** @@ -2922,12 +2726,8 @@ public class UserBackupManagerService { } // start up the confirmation UI - if (DEBUG) { - Slog.d( - TAG, - addUserIdToLogMessage( - mUserId, "Starting restore confirmation UI, token=" + token)); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "Starting restore confirmation UI, token=" + token)); if (!startConfirmationUi(token, FullBackup.FULL_RESTORE_INTENT_ACTION)) { Slog.e( TAG, @@ -2945,9 +2745,7 @@ public class UserBackupManagerService { startConfirmationTimeout(token, params); // wait for the restore to be performed - if (DEBUG) { - Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for restore completion...")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, "Waiting for restore completion...")); waitForCompletion(params); } finally { try { @@ -3022,7 +2820,7 @@ public class UserBackupManagerService { } private void startConfirmationTimeout(int token, AdbParams params) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, addUserIdToLogMessage(mUserId, "Posting conf timeout msg after " + TIMEOUT_FULL_CONFIRMATION + " millis")); } @@ -3055,13 +2853,8 @@ public class UserBackupManagerService { */ public void acknowledgeAdbBackupOrRestore(int token, boolean allow, String curPassword, String encPpassword, IFullBackupRestoreObserver observer) { - if (DEBUG) { - Slog.d( - TAG, - addUserIdToLogMessage( - mUserId, - "acknowledgeAdbBackupOrRestore : token=" + token + " allow=" + allow)); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "acknowledgeAdbBackupOrRestore : token=" + token + " allow=" + allow)); // TODO: possibly require not just this signature-only permission, but even // require that the specific designated confirmation-UI app uid is the caller? @@ -3088,7 +2881,7 @@ public class UserBackupManagerService { params.encryptPassword = encPpassword; - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage( @@ -3198,7 +2991,7 @@ public class UserBackupManagerService { scheduleNextFullBackupJob(0); } else if (!enable) { // No longer enabled, so stop running backups - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, addUserIdToLogMessage(mUserId, "Opting out of backup")); } @@ -3285,7 +3078,7 @@ public class UserBackupManagerService { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "getCurrentTransport"); String currentTransport = mTransportManager.getCurrentTransportName(); - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, addUserIdToLogMessage( @@ -3429,7 +3222,7 @@ public class UserBackupManagerService { final long oldId = Binder.clearCallingIdentity(); try { if (!mTransportManager.isTransportRegistered(transportName)) { - Slog.v( + Slog.d( TAG, addUserIdToLogMessage( mUserId, @@ -3441,7 +3234,7 @@ public class UserBackupManagerService { String previousTransportName = mTransportManager.selectTransport(transportName); updateStateForTransport(transportName); - Slog.v( + Slog.d( TAG, addUserIdToLogMessage( mUserId, @@ -3467,7 +3260,7 @@ public class UserBackupManagerService { final long oldId = Binder.clearCallingIdentity(); try { String transportString = transportComponent.flattenToShortString(); - Slog.v( + Slog.d( TAG, addUserIdToLogMessage( mUserId, @@ -3562,7 +3355,7 @@ public class UserBackupManagerService { "getConfigurationIntent"); try { Intent intent = mTransportManager.getTransportConfigurationIntent(transportName); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage( @@ -3595,7 +3388,7 @@ public class UserBackupManagerService { try { String string = mTransportManager.getTransportCurrentDestinationString(transportName); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage( @@ -3619,7 +3412,7 @@ public class UserBackupManagerService { try { Intent intent = mTransportManager.getTransportDataManagementIntent(transportName); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage( @@ -3646,7 +3439,7 @@ public class UserBackupManagerService { try { CharSequence label = mTransportManager.getTransportDataManagementLabel(transportName); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage( @@ -3682,20 +3475,11 @@ public class UserBackupManagerService { boolean skip = false; long restoreSet = getAvailableRestoreToken(packageName); - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, - "restoreAtInstall pkg=" - + packageName - + " token=" - + Integer.toHexString(token) - + " restoreSet=" - + Long.toHexString(restoreSet))); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "restoreAtInstall pkg=" + packageName + " token=" + Integer.toHexString(token) + + " restoreSet=" + Long.toHexString(restoreSet))); if (restoreSet == 0) { - if (MORE_DEBUG) Slog.i(TAG, addUserIdToLogMessage(mUserId, "No restore set")); + if (DEBUG) Slog.i(TAG, addUserIdToLogMessage(mUserId, "No restore set")); skip = true; } @@ -3705,7 +3489,7 @@ public class UserBackupManagerService { TransportConnection transportConnection = mTransportManager.getCurrentTransportClient("BMS.restoreAtInstall()"); if (transportConnection == null) { - if (DEBUG) Slog.w(TAG, addUserIdToLogMessage(mUserId, "No transport client")); + Slog.w(TAG, addUserIdToLogMessage(mUserId, "No transport client")); skip = true; } else if (Flags.enableIncreasedBmmLoggingForRestoreAtInstall()) { try { @@ -3729,12 +3513,8 @@ public class UserBackupManagerService { } if (!mAutoRestore) { - if (DEBUG) { - Slog.w( - TAG, - addUserIdToLogMessage( - mUserId, "Non-restorable state: auto=" + mAutoRestore)); - } + Slog.w(TAG, + addUserIdToLogMessage(mUserId, "Non-restorable state: auto=" + mAutoRestore)); skip = true; } @@ -3751,7 +3531,7 @@ public class UserBackupManagerService { mWakelock.release(); }; - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, addUserIdToLogMessage(mUserId, "Restore at install of " + packageName)); @@ -3796,7 +3576,7 @@ public class UserBackupManagerService { } // Tell the PackageManager to proceed with the post-install handling for this package. - if (DEBUG) Slog.v(TAG, addUserIdToLogMessage(mUserId, "Finishing install immediately")); + Slog.d(TAG, addUserIdToLogMessage(mUserId, "Finishing install immediately")); try { mPackageManagerBinder.finishPackageInstall(token, false); } catch (RemoteException e) { /* can't happen */ } @@ -3812,13 +3592,8 @@ public class UserBackupManagerService { /** Hand off a restore session. */ public IRestoreSession beginRestoreSession(String packageName, String transport) { - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, - "beginRestoreSession: pkg=" + packageName + " transport=" + transport)); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "beginRestoreSession: pkg=" + packageName + " transport=" + transport)); boolean needPermission = true; if (transport == null) { @@ -3849,13 +3624,8 @@ public class UserBackupManagerService { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.BACKUP, "beginRestoreSession"); } else { - if (DEBUG) { - Slog.d( - TAG, - addUserIdToLogMessage( - mUserId, - "restoring self on current transport; no permission needed")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "restoring self on current transport; no permission needed")); } int backupDestination; @@ -3905,12 +3675,8 @@ public class UserBackupManagerService { if (currentSession != mActiveRestoreSession) { Slog.e(TAG, addUserIdToLogMessage(mUserId, "ending non-current restore session")); } else { - if (DEBUG) { - Slog.v( - TAG, - addUserIdToLogMessage( - mUserId, "Clearing restore session and halting timeout")); - } + Slog.d(TAG, addUserIdToLogMessage(mUserId, + "Clearing restore session and halting timeout")); mActiveRestoreSession = null; mBackupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT); } diff --git a/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java b/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java index d13f71116c81..1cd8d8146d92 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java +++ b/services/backup/java/com/android/server/backup/fullbackup/AppMetadataBackupWriter.java @@ -1,6 +1,6 @@ package com.android.server.backup.fullbackup; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; +import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION; import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_VERSION; @@ -261,7 +261,7 @@ public class AppMetadataBackupWriter { new Environment.UserEnvironment(userId); File obbDir = userEnv.buildExternalStorageAppObbDirs(packageInfo.packageName)[0]; if (obbDir != null) { - if (MORE_DEBUG) { + if (DEBUG) { Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath()); } File[] obbFiles = obbDir.listFiles(); diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java index cf617a523bec..ebb1194c7c4a 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java +++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java @@ -17,7 +17,6 @@ package com.android.server.backup.fullbackup; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME; import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME; @@ -111,7 +110,7 @@ public class FullBackupEngine { shouldWriteApk(mPackage.applicationInfo, mIncludeApks, isSharedStorage); if (!isSharedStorage) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Writing manifest for " + packageName); } @@ -137,9 +136,7 @@ public class FullBackupEngine { appMetadataBackupWriter.backupObb(mUserId, mPackage); } - if (DEBUG) { - Slog.d(TAG, "Calling doFullBackup() on " + packageName); - } + Slog.d(TAG, "Calling doFullBackup() on " + packageName); long timeout = isSharedStorage @@ -216,14 +213,14 @@ public class FullBackupEngine { public int preflightCheck() throws RemoteException { if (mPreflightHook == null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "No preflight check"); } return BackupTransport.TRANSPORT_OK; } if (initializeAgent()) { int result = mPreflightHook.preflightFullBackup(mPkg, mAgent); - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "preflight returned " + result); } @@ -262,7 +259,7 @@ public class FullBackupEngine { if (!backupManagerService.waitUntilOperationComplete(mOpToken)) { Slog.e(TAG, "Full backup failed on package " + mPkg.packageName); } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Full package backup success: " + mPkg.packageName); } result = BackupTransport.TRANSPORT_OK; @@ -310,7 +307,7 @@ public class FullBackupEngine { private boolean initializeAgent() { if (mAgent == null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName); } mAgent = diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java index be6ac26ba92a..93499b9d763b 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java +++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupObbConnection.java @@ -16,7 +16,7 @@ package com.android.server.backup.fullbackup; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; +import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import android.app.backup.IBackupManager; @@ -58,7 +58,7 @@ public class FullBackupObbConnection implements ServiceConnection { } public void establish() { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Initiating bind of OBB service on " + this); } Intent obbIntent = new Intent().setComponent(new ComponentName( @@ -124,14 +124,14 @@ public class FullBackupObbConnection implements ServiceConnection { private void waitForConnection() { synchronized (this) { while (mService == null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "...waiting for OBB service binding..."); } try { this.wait(); } catch (InterruptedException e) { /* never interrupted */ } } - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Connected to OBB service; continuing"); } } @@ -141,7 +141,7 @@ public class FullBackupObbConnection implements ServiceConnection { public void onServiceConnected(ComponentName name, IBinder service) { synchronized (this) { mService = IObbBackupService.Stub.asInterface(service); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "OBB service connection " + mService + " connected on " + this); } this.notifyAll(); @@ -152,7 +152,7 @@ public class FullBackupObbConnection implements ServiceConnection { public void onServiceDisconnected(ComponentName name) { synchronized (this) { mService = null; - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "OBB service connection disconnected on " + this); } this.notifyAll(); diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java index 0ba0d710af38..0d4364e14e03 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformAdbBackupTask.java @@ -17,7 +17,6 @@ package com.android.server.backup.fullbackup; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT; import static com.android.server.backup.UserBackupManagerService.BACKUP_FILE_HEADER_MAGIC; @@ -121,7 +120,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor } else { mEncryptPassword = encryptPassword; } - if (MORE_DEBUG) { + if (DEBUG) { Slog.w(TAG, "Encrypting backup with passphrase=" + mEncryptPassword); } mCompress = doCompress; @@ -265,7 +264,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor List<String> pkgs = AppWidgetBackupBridge.getWidgetParticipants(UserHandle.USER_SYSTEM); if (pkgs != null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Adding widget participants to backup set:"); StringBuilder sb = new StringBuilder(128); sb.append(" "); @@ -297,16 +296,12 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor if (!mBackupEligibilityRules.appIsEligibleForBackup(pkg.applicationInfo) || mBackupEligibilityRules.appIsStopped(pkg.applicationInfo)) { iter.remove(); - if (DEBUG) { - Slog.i(TAG, "Package " + pkg.packageName + Slog.i(TAG, "Package " + pkg.packageName + " is not eligible for backup, removing."); - } } else if (mBackupEligibilityRules.appIsKeyValueOnly(pkg)) { iter.remove(); - if (DEBUG) { - Slog.i(TAG, "Package " + pkg.packageName + Slog.i(TAG, "Package " + pkg.packageName + " is key-value."); - } keyValueBackupQueue.add(pkg); } } @@ -326,9 +321,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor // Verify that the given password matches the currently-active // backup password, if any if (!mUserBackupManagerService.backupPasswordMatches(mCurrentPassword)) { - if (DEBUG) { - Slog.w(TAG, "Backup password mismatch; aborting"); - } + Slog.w(TAG, "Backup password mismatch; aborting"); return; } @@ -402,10 +395,8 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor int N = backupQueue.size(); for (int i = 0; i < N; i++) { pkg = backupQueue.get(i); - if (DEBUG) { - Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName + Slog.i(TAG, "--- Performing full backup for package " + pkg.packageName + " ---"); - } final boolean isSharedStorage = pkg.packageName.equals( SHARED_BACKUP_AGENT_PACKAGE); @@ -441,10 +432,8 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor // And for key-value backup if enabled if (mKeyValue) { for (PackageInfo keyValuePackage : keyValueBackupQueue) { - if (DEBUG) { - Slog.i(TAG, "--- Performing key-value backup for package " + Slog.i(TAG, "--- Performing key-value backup for package " + keyValuePackage.packageName + " ---"); - } KeyValueAdbBackupEngine kvBackupEngine = new KeyValueAdbBackupEngine(out, keyValuePackage, mUserBackupManagerService, @@ -478,10 +467,8 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor } sendEndBackup(); obbConnection.tearDown(); - if (DEBUG) { - Slog.d(TAG, "Full backup pass complete."); - } - mUserBackupManagerService.getWakelock().release(); + Slog.d(TAG, "Full backup pass complete."); + mUserBackupManagerService.getWakeLock().release(); } } @@ -499,9 +486,7 @@ public class PerformAdbBackupTask extends FullBackupTask implements BackupRestor @Override public void handleCancel(boolean cancelAll) { final PackageInfo target = mCurrentTarget; - if (DEBUG) { - Slog.w(TAG, "adb backup cancel of " + target); - } + Slog.w(TAG, "adb backup cancel of " + target); if (target != null) { mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent( target.applicationInfo, /* allowKill= */ true); diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index 990c9416e38d..bd34f33226a1 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -19,8 +19,6 @@ package com.android.server.backup.fullbackup; import static android.app.backup.BackupAnnotations.OperationType.BACKUP; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import android.annotation.Nullable; import android.app.IBackupAgent; @@ -201,9 +199,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mBackupEligibilityRules = backupEligibilityRules; if (backupManagerService.isBackupOperationInProgress()) { - if (DEBUG) { - Slog.d(TAG, "Skipping full backup. A backup is already in progress."); - } + Slog.d(TAG, "Skipping full backup. A backup is already in progress."); mCancelAll = true; return; } @@ -219,7 +215,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // that run as system-domain uids but do not define their own backup agents, // as well as any explicit mention of the 'special' shared-storage agent // package (we handle that one at the end). - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Ignoring ineligible package " + pkg); } mBackupManagerMonitorEventSender.monitorEvent( @@ -233,7 +229,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) { // Cull any packages that are found in the queue but now aren't supposed // to get full-data backup operations. - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Ignoring full-data backup of key/value participant " + pkg); } @@ -249,7 +245,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // Cull any packages in the 'stopped' state: they've either just been // installed or have explicitly been force-stopped by the user. In both // cases we do not want to launch them for backup. - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Ignoring stopped package " + pkg); } mBackupManagerMonitorEventSender.monitorEvent( @@ -346,12 +342,10 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (!mUserBackupManagerService.isEnabled() || !mUserBackupManagerService.isSetupComplete()) { // Backups are globally disabled, so don't proceed. - if (DEBUG) { - Slog.i(TAG, "full backup requested but enabled=" + mUserBackupManagerService - .isEnabled() - + " setupComplete=" + mUserBackupManagerService.isSetupComplete() - + "; ignoring"); - } + Slog.i(TAG, + "full backup requested but enabled=" + mUserBackupManagerService.isEnabled() + + " setupComplete=" + mUserBackupManagerService.isSetupComplete() + + "; ignoring"); int monitoringEvent; if (mUserBackupManagerService.isSetupComplete()) { monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_BACKUP_DISABLED; @@ -405,10 +399,8 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mBackupRunner = null; PackageInfo currentPackage = mPackages.get(i); String packageName = currentPackage.packageName; - if (DEBUG) { - Slog.i(TAG, "Initiating full-data transport backup of " + packageName - + " token: " + mCurrentOpToken); - } + Slog.i(TAG, "Initiating full-data transport backup of " + packageName + " token: " + + mCurrentOpToken); EventLog.writeEvent(EventLogTags.FULL_BACKUP_PACKAGE, packageName); transportPipes = ParcelFileDescriptor.createPipe(); @@ -462,7 +454,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba final long preflightResult = mBackupRunner.getPreflightResultBlocking(); // Preflight result is negative if some error happened on preflight. if (preflightResult < 0) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Backup error after preflight of package " + packageName + ": " + preflightResult + ", not running backup."); @@ -479,7 +471,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba int nRead = 0; do { nRead = in.read(buffer); - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "in.read(buffer) from app: " + nRead); } if (nRead > 0) { @@ -549,12 +541,12 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba backupPackageStatus = backupRunnerResult; } } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Transport-level failure; cancelling agent work"); } } - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Done delivering backup data: result=" + backupPackageStatus); } @@ -567,10 +559,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba // Also ask the transport how long it wants us to wait before // moving on to the next package, if any. backoff = transport.requestFullBackupTime(); - if (DEBUG_SCHEDULING) { - Slog.i(TAG, "Transport suggested backoff=" + backoff); - } - + Slog.i(TAG, "Transport suggested backoff=" + backoff); } // Roll this package to the end of the backup queue if we're @@ -581,13 +570,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } if (backupPackageStatus == BackupTransport.TRANSPORT_PACKAGE_REJECTED) { - BackupObserverUtils - .sendBackupOnPackageResult(mBackupObserver, packageName, - BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED); - if (DEBUG) { - Slog.i(TAG, "Transport rejected backup of " + packageName - + ", skipping"); - } + BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, packageName, + BackupManager.ERROR_TRANSPORT_PACKAGE_REJECTED); + Slog.i(TAG, "Transport rejected backup of " + packageName + ", skipping"); EventLog.writeEvent(EventLogTags.FULL_BACKUP_AGENT_FAILURE, packageName, "transport rejected"); // This failure state can come either a-priori from the transport, or @@ -602,11 +587,8 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba BackupObserverUtils .sendBackupOnPackageResult(mBackupObserver, packageName, BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED); - if (DEBUG) { - Slog.i(TAG, "Transport quota exceeded for package: " + packageName); - EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED, - packageName); - } + Slog.i(TAG, "Transport quota exceeded for package: " + packageName); + EventLog.writeEvent(EventLogTags.FULL_BACKUP_QUOTA_EXCEEDED, packageName); mUserBackupManagerService.getBackupAgentConnectionManager().unbindAgent( currentPackage.applicationInfo, /* allowKill= */ true); // Do nothing, clean up, and continue looping. @@ -675,9 +657,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba backupRunStatus = BackupManager.ERROR_BACKUP_CANCELLED; } - if (DEBUG) { - Slog.i(TAG, "Full backup completed with status: " + backupRunStatus); - } + Slog.i(TAG, "Full backup completed with status: " + backupRunStatus); BackupObserverUtils.sendBackupFinished(mBackupObserver, backupRunStatus); cleanUpPipes(transportPipes); @@ -708,7 +688,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba .getBackupAgentConnectionManager().clearNoRestrictedModePackages(); Slog.i(TAG, "Full data backup pass finished."); - mUserBackupManagerService.getWakelock().release(); + mUserBackupManagerService.getWakeLock().release(); } } @@ -781,7 +761,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba try { mUserBackupManagerService.prepareOperationTimeout( mCurrentOpToken, fullBackupAgentTimeoutMillis, this, OpType.BACKUP_WAIT); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Preflighting full payload of " + pkg.packageName); } agent.doMeasureFullBackup(mQuota, mCurrentOpToken, @@ -799,7 +779,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba if (totalSize < 0) { return (int) totalSize; } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Got preflight response; size=" + totalSize); } @@ -807,7 +787,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba mTransportConnection.connectOrThrow("PFTBT$SPBP.preflightFullBackup()"); result = transport.checkFullBackupSize(totalSize); if (result == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Package hit quota limit on preflight " + pkg.packageName + ": " + totalSize + " of " + mQuota); } @@ -830,7 +810,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba @Override public void operationComplete(long result) { // got the callback, and our preflightFullBackup() method is waiting for the result - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Preflight op complete, result=" + result); } mResult.set(result); @@ -840,7 +820,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba @Override public void handleCancel(boolean cancelAll) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Preflight cancelled; failing"); } mResult.set(BackupTransport.AGENT_ERROR); @@ -997,9 +977,7 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba @Override public void handleCancel(boolean cancelAll) { - if (DEBUG) { - Slog.w(TAG, "Full backup cancel of " + mTarget.packageName); - } + Slog.w(TAG, "Full backup cancel of " + mTarget.packageName); mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL, diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java index 38c7dd1e230b..87cf8a313651 100644 --- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java +++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java @@ -17,7 +17,6 @@ package com.android.server.backup.internal; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import android.app.backup.BackupAnnotations.BackupDestination; @@ -136,8 +135,8 @@ public class BackupHandler extends Handler { public void handleMessage(Message msg) { if (msg.what == MSG_STOP) { - Slog.v(TAG, "Stopping backup handler"); - backupManagerService.getWakelock().quit(); + Slog.d(TAG, "Stopping backup handler"); + backupManagerService.getWakeLock().quit(); mBackupThread.quitSafely(); } @@ -163,7 +162,7 @@ public class BackupHandler extends Handler { transportManager .disposeOfTransportClient(transportConnection, callerLogString); } - Slog.v(TAG, "Backup requested but no transport available"); + Slog.d(TAG, "Backup requested but no transport available"); break; } @@ -177,14 +176,12 @@ public class BackupHandler extends Handler { return; } - if (DEBUG) { - Slog.v(TAG, "Running a backup pass"); - } + Slog.d(TAG, "Running a backup pass"); // Acquire the wakelock and pass it to the backup thread. It will be released // once backup concludes. backupManagerService.setBackupRunning(true); - backupManagerService.getWakelock().acquire(); + backupManagerService.getWakeLock().acquire(); // Do we have any work to do? Construct the work queue // then release the synchronization lock to actually run @@ -193,9 +190,7 @@ public class BackupHandler extends Handler { for (BackupRequest b : backupManagerService.getPendingBackups().values()) { queue.add(b.packageName); } - if (DEBUG) { - Slog.v(TAG, "clearing pending backups"); - } + Slog.d(TAG, "clearing pending backups"); backupManagerService.getPendingBackups().clear(); // Start a new backup-queue journal file too @@ -249,7 +244,7 @@ public class BackupHandler extends Handler { staged = false; } } else { - Slog.v(TAG, "Backup requested but nothing pending"); + Slog.d(TAG, "Backup requested but nothing pending"); staged = false; } @@ -259,7 +254,7 @@ public class BackupHandler extends Handler { synchronized (backupManagerService.getQueueLock()) { backupManagerService.setBackupRunning(false); } - backupManagerService.getWakelock().release(); + backupManagerService.getWakeLock().release(); } break; } @@ -267,7 +262,7 @@ public class BackupHandler extends Handler { case MSG_BACKUP_RESTORE_STEP: { try { BackupRestoreTask task = (BackupRestoreTask) msg.obj; - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Got next step for " + task + ", executing"); } task.execute(); @@ -324,16 +319,12 @@ public class BackupHandler extends Handler { synchronized (backupManagerService.getPendingRestores()) { if (backupManagerService.isRestoreInProgress()) { - if (DEBUG) { - Slog.d(TAG, "Restore in progress, queueing."); - } + Slog.d(TAG, "Restore in progress, queueing."); backupManagerService.getPendingRestores().add(task); // This task will be picked up and executed when the the currently running // restore task finishes. } else { - if (DEBUG) { - Slog.d(TAG, "Starting restore."); - } + Slog.d(TAG, "Starting restore."); backupManagerService.setRestoreInProgress(true); Message restoreMsg = obtainMessage(MSG_BACKUP_RESTORE_STEP, task); sendMessage(restoreMsg); @@ -471,11 +462,11 @@ public class BackupHandler extends Handler { case MSG_REQUEST_BACKUP: { BackupParams params = (BackupParams) msg.obj; - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "MSG_REQUEST_BACKUP observer=" + params.observer); } backupManagerService.setBackupRunning(true); - backupManagerService.getWakelock().acquire(); + backupManagerService.getWakeLock().acquire(); KeyValueBackupTask.start( backupManagerService, @@ -496,7 +487,7 @@ public class BackupHandler extends Handler { case MSG_SCHEDULE_BACKUP_PACKAGE: { String pkgName = (String) msg.obj; - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "MSG_SCHEDULE_BACKUP_PACKAGE " + pkgName); } backupManagerService.dataChangedImpl(pkgName); diff --git a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java index a94167eb9fa7..0b974e2d0a8a 100644 --- a/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java +++ b/services/backup/java/com/android/server/backup/internal/LifecycleOperationStorage.java @@ -17,7 +17,6 @@ package com.android.server.backup.internal; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import android.annotation.UserIdInt; import android.util.Slog; @@ -206,7 +205,7 @@ public class LifecycleOperationStorage implements OperationStorage { * @return true if the operation was ACKed prior to or during this call. */ public boolean waitUntilOperationComplete(int token, IntConsumer callback) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "[UserID:" + mUserId + "] Blocking until operation complete for " + Integer.toHexString(token)); } @@ -227,7 +226,7 @@ public class LifecycleOperationStorage implements OperationStorage { } // When the wait is notified we loop around and recheck the current state } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "[UserID:" + mUserId + "] Unblocked waiting for operation token=" + Integer.toHexString(token)); @@ -244,7 +243,7 @@ public class LifecycleOperationStorage implements OperationStorage { if (op != null) { callback.accept(op.type); } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "[UserID:" + mUserId + "] operation " + Integer.toHexString(token) + " complete: finalState=" + finalState); } @@ -263,7 +262,7 @@ public class LifecycleOperationStorage implements OperationStorage { * operation from PENDING to ACKNOWLEDGED state. */ public void onOperationComplete(int token, long result, Consumer<BackupRestoreTask> callback) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "[UserID:" + mUserId + "] onOperationComplete: " + Integer.toHexString(token) + " result=" + result); } @@ -277,10 +276,8 @@ public class LifecycleOperationStorage implements OperationStorage { op = null; mOperations.remove(token); } else if (op.state == OpState.ACKNOWLEDGED) { - if (DEBUG) { - Slog.w(TAG, "[UserID:" + mUserId + "] Received duplicate ack for token=" + Slog.w(TAG, "[UserID:" + mUserId + "] Received duplicate ack for token=" + Integer.toHexString(token)); - } op = null; mOperations.remove(token); } else if (op.state == OpState.PENDING) { @@ -317,7 +314,7 @@ public class LifecycleOperationStorage implements OperationStorage { Operation op = null; synchronized (mOperationsLock) { op = mOperations.get(token); - if (MORE_DEBUG) { + if (DEBUG) { if (op == null) { Slog.w(TAG, "[UserID:" + mUserId + "] Cancel of token " + Integer.toHexString(token) + " but no op found"); @@ -326,17 +323,13 @@ public class LifecycleOperationStorage implements OperationStorage { int state = (op != null) ? op.state : OpState.TIMEOUT; if (state == OpState.ACKNOWLEDGED) { // The operation finished cleanly, so we have nothing more to do. - if (DEBUG) { - Slog.w(TAG, "[UserID:" + mUserId + "] Operation already got an ack." + Slog.w(TAG, "[UserID:" + mUserId + "] Operation already got an ack." + "Should have been removed from mCurrentOperations."); - } op = null; mOperations.delete(token); } else if (state == OpState.PENDING) { - if (DEBUG) { - Slog.v(TAG, "[UserID:" + mUserId + "] Cancel: token=" + Slog.d(TAG, "[UserID:" + mUserId + "] Cancel: token=" + Integer.toHexString(token)); - } op.state = OpState.TIMEOUT; // Can't delete op from mOperations here. waitUntilOperationComplete may be // called after we receive cancel here. We need this op's state there. @@ -347,7 +340,7 @@ public class LifecycleOperationStorage implements OperationStorage { // If there's a TimeoutHandler for this event, call it if (op != null && op.callback != null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "[UserID:" + mUserId + " Invoking cancel on " + op.callback); } op.callback.handleCancel(cancelAll); diff --git a/services/backup/java/com/android/server/backup/internal/PerformClearTask.java b/services/backup/java/com/android/server/backup/internal/PerformClearTask.java index de0177c1b62c..b9ac5648a5f4 100644 --- a/services/backup/java/com/android/server/backup/internal/PerformClearTask.java +++ b/services/backup/java/com/android/server/backup/internal/PerformClearTask.java @@ -75,7 +75,7 @@ public class PerformClearTask implements Runnable { } mListener.onFinished(callerLogString); // Last but not least, release the cpu - mBackupManagerService.getWakelock().release(); + mBackupManagerService.getWakeLock().release(); } } } diff --git a/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java b/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java index 160124b6f1d3..98563a7d7d8e 100644 --- a/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java +++ b/services/backup/java/com/android/server/backup/internal/RunInitializeReceiver.java @@ -16,7 +16,6 @@ package com.android.server.backup.internal; -import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.RUN_INITIALIZE_ACTION; @@ -47,9 +46,7 @@ public class RunInitializeReceiver extends BroadcastReceiver { synchronized (mUserBackupManagerService.getQueueLock()) { Set<String> pendingInits = mUserBackupManagerService.getPendingInits(); - if (DEBUG) { - Slog.v(TAG, "Running a device init; " + pendingInits.size() + " pending"); - } + Slog.d(TAG, "Running a device init; " + pendingInits.size() + " pending"); if (pendingInits.size() > 0) { String[] transports = pendingInits.toArray(new String[pendingInits.size()]); diff --git a/services/backup/java/com/android/server/backup/internal/SetupObserver.java b/services/backup/java/com/android/server/backup/internal/SetupObserver.java index f399fe9e7ab9..309fe29b6866 100644 --- a/services/backup/java/com/android/server/backup/internal/SetupObserver.java +++ b/services/backup/java/com/android/server/backup/internal/SetupObserver.java @@ -16,7 +16,7 @@ package com.android.server.backup.internal; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; +import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.getSetupCompleteSettingForUser; @@ -57,7 +57,7 @@ public class SetupObserver extends ContentObserver { boolean resolvedSetupComplete = previousSetupComplete || newSetupComplete; mUserBackupManagerService.setSetupComplete(resolvedSetupComplete); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d( TAG, "Setup complete change: was=" @@ -73,7 +73,7 @@ public class SetupObserver extends ContentObserver { if (resolvedSetupComplete && !previousSetupComplete && mUserBackupManagerService.isEnabled()) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Setup complete so starting backups"); } KeyValueBackupJob.schedule(mUserBackupManagerService.getUserId(), mContext, diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java index 20c8cf6c6923..af0c895c1261 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupReporter.java @@ -54,13 +54,10 @@ import java.util.List; @VisibleForTesting public class KeyValueBackupReporter { @VisibleForTesting static final String TAG = "KeyValueBackupTask"; - private static final boolean DEBUG = BackupManagerService.DEBUG; - @VisibleForTesting static final boolean MORE_DEBUG = BackupManagerService.MORE_DEBUG; + @VisibleForTesting static final boolean DEBUG = BackupManagerService.DEBUG; static void onNewThread(String threadName) { - if (DEBUG) { - Slog.d(TAG, "Spinning thread " + threadName); - } + Slog.d(TAG, "Spinning thread " + threadName); } private final UserBackupManagerService mBackupManagerService; @@ -87,9 +84,7 @@ public class KeyValueBackupReporter { } void onSkipBackup() { - if (DEBUG) { - Slog.d(TAG, "Skipping backup since one is already in progress"); - } + Slog.d(TAG, "Skipping backup since one is already in progress"); } void onEmptyQueueAtStart() { @@ -97,9 +92,7 @@ public class KeyValueBackupReporter { } void onQueueReady(List<String> queue) { - if (DEBUG) { - Slog.v(TAG, "Beginning backup of " + queue.size() + " targets"); - } + Slog.d(TAG, "Beginning backup of " + queue.size() + " targets"); } void onTransportReady(String transportName) { @@ -167,7 +160,7 @@ public class KeyValueBackupReporter { } void onAgentError(String packageName) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Agent failure for " + packageName + ", re-staging"); } BackupObserverUtils.sendBackupOnPackageResult( @@ -175,13 +168,11 @@ public class KeyValueBackupReporter { } void onExtractAgentData(String packageName) { - if (DEBUG) { - Slog.d(TAG, "Invoking agent on " + packageName); - } + Slog.d(TAG, "Invoking agent on " + packageName); } void onAgentFilesReady(File backupDataFile) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Data file: " + backupDataFile); } } @@ -216,7 +207,7 @@ public class KeyValueBackupReporter { null, BackupManagerMonitor.EXTRA_LOG_ILLEGAL_KEY, key)); BackupObserverUtils.sendBackupOnPackageResult( mObserver, packageName, BackupManager.ERROR_AGENT_FAILURE); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, "Agent failure for " + packageName + " with illegal key " + key + ", dropped"); @@ -232,7 +223,7 @@ public class KeyValueBackupReporter { } void onWriteWidgetData(boolean priorStateExists, @Nullable byte[] widgetState) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, "Checking widget update: state=" @@ -243,13 +234,13 @@ public class KeyValueBackupReporter { } void onTransportPerformBackup(String packageName) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Sending non-empty data to transport for " + packageName); } } void onEmptyData(PackageInfo packageInfo) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "No backup data written, not calling transport"); } mBackupManagerMonitorEventSender.monitorEvent( @@ -273,7 +264,7 @@ public class KeyValueBackupReporter { } void onPackageBackupQuotaExceeded(String packageName) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Package " + packageName + " hit quota limit on key-value backup"); } BackupObserverUtils.sendBackupOnPackageResult( @@ -319,7 +310,7 @@ public class KeyValueBackupReporter { } void onCancel() { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Cancel received"); } } @@ -363,7 +354,7 @@ public class KeyValueBackupReporter { } void onRevertTask() { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Reverting backup queue, re-staging everything"); } } @@ -373,7 +364,7 @@ public class KeyValueBackupReporter { } void onRemoteCallReturned(RemoteResult result, String logIdentifier) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Agent call " + logIdentifier + " returned " + result); } } @@ -388,7 +379,7 @@ public class KeyValueBackupReporter { void onTransportNotInitialized(@Nullable String transportName) { EventLog.writeEvent(EventLogTags.BACKUP_RESET, transportName); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Transport requires initialization, rerunning"); } } diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index 689c43a1cc96..494b9d59a238 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -826,7 +826,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { } mTaskFinishedListener.onFinished(callerLogString); mReporter.onBackupFinished(getBackupFinishedStatus(mCancelled, status)); - mBackupManagerService.getWakelock().release(); + mBackupManagerService.getWakeLock().release(); } private int getBackupFinishedStatus(boolean cancelled, int transportStatus) { @@ -878,12 +878,13 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { * the transport or not. It's the caller responsibility to do the clean-up or delegate it. */ private void extractAgentData(PackageInfo packageInfo) throws AgentException, TaskException { - mBackupManagerService.setWorkSource(new WorkSource(packageInfo.applicationInfo.uid)); + mBackupManagerService.getWakeLock().setWorkSource( + new WorkSource(packageInfo.applicationInfo.uid)); try { mAgent = bindAgent(packageInfo); extractAgentData(packageInfo, mAgent); } finally { - mBackupManagerService.setWorkSource(null); + mBackupManagerService.getWakeLock().setWorkSource(null); } } diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java index 9f7b62763ed3..41134d68916a 100644 --- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java +++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java @@ -17,7 +17,6 @@ package com.android.server.backup.restore; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_SESSION_TIMEOUT; import static com.android.server.backup.internal.BackupHandler.MSG_RUN_GET_RESTORE_SETS; import static com.android.server.backup.internal.BackupHandler.MSG_RUN_RESTORE; @@ -41,6 +40,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; +import com.android.server.backup.BackupWakeLock; import com.android.server.backup.Flags; import com.android.server.backup.TransportManager; import com.android.server.backup.UserBackupManagerService; @@ -121,7 +121,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { // comes in. mBackupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT); - UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock(); + BackupWakeLock wakelock = mBackupManagerService.getWakeLock(); wakelock.acquire(); // Prevent lambda from leaking 'this' @@ -150,10 +150,8 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { android.Manifest.permission.BACKUP, "performRestore"); - if (DEBUG) { - Slog.d(TAG, "restoreAll token=" + Long.toHexString(token) + Slog.d(TAG, "restoreAll token=" + Long.toHexString(token) + " observer=" + observer); - } if (mEnded) { throw new IllegalStateException("Restore session already ended"); @@ -213,40 +211,38 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { android.Manifest.permission.BACKUP, "performRestore"); - if (DEBUG) { - StringBuilder b = new StringBuilder(128); - b.append("restorePackages token="); - b.append(Long.toHexString(token)); - b.append(" observer="); - if (observer == null) { - b.append("null"); - } else { - b.append(observer.toString()); - } - b.append(" monitor="); - if (monitor == null) { - b.append("null"); - } else { - b.append(monitor.toString()); - } - b.append(" packages="); - if (packages == null) { - b.append("null"); - } else { - b.append('{'); - boolean first = true; - for (String s : packages) { - if (!first) { - b.append(", "); - } else { - first = false; - } - b.append(s); + StringBuilder b = new StringBuilder(128); + b.append("restorePackages token="); + b.append(Long.toHexString(token)); + b.append(" observer="); + if (observer == null) { + b.append("null"); + } else { + b.append(observer.toString()); + } + b.append(" monitor="); + if (monitor == null) { + b.append("null"); + } else { + b.append(monitor.toString()); + } + b.append(" packages="); + if (packages == null) { + b.append("null"); + } else { + b.append('{'); + boolean first = true; + for (String s : packages) { + if (!first) { + b.append(", "); + } else { + first = false; } - b.append('}'); + b.append(s); } - Slog.d(TAG, b.toString()); + b.append('}'); } + Slog.d(TAG, b.toString()); if (mEnded) { throw new IllegalStateException("Restore session already ended"); @@ -325,10 +321,8 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { public synchronized int restorePackage(String packageName, IRestoreObserver observer, IBackupManagerMonitor monitor) { - if (DEBUG) { - Slog.v(TAG, "restorePackage pkg=" + packageName + " obs=" + observer + Slog.d(TAG, "restorePackage pkg=" + packageName + " obs=" + observer + "monitor=" + monitor); - } if (mEnded) { throw new IllegalStateException("Restore session already ended"); @@ -379,18 +373,14 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { // Check whether there is data for it in the current dataset, falling back // to the ancestral dataset if not. long token = mBackupManagerService.getAvailableRestoreToken(packageName); - if (DEBUG) { - Slog.v(TAG, "restorePackage pkg=" + packageName + Slog.d(TAG, "restorePackage pkg=" + packageName + " token=" + Long.toHexString(token)); - } // If we didn't come up with a place to look -- no ancestral dataset and // the app has never been backed up from this device -- there's nothing // to do but return failure. if (token == 0) { - if (DEBUG) { - Slog.w(TAG, "No data available for this package; not restoring"); - } + Slog.w(TAG, "No data available for this package; not restoring"); return -1; } @@ -431,9 +421,9 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { Handler backupHandler = mBackupManagerService.getBackupHandler(); backupHandler.removeMessages(MSG_RESTORE_SESSION_TIMEOUT); - UserBackupManagerService.BackupWakeLock wakelock = mBackupManagerService.getWakelock(); + BackupWakeLock wakelock = mBackupManagerService.getWakeLock(); wakelock.acquire(); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, callerLogString); } @@ -473,9 +463,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { } public synchronized void endRestoreSession() { - if (DEBUG) { - Slog.d(TAG, "endRestoreSession"); - } + Slog.d(TAG, "endRestoreSession"); if (mTimedOut) { Slog.i(TAG, "Session already timed out"); diff --git a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java index cfc0f203e6ec..cb491c6f384e 100644 --- a/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java +++ b/services/backup/java/com/android/server/backup/restore/AdbRestoreFinishedLatch.java @@ -17,7 +17,6 @@ package com.android.server.backup.restore; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import android.util.Slog; @@ -72,7 +71,7 @@ public class AdbRestoreFinishedLatch implements BackupRestoreTask { @Override public void operationComplete(long result) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.w(TAG, "adb onRestoreFinished() complete"); } mLatch.countDown(); @@ -81,9 +80,7 @@ public class AdbRestoreFinishedLatch implements BackupRestoreTask { @Override public void handleCancel(boolean cancelAll) { - if (DEBUG) { - Slog.w(TAG, "adb onRestoreFinished() timed out"); - } + Slog.w(TAG, "adb onRestoreFinished() timed out"); mLatch.countDown(); mOperationStorage.removeOperation(mCurrentOpToken); } diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java index 237ffa3bdb21..19e565dab719 100644 --- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java +++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java @@ -17,7 +17,6 @@ package com.android.server.backup.restore; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME; import static com.android.server.backup.UserBackupManagerService.BACKUP_METADATA_FILENAME; @@ -215,12 +214,12 @@ public class FullRestoreEngine extends RestoreEngine { FileMetadata info; try { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Reading tar header for restoring file"); } info = tarBackupReader.readTarHeaders(); if (info != null) { - if (MORE_DEBUG) { + if (DEBUG) { info.dump(); } @@ -249,9 +248,7 @@ public class FullRestoreEngine extends RestoreEngine { // Clean up the previous agent relationship if necessary, // and let the observer know we're considering a new app. if (mAgent != null) { - if (DEBUG) { - Slog.d(TAG, "Saw new package; finalizing old one"); - } + Slog.d(TAG, "Saw new package; finalizing old one"); // Now we're really done tearDownPipes(); tearDownAgent(mTargetApp, mIsAdbRestore); @@ -307,9 +304,7 @@ public class FullRestoreEngine extends RestoreEngine { // If we're in accept-if-apk state, then the first file we // see MUST be the apk. if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (DEBUG) { - Slog.d(TAG, "APK file; installing"); - } + Slog.d(TAG, "APK file; installing"); // Try to install the app. String installerPackageName = mPackageInstallers.get(pkg); boolean isSuccessfullyInstalled = RestoreUtils.installApk( @@ -336,9 +331,7 @@ public class FullRestoreEngine extends RestoreEngine { case ACCEPT: if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (DEBUG) { - Slog.d(TAG, "apk present but ACCEPT"); - } + Slog.d(TAG, "apk present but ACCEPT"); // we can take the data without the apk, so we // *want* to do so. skip the apk by declaring this // one file not-okay without changing the restore @@ -364,11 +357,11 @@ public class FullRestoreEngine extends RestoreEngine { // If the policy is satisfied, go ahead and set up to pipe the // data to the agent. - if (MORE_DEBUG && okay && mAgent != null) { + if (DEBUG && okay && mAgent != null) { Slog.i(TAG, "Reusing existing agent instance"); } if (okay && mAgent == null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Need to launch agent for " + pkg); } @@ -389,20 +382,18 @@ public class FullRestoreEngine extends RestoreEngine { boolean forceClear = shouldForceClearAppDataOnFullRestore( mTargetApp.packageName); if (mTargetApp.backupAgentName == null || forceClear) { - if (DEBUG) { - Slog.d(TAG, + Slog.d(TAG, "Clearing app data preparatory to full restore"); - } mBackupManagerService.clearApplicationDataBeforeRestore(pkg); } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "backup agent (" + mTargetApp.backupAgentName + ") => no clear"); } } mClearedPackages.add(pkg); } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "We've initialized this app already; no clear " + "required"); } @@ -459,10 +450,8 @@ public class FullRestoreEngine extends RestoreEngine { OpType.RESTORE_WAIT); if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) { - if (DEBUG) { - Slog.d(TAG, "Restoring OBB file for " + pkg + Slog.d(TAG, "Restoring OBB file for " + pkg + " : " + info.path); - } mObbConnection.restoreObbFile(pkg, mPipes[0], info.size, info.type, info.path, info.mode, info.mtime, token, @@ -470,10 +459,8 @@ public class FullRestoreEngine extends RestoreEngine { } else if (FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)) { // This is only possible during adb restore. // TODO: Refactor to clearly separate the flows. - if (DEBUG) { - Slog.d(TAG, "Restoring key-value file for " + pkg + Slog.d(TAG, "Restoring key-value file for " + pkg + " : " + info.path); - } // Set the version saved from manifest entry. info.version = mAppVersion; KeyValueAdbRestoreEngine restoreEngine = @@ -483,7 +470,7 @@ public class FullRestoreEngine extends RestoreEngine { mAgent, token); new Thread(restoreEngine, "restore-key-value-runner").start(); } else { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "Invoking agent to restore file " + info.path); } // fire up the app's agent listening on the socket. If @@ -519,7 +506,7 @@ public class FullRestoreEngine extends RestoreEngine { // Copy over the data if the agent is still good if (okay) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, " copying to restore agent: " + toCopy + " bytes"); } boolean pipeOkay = true; @@ -586,7 +573,7 @@ public class FullRestoreEngine extends RestoreEngine { // dropped file, or an already-ignored package: skip to the // next stream entry by reading and discarding this file. if (!okay) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "[discarding file content]"); } long bytesToConsume = (info.size + 511) & ~511; @@ -603,9 +590,7 @@ public class FullRestoreEngine extends RestoreEngine { } } } catch (IOException e) { - if (DEBUG) { - Slog.w(TAG, "io exception on restore socket read: " + e.getMessage()); - } + Slog.w(TAG, "io exception on restore socket read: " + e.getMessage()); logBMMEvent(BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT, onlyPackage); setResult(RestoreEngine.TRANSPORT_FAILURE); @@ -614,7 +599,7 @@ public class FullRestoreEngine extends RestoreEngine { // If we got here we're either running smoothly or we've finished if (info == null) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "No [more] data for this package; tearing down"); } tearDownPipes(); @@ -713,7 +698,7 @@ public class FullRestoreEngine extends RestoreEngine { mBackupManagerService.prepareOperationTimeout( token, fullBackupAgentTimeoutMillis, latch, OpType.RESTORE_WAIT); if (mTargetApp.processName.equals("system")) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "system agent - restoreFinished on thread"); } Runnable runner = new AdbRestoreFinishedRunnable(mAgent, token, @@ -749,7 +734,7 @@ public class FullRestoreEngine extends RestoreEngine { return true; } if (FullBackup.CACHE_TREE_TOKEN.equals(info.domain)) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Dropping cache file path " + info.path); } return false; @@ -761,7 +746,7 @@ public class FullRestoreEngine extends RestoreEngine { // API. Respect the no-backup intention and don't let the data get to // the app. if (info.path.startsWith("no_backup/")) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Dropping no_backup file path " + info.path); } return false; @@ -774,7 +759,7 @@ public class FullRestoreEngine extends RestoreEngine { private static boolean isCanonicalFilePath(String path) { if (path.contains("..") || path.contains("//")) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.w(TAG, "Dropping invalid path " + path); } return false; diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java index 2374dee7755e..5a3494c2bc6e 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java @@ -17,7 +17,6 @@ package com.android.server.backup.restore; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.BackupPasswordManager.PBKDF_CURRENT; import static com.android.server.backup.BackupPasswordManager.PBKDF_FALLBACK; @@ -93,9 +92,7 @@ public class PerformAdbRestoreTask implements Runnable { FileInputStream rawInStream = null; try { if (!mBackupManagerService.backupPasswordMatches(mCurrentPassword)) { - if (DEBUG) { - Slog.w(TAG, "Backup password mismatch; aborting"); - } + Slog.w(TAG, "Backup password mismatch; aborting"); return; } @@ -122,7 +119,7 @@ public class PerformAdbRestoreTask implements Runnable { tarInputStream); mEngineThread.run(); - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Done consuming input tarfile."); } } catch (Exception e) { @@ -144,7 +141,7 @@ public class PerformAdbRestoreTask implements Runnable { mObbConnection.tearDown(); mObserver = FullBackupRestoreObserverUtils.sendEndRestore(mObserver); Slog.d(TAG, "Full restore pass complete."); - mBackupManagerService.getWakelock().release(); + mBackupManagerService.getWakeLock().release(); } } diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index ec9d340abe45..707ae03b3964 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -19,7 +19,6 @@ package com.android.server.backup.restore; import static android.app.backup.BackupAnnotations.OperationType.RESTORE; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.KEY_WIDGET_STATE; import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL; @@ -253,9 +252,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mUserId, backupEligibilityRules); filterSet = packagesToNames(apps); - if (DEBUG) { - Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps"); - } + Slog.i(TAG, "Full restore; asking about " + filterSet.length + " apps"); } mAcceptSet = new ArrayList<>(filterSet.length); @@ -315,7 +312,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Restore; accept set size is " + mAcceptSet.size()); for (PackageInfo info : mAcceptSet) { Slog.v(TAG, " " + info.packageName); @@ -335,7 +332,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Execute one tick of whatever state machine the task implements @Override public void execute() { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "*** Executing restore step " + mState); } switch (mState) { @@ -517,7 +514,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { mCurrentPackage.applicationInfo.uid = Process.SYSTEM_UID; mPmAgent = backupManagerService.makeMetadataAgent(null); mAgent = IBackupAgent.Stub.asInterface(mPmAgent.onBind()); - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "initiating restore for PMBA"); } initiateOneRestore(mCurrentPackage, 0); @@ -596,9 +593,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { return; } else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) { // Yay we've reached the end cleanly - if (DEBUG) { - Slog.v(TAG, "No more packages; finishing restore"); - } + Slog.d(TAG, "No more packages; finishing restore"); int millis = (int) (SystemClock.elapsedRealtime() - mStartRealtime); EventLog.writeEvent( EventLogTags.RESTORE_SUCCESS, mRestoreAttemptedAppsCount, millis); @@ -606,9 +601,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { return; } - if (DEBUG) { - Slog.i(TAG, "Next restore package: " + mRestoreDescription); - } + Slog.i(TAG, "Next restore package: " + mRestoreDescription); mRestoreAttemptedAppsCount++; sendOnRestorePackage(mRestoreAttemptedAppsCount, pkgName); @@ -715,7 +708,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v( TAG, "Package " @@ -776,7 +769,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { /* extras= */ addRestoreOperationTypeToEvent(/* extras= */ null)); if (mCurrentPackage.applicationInfo.backupAgentName == null || "".equals(mCurrentPackage.applicationInfo.backupAgentName)) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, "Data exists for package " @@ -852,9 +845,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { private void initiateOneRestore(PackageInfo app, long appVersionCode) { final String packageName = app.packageName; - if (DEBUG) { - Slog.d(TAG, "initiateOneRestore packageName=" + packageName); - } + Slog.d(TAG, "initiateOneRestore packageName=" + packageName); // !!! TODO: get the dirs from the transport mBackupDataName = new File(backupManagerService.getDataDir(), packageName + ".restore"); @@ -1010,9 +1001,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // is this a special key? if (key.equals(KEY_WIDGET_STATE)) { - if (DEBUG) { - Slog.i(TAG, "Restoring widget state for " + packageName); - } + Slog.i(TAG, "Restoring widget state for " + packageName); mWidgetData = new byte[size]; in.readEntityData(mWidgetData, 0, size); } else { @@ -1044,7 +1033,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { /* extras= */ addRestoreOperationTypeToEvent(/* extras= */ null)); try { StreamFeederThread feeder = new StreamFeederThread(); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, "Spinning threads for stream restore of " + mCurrentPackage.packageName); @@ -1070,9 +1059,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // state RESTORE_FINISHED : provide the "no more data" signpost callback at the end private void restoreFinished() { - if (DEBUG) { - Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName); - } + Slog.d(TAG, "restoreFinished packageName=" + mCurrentPackage.packageName); try { long restoreAgentFinishedTimeoutMillis = mAgentTimeoutParameters.getRestoreAgentFinishedTimeoutMillis(); @@ -1167,7 +1154,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { if (result > 0) { // The transport wrote this many bytes of restore data to the // pipe, so pass it along to the engine. - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, " <- transport provided chunk size " + result); } if (result > bufferSize) { @@ -1179,13 +1166,13 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { int n = transportIn.read(buffer, 0, toCopy); engineOut.write(buffer, 0, n); toCopy -= n; - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, " -> wrote " + n + " to engine, left=" + toCopy); } } } else if (result == BackupTransport.NO_MORE_DATA) { // Clean finish. Wind up and we're done! - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, "Got clean full-restore EOF for " @@ -1213,7 +1200,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { status = result; } } - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "Done copying to engine, falling through"); } } catch (IOException e) { @@ -1322,9 +1309,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { @Override public void handleCancel(boolean cancelAll) { mOperationStorage.removeOperation(mEphemeralOpToken); - if (DEBUG) { - Slog.w(TAG, "Full-data restore target timed out; shutting down"); - } + Slog.w(TAG, "Full-data restore target timed out; shutting down"); Bundle monitoringExtras = addRestoreOperationTypeToEvent(/* extras= */ null); mBackupManagerMonitorEventSender.monitorEvent( BackupManagerMonitor.LOG_EVENT_ID_FULL_RESTORE_TIMEOUT, @@ -1342,7 +1327,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // state FINAL : tear everything down and we're done. private void finalizeRestore() { - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "finishing restore mObserver=" + mObserver); } @@ -1366,7 +1351,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // If we have a PM token, we must under all circumstances be sure to // handshake when we've finished. if (mPmToken > 0) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, "finishing PM token " + mPmToken); } try { @@ -1404,9 +1389,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { synchronized (backupManagerService.getPendingRestores()) { if (backupManagerService.getPendingRestores().size() > 0) { - if (DEBUG) { - Slog.d(TAG, "Starting next pending restore."); - } + Slog.d(TAG, "Starting next pending restore."); PerformUnifiedRestoreTask task = backupManagerService.getPendingRestores().remove(); backupManagerService .getBackupHandler() @@ -1417,7 +1400,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } else { backupManagerService.setRestoreInProgress(false); - if (MORE_DEBUG) { + if (DEBUG) { Slog.d(TAG, "No pending restores."); } } @@ -1499,7 +1482,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { public void operationComplete(long unusedResult) { mOperationStorage.removeOperation(mEphemeralOpToken); - if (MORE_DEBUG) { + if (DEBUG) { Slog.i( TAG, "operationComplete() during restore: target=" @@ -1590,7 +1573,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { @VisibleForTesting void executeNextState(UnifiedRestoreState nextState) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, " => executing next step on " + this + " nextState=" + nextState); } mState = nextState; @@ -1689,25 +1672,19 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // (and not in the denylist) // - The package has restoreAnyVersion set to true and is not part of the denylist if (mVToUDenylist.contains(mCurrentPackage.packageName)){ - if (DEBUG) { - Slog.i(TAG, mCurrentPackage.packageName + " : Package is in V to U denylist"); - } + Slog.i(TAG, mCurrentPackage.packageName + " : Package is in V to U denylist"); return false; } else if ((mCurrentPackage.applicationInfo.flags & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) == 0) { // package has restoreAnyVersion set to false - if (DEBUG) { - Slog.i(TAG, mCurrentPackage.packageName + Slog.i(TAG, mCurrentPackage.packageName + " : Package has restoreAnyVersion=false and is in V to U allowlist"); - } return mVToUAllowlist.contains(mCurrentPackage.packageName); } else { // package has restoreAnyVersion set to true and is nor in denylist - if (DEBUG) { - Slog.i(TAG, mCurrentPackage.packageName + Slog.i(TAG, mCurrentPackage.packageName + " : Package has restoreAnyVersion=true and is not in V to U denylist"); - } return true; } } diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java index 508b62cd83f0..30243013a79b 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java +++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java @@ -16,7 +16,6 @@ package com.android.server.backup.utils; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.PACKAGE_MANAGER_SENTINEL; import static com.android.server.backup.UserBackupManagerService.SETTINGS_PACKAGE; @@ -46,6 +45,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.server.backup.BackupManagerService; import com.android.server.backup.SetUtils; import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.TransportConnection; @@ -413,8 +413,8 @@ public class BackupEligibilityRules { // partition will be signed with the device's platform certificate, so on // different phones the same system app will have different signatures.) if ((target.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - if (MORE_DEBUG) { - Slog.v(TAG, "System app " + target.packageName + " - skipping sig check"); + if (BackupManagerService.DEBUG) { + Slog.d(TAG, "System app " + target.packageName + " - skipping sig check"); } return true; } @@ -431,10 +431,8 @@ public class BackupEligibilityRules { return false; } - if (DEBUG) { - Slog.v(TAG, "signaturesMatch(): stored=" + Arrays.toString(storedSigs) + Slog.d(TAG, "signaturesMatch(): stored=" + Arrays.toString(storedSigs) + " device=" + Arrays.toString(signingInfo.getApkContentsSigners())); - } final int nStored = storedSigs.length; if (nStored == 1) { diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java index 549d08c03933..c4519b1173eb 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java @@ -22,7 +22,6 @@ import static android.app.backup.BackupManagerMonitor.EXTRA_LOG_OPERATION_TYPE; import static android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT; import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING_RESULTS; -import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import android.annotation.Nullable; @@ -115,15 +114,11 @@ public class BackupManagerMonitorEventSender { if (mMonitor != null) { mMonitor.onEvent(bundle); } else { - if (DEBUG) { - Slog.w(TAG, "backup manager monitor is null unable to send event"); - } + Slog.w(TAG, "backup manager monitor is null unable to send event"); } } catch (RemoteException e) { mMonitor = null; - if (DEBUG) { - Slog.w(TAG, "backup manager monitor went away"); - } + Slog.w(TAG, "backup manager monitor went away"); } } diff --git a/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java b/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java index c0cf2ef86920..95e6cfc2f252 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java +++ b/services/backup/java/com/android/server/backup/utils/BackupObserverUtils.java @@ -16,7 +16,6 @@ package com.android.server.backup.utils; -import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import android.app.backup.BackupProgress; @@ -38,9 +37,7 @@ public class BackupObserverUtils { try { observer.onUpdate(packageName, progress); } catch (RemoteException e) { - if (DEBUG) { - Slog.w(TAG, "Backup observer went away: onUpdate"); - } + Slog.w(TAG, "Backup observer went away: onUpdate"); } } } @@ -55,9 +52,7 @@ public class BackupObserverUtils { try { observer.onResult(packageName, status); } catch (RemoteException e) { - if (DEBUG) { - Slog.w(TAG, "Backup observer went away: onResult"); - } + Slog.w(TAG, "Backup observer went away: onResult"); } } } @@ -71,9 +66,7 @@ public class BackupObserverUtils { try { observer.backupFinished(status); } catch (RemoteException e) { - if (DEBUG) { - Slog.w(TAG, "Backup observer went away: backupFinished"); - } + Slog.w(TAG, "Backup observer went away: backupFinished"); } } } diff --git a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java index 5a8533a2daee..23e5bb965d7a 100644 --- a/services/backup/java/com/android/server/backup/utils/RestoreUtils.java +++ b/services/backup/java/com/android/server/backup/utils/RestoreUtils.java @@ -16,7 +16,6 @@ package com.android.server.backup.utils; -import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import android.content.Context; @@ -78,9 +77,7 @@ public class RestoreUtils { int userId) { boolean okay = true; - if (DEBUG) { - Slog.d(TAG, "Installing from backup: " + info.packageName); - } + Slog.d(TAG, "Installing from backup: " + info.packageName); try { LocalIntentReceiver receiver = new LocalIntentReceiver(); diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java index 22eefb3b73c2..44b536adcf31 100644 --- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java +++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java @@ -36,7 +36,6 @@ import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATC import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER; import static com.android.server.backup.BackupManagerService.DEBUG; -import static com.android.server.backup.BackupManagerService.MORE_DEBUG; import static com.android.server.backup.BackupManagerService.TAG; import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_FILENAME; import static com.android.server.backup.UserBackupManagerService.BACKUP_MANIFEST_VERSION; @@ -175,7 +174,7 @@ public class TarBackupReader { } case 0: { // presume EOF - if (MORE_DEBUG) { + if (DEBUG) { Slog.w(TAG, "Saw type=0 in tar header block, info=" + info); } return null; @@ -195,9 +194,7 @@ public class TarBackupReader { info.path = info.path.substring(FullBackup.SHARED_PREFIX.length()); info.packageName = SHARED_BACKUP_AGENT_PACKAGE; info.domain = FullBackup.SHARED_STORAGE_TOKEN; - if (DEBUG) { - Slog.i(TAG, "File in shared storage: " + info.path); - } + Slog.i(TAG, "File in shared storage: " + info.path); } else if (FullBackup.APPS_PREFIX.regionMatches(0, info.path, 0, FullBackup.APPS_PREFIX.length())) { // App content! Parse out the package name and domain @@ -227,11 +224,9 @@ public class TarBackupReader { } } } catch (IOException e) { + Slog.e(TAG, "Parse error in header: " + e.getMessage()); if (DEBUG) { - Slog.e(TAG, "Parse error in header: " + e.getMessage()); - if (MORE_DEBUG) { - hexLog(block); - } + hexLog(block); } throw e; } @@ -254,20 +249,20 @@ public class TarBackupReader { if (size <= 0) { throw new IllegalArgumentException("size must be > 0"); } - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, " ... readExactly(" + size + ") called"); } int soFar = 0; while (soFar < size) { int nRead = in.read(buffer, offset + soFar, size - soFar); if (nRead <= 0) { - if (MORE_DEBUG) { + if (DEBUG) { Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar); } break; } soFar += nRead; - if (MORE_DEBUG) { + if (DEBUG) { Slog.v(TAG, " + got " + nRead + "; now wanting " + (size - soFar)); } } @@ -290,7 +285,7 @@ public class TarBackupReader { } byte[] buffer = new byte[(int) info.size]; - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, " readAppManifestAndReturnSignatures() looking for " + info.size + " bytes"); } @@ -514,10 +509,7 @@ public class TarBackupReader { null); } } else { - if (DEBUG) { - Slog.i(TAG, - "Restore manifest from " + info.packageName + " but allowBackup=false"); - } + Slog.i(TAG, "Restore manifest from " + info.packageName + " but allowBackup=false"); mBackupManagerMonitorEventSender.monitorEvent( LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE, pkgInfo, @@ -529,10 +521,8 @@ public class TarBackupReader { // the restore properly only if the dataset provides the // apk file and we can successfully install it. if (allowApks) { - if (DEBUG) { - Slog.i(TAG, "Package " + info.packageName - + " not installed; requiring apk in dataset"); - } + Slog.i(TAG, + "Package " + info.packageName + " not installed; requiring apk in dataset"); policy = RestorePolicy.ACCEPT_IF_APK; } else { policy = RestorePolicy.IGNORE; @@ -570,7 +560,7 @@ public class TarBackupReader { long partial = (size + 512) % 512; if (partial > 0) { final int needed = 512 - (int) partial; - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Skipping tar padding: " + needed + " bytes"); } byte[] buffer = new byte[needed]; @@ -619,7 +609,7 @@ public class TarBackupReader { } switch (token) { case BACKUP_WIDGET_METADATA_TOKEN: { - if (MORE_DEBUG) { + if (DEBUG) { Slog.i(TAG, "Got widget metadata for " + info.packageName); } mWidgetData = new byte[size]; @@ -627,10 +617,9 @@ public class TarBackupReader { break; } default: { - if (DEBUG) { - Slog.i(TAG, "Ignoring metadata blob " + Integer.toHexString(token) - + " for " + info.packageName); - } + Slog.i(TAG, + "Ignoring metadata blob " + Integer.toHexString(token) + " for " + + info.packageName); in.skipBytes(size); break; } @@ -759,9 +748,7 @@ public class TarBackupReader { } else if ("size".equals(keyStr)) { info.size = Long.parseLong(valStr); } else { - if (DEBUG) { - Slog.i(TAG, "Unhandled pax key: " + key); - } + Slog.i(TAG, "Unhandled pax key: " + key); } offset += linelen; 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 260ea75a1f4c..5edd9d7041ba 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -21,7 +21,6 @@ import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_ENABLED; import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY; import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY; -import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED; @@ -492,17 +491,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub this, getDeviceId(), getPersistentDeviceId(), mParams.getName()); } - if (Flags.dynamicPolicy()) { - mActivityPolicyExemptions = new ArraySet<>( - mParams.getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT - ? mParams.getBlockedActivities() - : mParams.getAllowedActivities()); - } else { - mActivityPolicyExemptions = - mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED - ? mParams.getBlockedActivities() - : mParams.getAllowedActivities(); - } + mActivityPolicyExemptions = new ArraySet<>( + mParams.getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT + ? mParams.getBlockedActivities() + : mParams.getAllowedActivities()); if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) { final String imeId = mParams.getInputMethodComponent().flattenToShortString(); @@ -587,12 +579,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override // Binder call public @VirtualDeviceParams.DevicePolicy int getDevicePolicy( @VirtualDeviceParams.PolicyType int policyType) { - if (Flags.dynamicPolicy()) { - synchronized (mVirtualDeviceLock) { - return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT); - } - } else { - return mParams.getDevicePolicy(policyType); + synchronized (mVirtualDeviceLock) { + return mDevicePolicies.get(policyType, DEVICE_POLICY_DEFAULT); } } @@ -891,9 +879,6 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType, @VirtualDeviceParams.DevicePolicy int devicePolicy) { checkCallerIsDeviceOwner(); - if (!Flags.dynamicPolicy()) { - return; - } switch (policyType) { case POLICY_TYPE_RECENTS: @@ -924,23 +909,21 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } break; case POLICY_TYPE_CLIPBOARD: - if (Flags.crossDeviceClipboard()) { - if (devicePolicy == DEVICE_POLICY_CUSTOM + if (devicePolicy == DEVICE_POLICY_CUSTOM && mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to " - + "set a custom clipboard policy."); - } - synchronized (mVirtualDeviceLock) { - for (int i = 0; i < mVirtualDisplays.size(); i++) { - VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i); - if (!wrapper.isTrusted() && !wrapper.isMirror()) { - throw new SecurityException("All displays must be trusted for " - + "devices with custom clipboard policy."); - } + throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to " + + "set a custom clipboard policy."); + } + synchronized (mVirtualDeviceLock) { + for (int i = 0; i < mVirtualDisplays.size(); i++) { + VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i); + if (!wrapper.isTrusted() && !wrapper.isMirror()) { + throw new SecurityException("All displays must be trusted for " + + "devices with custom clipboard policy."); } - mDevicePolicies.put(policyType, devicePolicy); } + mDevicePolicies.put(policyType, devicePolicy); } break; case POLICY_TYPE_BLOCKED_ACTIVITY: @@ -1443,15 +1426,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private GenericWindowPolicyController createWindowPolicyControllerLocked( @NonNull Set<String> displayCategories) { final boolean activityLaunchAllowedByDefault = - Flags.dynamicPolicy() - ? getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT - : mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED; + getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT; final boolean crossTaskNavigationAllowedByDefault = mParams.getDefaultNavigationPolicy() == NAVIGATION_POLICY_DEFAULT_ALLOWED; final boolean showTasksInHostDeviceRecents = getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT; - final ComponentName homeComponent = - Flags.vdmCustomHome() ? mParams.getHomeComponent() : null; if (mActivityListenerAdapter == null) { mActivityListenerAdapter = new GwpcActivityListener(); @@ -1472,7 +1451,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mActivityListenerAdapter, displayCategories, showTasksInHostDeviceRecents, - homeComponent); + mParams.getHomeComponent()); gwpc.registerRunningAppsChangedListener(/* listener= */ this); return gwpc; } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index aeb2f5e9be84..a60fa693350c 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -216,17 +216,14 @@ public class VirtualDeviceManagerService extends SystemService { VIRTUAL_DEVICE_SERVICE_ORDERED_ID, mActivityInterceptorCallback); - if (Flags.persistentDeviceIdApi()) { - CompanionDeviceManager cdm = - getContext().getSystemService(CompanionDeviceManager.class); - if (cdm != null) { - onCdmAssociationsChanged(cdm.getAllAssociations(UserHandle.USER_ALL)); - cdm.addOnAssociationsChangedListener(getContext().getMainExecutor(), - this::onCdmAssociationsChanged, UserHandle.USER_ALL); - } else { - Slog.e(TAG, "Failed to find CompanionDeviceManager. No CDM association info " - + " will be available."); - } + CompanionDeviceManager cdm = getContext().getSystemService(CompanionDeviceManager.class); + if (cdm != null) { + onCdmAssociationsChanged(cdm.getAllAssociations(UserHandle.USER_ALL)); + cdm.addOnAssociationsChangedListener(getContext().getMainExecutor(), + this::onCdmAssociationsChanged, UserHandle.USER_ALL); + } else { + Slog.e(TAG, "Failed to find CompanionDeviceManager. No CDM association info " + + " will be available."); } if (android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower()) { mStrongAuthTracker = new StrongAuthTracker(getContext()); @@ -338,14 +335,6 @@ public class VirtualDeviceManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { getContext().sendBroadcastAsUser(i, UserHandle.ALL); - - if (!Flags.persistentDeviceIdApi()) { - synchronized (mVirtualDeviceManagerLock) { - if (mVirtualDevices.size() == 0) { - unregisterCdmAssociationListener(); - } - } - } } finally { Binder.restoreCallingIdentity(identity); } @@ -379,21 +368,6 @@ public class VirtualDeviceManagerService extends SystemService { } } - @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) - private void registerCdmAssociationListener() { - final CompanionDeviceManager cdm = getContext().getSystemService( - CompanionDeviceManager.class); - cdm.addOnAssociationsChangedListener(getContext().getMainExecutor(), - mCdmAssociationListener); - } - - @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES) - private void unregisterCdmAssociationListener() { - final CompanionDeviceManager cdm = getContext().getSystemService( - CompanionDeviceManager.class); - cdm.removeOnAssociationsChangedListener(mCdmAssociationListener); - } - void onCdmAssociationsChanged(List<AssociationInfo> associations) { ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>(); for (int i = 0; i < associations.size(); ++i) { @@ -479,9 +453,8 @@ public class VirtualDeviceManagerService extends SystemService { AssociationInfo associationInfo = getAssociationInfo(packageName, associationId); if (associationInfo == null) { throw new IllegalArgumentException("No association with ID " + associationId); - } else if (!VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES - .contains(associationInfo.getDeviceProfile()) - && Flags.persistentDeviceIdApi()) { + } else if (!VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains( + associationInfo.getDeviceProfile())) { throw new IllegalArgumentException("Unsupported CDM Association device profile " + associationInfo.getDeviceProfile() + " for virtual device creation."); } @@ -522,14 +495,6 @@ public class VirtualDeviceManagerService extends SystemService { Counter.logIncrement("virtual_devices.value_virtual_devices_created_count"); synchronized (mVirtualDeviceManagerLock) { - if (!Flags.persistentDeviceIdApi() && mVirtualDevices.size() == 0) { - final long callingId = Binder.clearCallingIdentity(); - try { - registerCdmAssociationListener(); - } finally { - Binder.restoreCallingIdentity(callingId); - } - } mVirtualDevices.put(deviceId, virtualDevice); } @@ -767,7 +732,7 @@ public class VirtualDeviceManagerService extends SystemService { params, /* activityListener= */ null, /* soundEffectListener= */ null); - return new VirtualDeviceManager.VirtualDevice(mImpl, getContext(), virtualDevice); + return new VirtualDeviceManager.VirtualDevice(getContext(), virtualDevice); } @Override diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 31f6ef9fc062..1d914c89c570 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -729,8 +729,10 @@ public class BinaryTransparencyService extends SystemService { private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) { pw.println("--- Module Details ---"); pw.println("Module name: " + moduleInfo.getName()); - pw.println("Module visibility: " - + (moduleInfo.isHidden() ? "hidden" : "visible")); + if (!android.content.pm.Flags.removeHiddenModuleUsage()) { + pw.println("Module visibility: " + + (moduleInfo.isHidden() ? "hidden" : "visible")); + } } private void printAppDetails(PackageInfo packageInfo, diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 4b8770b3cd35..50876dafa819 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -277,7 +277,9 @@ final class ActivityManagerConstants extends ContentObserver { private static final long DEFAULT_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION = 1000L; /** The default value to {@link #KEY_FREEZER_CUTOFF_ADJ} */ - private static final int DEFAULT_FREEZER_CUTOFF_ADJ = ProcessList.CACHED_APP_MIN_ADJ; + private static final int DEFAULT_FREEZER_CUTOFF_ADJ = + Flags.prototypeAggressiveFreezing() ? ProcessList.HOME_APP_ADJ + : ProcessList.CACHED_APP_MIN_ADJ; /** * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b536dc524a80..0603c4506cd1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -256,7 +256,7 @@ import android.app.ServiceStartNotAllowedException; import android.app.WaitResult; import android.app.assist.ActivityId; import android.app.backup.BackupAnnotations.BackupDestination; -import android.app.backup.IBackupManager; +import android.app.backup.BackupManagerInternal; import android.app.compat.CompatChanges; import android.app.job.JobParameters; import android.app.usage.UsageEvents; @@ -4490,11 +4490,8 @@ public class ActivityManagerService extends IActivityManager.Stub final int userId = app.userId; final String packageName = app.info.packageName; mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(userId, packageName); - } catch (RemoteException e) { - // Can't happen; the backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + packageName, userId); }); } } else { @@ -12864,6 +12861,28 @@ public class ActivityManagerService extends IActivityManager.Stub } } + final long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage >= 0) { + pw.print(" Kernel CMA: "); + pw.println(stringifyKBSize(kernelCmaUsage)); + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android kernels. + // This is accounted for by calculating the amount of memory ZRAM + // consumes and including it in the lostRAM calculuation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far. Since the + // allocations come from a kernel driver, add it to kernelUsed. + kernelUsed += kernelCmaUsage; + } + // Note: ION/DMA-BUF heap pools are reclaimable and hence, they are included as part of // memInfo.getCachedSizeKb(). final long lostRAM = memInfo.getTotalSizeKb() @@ -13381,12 +13400,32 @@ public class ActivityManagerService extends IActivityManager.Stub proto.write(MemInfoDumpProto.CACHED_KERNEL_KB, memInfo.getCachedSizeKb()); proto.write(MemInfoDumpProto.FREE_KB, memInfo.getFreeSizeKb()); } + // CMA memory can be in one of the following four states: + // + // 1. Free, in which case it is accounted for as part of MemFree, which + // is already considered in the lostRAM calculation below. + // + // 2. Allocated as part of a userspace allocated, in which case it is + // already accounted for in the total PSS value that was computed. + // + // 3. Allocated for storing compressed memory (ZRAM) on Android Kernels. + // This is accounted for by calculating hte amount of memory ZRAM + // consumes and including it in the lostRAM calculation. + // + // 4. Allocated by a kernel driver, in which case, it is currently not + // attributed to any term that has been derived thus far, so subtract + // it from lostRAM. + long kernelCmaUsage = Debug.getKernelCmaUsageKb(); + if (kernelCmaUsage < 0) { + kernelCmaUsage = 0; + } long lostRAM = memInfo.getTotalSizeKb() - (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS]) - memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb() // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb()) + memInfo.getShmemSizeKb() - - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb(); + - memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb() + - kernelCmaUsage; proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss); proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb()); proto.write(MemInfoDumpProto.LOST_RAM_KB, lostRAM); @@ -13505,11 +13544,8 @@ public class ActivityManagerService extends IActivityManager.Stub if (DEBUG_BACKUP || DEBUG_CLEANUP) Slog.d(TAG_CLEANUP, "App " + backupTarget.appInfo + " died during backup"); mHandler.post(() -> { - try { - getBackupManager().agentDisconnectedForUser(app.userId, app.info.packageName); - } catch (RemoteException e) { - // can't happen; backup manager is local - } + LocalServices.getService(BackupManagerInternal.class).agentDisconnectedForUser( + app.info.packageName, app.userId); }); } @@ -14223,9 +14259,8 @@ public class ActivityManagerService extends IActivityManager.Stub final long oldIdent = Binder.clearCallingIdentity(); try { - getBackupManager().agentConnectedForUser(userId, agentPackageName, agent); - } catch (RemoteException e) { - // can't happen; the backup manager service is local + LocalServices.getService(BackupManagerInternal.class).agentConnectedForUser( + agentPackageName, userId, agent); } catch (Exception e) { Slog.w(TAG, "Exception trying to deliver BackupAgent binding: "); e.printStackTrace(); @@ -14565,7 +14600,7 @@ public class ActivityManagerService extends IActivityManager.Stub app.mProfile.addHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); } - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14711,7 +14746,7 @@ public class ActivityManagerService extends IActivityManager.Stub abiOverride, ZYGOTE_POLICY_FLAG_EMPTY); - app.setActiveInstrumentation(activeInstr); + mProcessStateController.setActiveInstrumentation(app, activeInstr); activeInstr.mFinished = false; activeInstr.mSourceUid = callingUid; activeInstr.mRunningProcesses.add(app); @@ -14848,7 +14883,7 @@ public class ActivityManagerService extends IActivityManager.Stub } instr.removeProcess(app); - app.setActiveInstrumentation(null); + mProcessStateController.setActiveInstrumentation(app, null); } app.mProfile.clearHostingComponentType(HOSTING_COMPONENT_TYPE_INSTRUMENTATION); @@ -16617,7 +16652,7 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void onUserRemoved(@UserIdInt int userId) { + public void onUserRemoving(@UserIdInt int userId) { // Clean up any ActivityTaskManager state (by telling it the user is stopped) mAtmInternal.onUserStopped(userId); // Clean up various services by removing the user @@ -16631,6 +16666,12 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public void onUserRemoved(int userId) { + // Clean up UserController state + mUserController.onUserRemoved(userId); + } + + @Override public boolean startUserInBackground(final int userId) { return ActivityManagerService.this.startUserInBackground(userId); } @@ -19374,7 +19415,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) { // this flag will be ramped to public. - intent.collectExtraIntentKeys(); + intent.collectExtraIntentKeys(true); } } @@ -19440,8 +19481,4 @@ public class ActivityManagerService extends IActivityManager.Stub } return token; } - - private IBackupManager getBackupManager() { - return IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE)); - } } diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 6b24df4a1fa8..225c7ca2ca9e 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -2477,13 +2477,15 @@ public class AppProfiler { // This is the wildcard mode, where every process brought up for // the target instrumentation should be included. if (aInstr.mTargetInfo.packageName.equals(app.info.packageName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); } } else { for (String proc : aInstr.mTargetProcesses) { if (proc.equals(app.processName)) { - app.setActiveInstrumentation(aInstr); + mService.mProcessStateController.setActiveInstrumentation(app, + aInstr); aInstr.mRunningProcesses.add(app); break; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 9c569db99797..3abcd4e7a143 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -156,7 +156,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; -import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; @@ -3401,13 +3400,10 @@ public class OomAdjuster { } private static int getCpuCapability(ProcessRecord app, long nowUptime) { + // Note: persistent processes get all capabilities, including CPU_TIME. final UidRecord uidRec = app.getUidRecord(); if (uidRec != null && uidRec.isCurAllowListed()) { - // Process has user visible activities. - return PROCESS_CAPABILITY_CPU_TIME; - } - if (UserHandle.isCore(app.uid)) { - // Make sure all system components are not frozen. + // Process is in the power allowlist. return PROCESS_CAPABILITY_CPU_TIME; } if (app.mState.getCachedHasVisibleActivities()) { @@ -3418,6 +3414,12 @@ public class OomAdjuster { // It running a short fgs, just give it cpu time. return PROCESS_CAPABILITY_CPU_TIME; } + if (app.mReceivers.numberOfCurReceivers() > 0) { + return PROCESS_CAPABILITY_CPU_TIME; + } + if (app.hasActiveInstrumentation()) { + return PROCESS_CAPABILITY_CPU_TIME; + } // TODO(b/370817323): Populate this method with all of the reasons to keep a process // unfrozen. return 0; diff --git a/services/core/java/com/android/server/am/ProcessStateController.java b/services/core/java/com/android/server/am/ProcessStateController.java index 57899228e6ad..f44fb06727cf 100644 --- a/services/core/java/com/android/server/am/ProcessStateController.java +++ b/services/core/java/com/android/server/am/ProcessStateController.java @@ -246,12 +246,11 @@ public class ProcessStateController { } /** - * Set what sched group to grant a process due to running a broadcast. - * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + * Sets an active instrumentation running within the given process. */ - public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { - // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model - throw new UnsupportedOperationException("Not implemented yet"); + public void setActiveInstrumentation(@NonNull ProcessRecord proc, + ActiveInstrumentation activeInstrumentation) { + proc.setActiveInstrumentation(activeInstrumentation); } /********************* Process Visibility State Events *********************/ @@ -587,6 +586,34 @@ public class ProcessStateController { psr.updateHasTopStartedAlmostPerceptibleServices(); } + /************************ Broadcast Receiver State Events **************************/ + /** + * Set what sched group to grant a process due to running a broadcast. + * {@link ProcessList.SCHED_GROUP_UNDEFINED} means the process is not running a broadcast. + */ + public void setBroadcastSchedGroup(@NonNull ProcessRecord proc, int schedGroup) { + // TODO(b/302575389): Migrate state pulled from BroadcastQueue to a pushed model + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has started processing a broadcast receiver. + */ + public boolean incrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** + * Note that the process has finished processing a broadcast receiver. + */ + public boolean decrementCurReceivers(@NonNull ProcessRecord app) { + // TODO(b/302575389): Migrate state pulled from ATMS to a pushed model + // maybe used ActivityStateFlags instead. + throw new UnsupportedOperationException("Not implemented yet"); + } + /** * Builder for ProcessStateController. */ diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index ec74f60539a2..d76c04ac7f31 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -125,7 +125,6 @@ import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.ObjectUtils; @@ -161,7 +160,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; -import java.util.function.Consumer; /** * Helper class for {@link ActivityManagerService} responsible for multi-user functionality. @@ -227,14 +225,6 @@ class UserController implements Handler.Callback { private static final int USER_SWITCH_CALLBACKS_TIMEOUT_MS = 5 * 1000; /** - * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be - * called after dismissing the keyguard. - * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()} - * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}. - */ - private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000; - - /** * Time after last scheduleOnUserCompletedEvent() call at which USER_COMPLETED_EVENT_MSG will be * scheduled (although it may fire sooner instead). * When it fires, {@link #reportOnUserCompletedEvent} will be processed. @@ -455,11 +445,6 @@ class UserController implements Handler.Callback { public void onUserCreated(UserInfo user, Object token) { onUserAdded(user); } - - @Override - public void onUserRemoved(UserInfo user) { - UserController.this.onUserRemoved(user.id); - } }; UserController(ActivityManagerService service) { @@ -2010,7 +1995,7 @@ class UserController implements Handler.Callback { mInjector.getWindowManager().setSwitchingUser(true); // Only lock if the user has a secure keyguard PIN/Pattern/Pwd if (mInjector.getKeyguardManager().isDeviceSecure(userId)) { - // Make sure the device is locked before moving on with the user switch + Slogf.d(TAG, "Locking the device before moving on with the user switch"); mInjector.lockDeviceNowAndWaitForKeyguardShown(); } } @@ -2640,7 +2625,7 @@ class UserController implements Handler.Callback { EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId); - // Do the keyguard dismiss and dismiss the user switching dialog later + // Dismiss the user switching dialog and complete the user switch mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG); mHandler.sendMessage(mHandler.obtainMessage( COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId)); @@ -2655,31 +2640,17 @@ class UserController implements Handler.Callback { @VisibleForTesting void completeUserSwitch(int oldUserId, int newUserId) { - final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled(); - // serialize each conditional step - await( - // STEP 1 - If there is no challenge set, dismiss the keyguard right away - isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId), - mInjector::dismissKeyguard, - () -> await( - // STEP 2 - If user switch ui was enabled, dismiss user switch dialog - isUserSwitchUiEnabled, - this::dismissUserSwitchDialog, - () -> { - // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast - // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete - mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); - mHandler.sendMessage(mHandler.obtainMessage( - REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); - } - )); - } - - private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) { - if (condition) { - conditionalStep.accept(nextStep); + final Runnable runnable = () -> { + // Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast ACTION_USER_SWITCHED and call + // onUserSwitchComplete on UserSwitchObservers. + mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG); + mHandler.sendMessage(mHandler.obtainMessage( + REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId)); + }; + if (isUserSwitchUiEnabled()) { + dismissUserSwitchDialog(runnable); } else { - nextStep.run(); + runnable.run(); } } @@ -3381,10 +3352,12 @@ class UserController implements Handler.Callback { if (mUserProfileGroupIds.keyAt(i) == userId || mUserProfileGroupIds.valueAt(i) == userId) { mUserProfileGroupIds.removeAt(i); - } } mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId); + mUserLru.remove((Integer) userId); + mStartedUsers.remove(userId); + updateStartedUserArrayLU(); } } @@ -4127,33 +4100,6 @@ class UserController implements Handler.Callback { return IStorageManager.Stub.asInterface(ServiceManager.getService("mount")); } - protected void dismissKeyguard(Runnable runnable) { - final AtomicBoolean isFirst = new AtomicBoolean(true); - final Runnable runOnce = () -> { - if (isFirst.getAndSet(false)) { - runnable.run(); - } - }; - - mHandler.postDelayed(runOnce, DISMISS_KEYGUARD_TIMEOUT_MS); - getWindowManager().dismissKeyguard(new IKeyguardDismissCallback.Stub() { - @Override - public void onDismissError() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissSucceeded() throws RemoteException { - mHandler.post(runOnce); - } - - @Override - public void onDismissCancelled() throws RemoteException { - mHandler.post(runOnce); - } - }, /* message= */ null); - } - boolean isHeadlessSystemUserMode() { return UserManager.isHeadlessSystemUserMode(); } @@ -4178,6 +4124,7 @@ class UserController implements Handler.Callback { void lockDeviceNowAndWaitForKeyguardShown() { if (getWindowManager().isKeyguardLocked()) { + Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); return; } diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index cfcede8ee40d..d9be47193c89 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -270,6 +270,16 @@ flag { } flag { + name: "prototype_aggressive_freezing" + namespace: "backstage_power" + description: "Grant PROCESS_CAPABILITY_CPU_TIME to as many valid states and aggressively freeze other states" + bug: "370798593" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "add_modify_raw_oom_adj_service_level" namespace: "backstage_power" description: "Add a SERVICE_ADJ level to the modifyRawOomAdj method" diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java index e4c36cc214e8..695032e6476c 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java @@ -110,7 +110,12 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper { } db.setTransactionSuccessful(); } finally { - db.endTransaction(); + try { + db.endTransaction(); + } catch (SQLiteException exception) { + Slog.e(LOG_TAG, "Couldn't commit transaction when inserting discrete ops, database" + + " file size (bytes) : " + getDatabaseFile().length(), exception); + } } } diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java index 4b3981cd4bc0..30c2a82296ca 100644 --- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java +++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java @@ -43,6 +43,8 @@ import java.io.File; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -176,6 +178,8 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry { writeAndClearOldAccessHistory(); boolean assembleChains = attributionExemptPkgs != null; IntArray opCodes = getAppOpCodes(filter, opNamesFilter); + beginTimeMillis = Math.max(beginTimeMillis, Instant.now().minus(sDiscreteHistoryCutoff, + ChronoUnit.MILLIS).toEpochMilli()); List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter, packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis, endTimeMillis, -1, null); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index bd2714211796..b9b06701a11b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -47,6 +47,7 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.IAudioManagerNative.HardeningType; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; import static android.media.audio.Flags.concurrentAudioRecordBypassPermission; @@ -151,6 +152,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; import android.media.IAudioFocusDispatcher; +import android.media.IAudioManagerNative; import android.media.IAudioModeDispatcher; import android.media.IAudioRoutesObserver; import android.media.IAudioServerStateDispatcher; @@ -835,6 +837,18 @@ public class AudioService extends IAudioService.Stub private final UserRestrictionsListener mUserRestrictionsListener = new AudioServiceUserRestrictionsListener(); + private final IAudioManagerNative mNativeShim = new IAudioManagerNative.Stub() { + // oneway + @Override + public void playbackHardeningEvent(int uid, byte type, boolean bypassed) { + } + + @Override + public void permissionUpdateBarrier() { + AudioService.this.permissionUpdateBarrier(); + } + }; + // List of binder death handlers for setMode() client processes. // The last process to have called setMode() is at the top of the list. // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers @@ -2811,6 +2825,11 @@ public class AudioService extends IAudioService.Stub args, callback, resultReceiver); } + @Override + public IAudioManagerNative getNativeInterface() { + return mNativeShim; + } + /** @see AudioManager#getSurroundFormats() */ @Override public Map<Integer, Boolean> getSurroundFormats() { @@ -7214,7 +7233,7 @@ public class AudioService extends IAudioService.Stub final int pid = Binder.getCallingPid(); final String eventSource = new StringBuilder("setBluetoothA2dpOn(").append(on) .append(") from u/pid:").append(uid).append("/") - .append(pid).toString(); + .append(pid).append(" src:AudioService.setBtA2dpOn").toString(); new MediaMetrics.Item(MediaMetrics.Name.AUDIO_DEVICE + MediaMetrics.SEPARATOR + "setBluetoothA2dpOn") @@ -8571,6 +8590,12 @@ public class AudioService extends IAudioService.Stub return true; } + private boolean shouldPreserveVolume(boolean userSwitch, VolumeGroupState vgs) { + // as for STREAM_MUSIC, preserve volume from one user to the next except + // Android Automotive platform + return (userSwitch && vgs.isMusic()) && !isPlatformAutomotive(); + } + private void readVolumeGroupsSettings(boolean userSwitch) { synchronized (mSettingsLock) { synchronized (VolumeStreamState.class) { @@ -8579,8 +8604,7 @@ public class AudioService extends IAudioService.Stub } for (int i = 0; i < sVolumeGroupStates.size(); i++) { VolumeGroupState vgs = sVolumeGroupStates.valueAt(i); - // as for STREAM_MUSIC, preserve volume from one user to the next. - if (!(userSwitch && vgs.isMusic())) { + if (!shouldPreserveVolume(userSwitch, vgs)) { vgs.clearIndexCache(); vgs.readSettings(); } @@ -9019,6 +9043,11 @@ public class AudioService extends IAudioService.Stub mIndexMap.clear(); } + private @UserIdInt int getVolumePersistenceUserId() { + return isMusic() && !isPlatformAutomotive() + ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT; + } + private void persistVolumeGroup(int device) { // No need to persist the index if the volume group is backed up // by a public stream type as this is redundant @@ -9036,7 +9065,7 @@ public class AudioService extends IAudioService.Stub boolean success = mSettings.putSystemIntForUser(mContentResolver, getSettingNameForDevice(device), getIndex(device), - isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); + getVolumePersistenceUserId()); if (!success) { Log.e(TAG, "persistVolumeGroup failed for group " + mAudioVolumeGroup.name()); } @@ -9059,7 +9088,7 @@ public class AudioService extends IAudioService.Stub String name = getSettingNameForDevice(device); index = mSettings.getSystemIntForUser( mContentResolver, name, defaultIndex, - isMusic() ? UserHandle.USER_SYSTEM : UserHandle.USER_CURRENT); + getVolumePersistenceUserId()); if (index == -1) { continue; } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index c8192e534f5c..b530da2a5f5e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2246,10 +2246,6 @@ public final class DisplayManagerService extends SystemService { @GuardedBy("mSyncRoot") private void handleLogicalDisplayDisconnectedLocked(LogicalDisplay display) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "DisplayDisconnected shouldn't be received when the flag is off"); - return; - } releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED); mExternalDisplayPolicy.handleLogicalDisplayDisconnectedLocked(display); } @@ -2315,11 +2311,6 @@ public final class DisplayManagerService extends SystemService { @SuppressLint("AndroidFrameworkRequiresPermission") private void handleLogicalDisplayConnectedLocked(LogicalDisplay display) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "DisplayConnected shouldn't be received when the flag is off"); - return; - } - setupLogicalDisplay(display); if (ExternalDisplayPolicy.isExternalDisplayLocked(display)) { @@ -2346,9 +2337,6 @@ public final class DisplayManagerService extends SystemService { private void handleLogicalDisplayAddedLocked(LogicalDisplay display) { final int displayId = display.getDisplayIdLocked(); final boolean isDefault = displayId == Display.DEFAULT_DISPLAY; - if (!mFlags.isConnectedDisplayManagementEnabled()) { - setupLogicalDisplay(display); - } // Wake up waitForDefaultDisplay. if (isDefault) { @@ -2443,21 +2431,17 @@ public final class DisplayManagerService extends SystemService { } private void handleLogicalDisplayRemovedLocked(@NonNull LogicalDisplay display) { - // With display management, the display is removed when disabled, and it might still exist. + // The display is removed when disabled, and it might still exist. // Resources must only be released when the disconnected signal is received. - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (display.isValidLocked()) { - updateViewportPowerStateLocked(display); - } + if (display.isValidLocked()) { + updateViewportPowerStateLocked(display); + } - // Note: This method is only called if the display was enabled before being removed. - sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + // Note: This method is only called if the display was enabled before being removed. + sendDisplayEventLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); - if (display.isValidLocked()) { - applyDisplayChangedLocked(display); - } - } else { - releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED); + if (display.isValidLocked()) { + applyDisplayChangedLocked(display); } if (mDisplayTopologyCoordinator != null) { mDisplayTopologyCoordinator.onDisplayRemoved(display.getDisplayIdLocked()); @@ -4565,13 +4549,11 @@ public final class DisplayManagerService extends SystemService { final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); - if (mFlags.isConnectedDisplayManagementEnabled()) { - if ((internalEventFlagsMask - & DisplayManagerGlobal - .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { - mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, - "Permission required to get signals about connection events."); - } + if ((internalEventFlagsMask + & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { + mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, + "Permission required to get signals about connection events."); } final long token = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java index e46397bc8ab7..f6b2591ea440 100644 --- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java +++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java @@ -179,12 +179,10 @@ class DisplayManagerShellCommand extends ShellCommand { pw.println(" Sets brightness to docked + idle screen brightness mode"); pw.println(" undock"); pw.println(" Sets brightness to active (normal) screen brightness mode"); - if (mFlags.isConnectedDisplayManagementEnabled()) { - pw.println(" enable-display DISPLAY_ID"); - pw.println(" Enable the DISPLAY_ID. Only possible if this is a connected display."); - pw.println(" disable-display DISPLAY_ID"); - pw.println(" Disable the DISPLAY_ID. Only possible if this is a connected display."); - } + pw.println(" enable-display DISPLAY_ID"); + pw.println(" Enable the DISPLAY_ID. Only possible if this is a connected display."); + pw.println(" disable-display DISPLAY_ID"); + pw.println(" Disable the DISPLAY_ID. Only possible if this is a connected display."); pw.println(" power-reset DISPLAY_ID"); pw.println(" Turn the DISPLAY_ID power to a state the display supposed to have."); pw.println(" power-off DISPLAY_ID"); @@ -601,11 +599,6 @@ class DisplayManagerShellCommand extends ShellCommand { } private int setDisplayEnabled(boolean enable) { - if (!mFlags.isConnectedDisplayManagementEnabled()) { - getErrPrintWriter() - .println("Error: external display management is not available on this device."); - return 1; - } final String displayIdText = getNextArg(); if (displayIdText == null) { getErrPrintWriter().println("Error: no displayId specified"); diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index 519763a1c3db..a47853c8e555 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -142,14 +142,6 @@ class ExternalDisplayPolicy { mDisplayIdsWaitingForBootCompletion.clear(); } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "External display management is not enabled on your device:" - + " cannot register thermal listener."); - } - return; - } - if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) { if (DEBUG) { Slog.d(TAG, "ConnectedDisplayErrorHandlingEnabled is not enabled on your device:" @@ -173,14 +165,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "setExternalDisplayEnabledLocked: External display management is not" - + " enabled on your device, cannot enable/disable display."); - } - return; - } - if (enabled && !isExternalDisplayAllowed()) { Slog.w(TAG, "setExternalDisplayEnabledLocked: External display can not be enabled" + " because it is currently not allowed."); @@ -202,14 +186,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - if (DEBUG) { - Slog.d(TAG, "handleExternalDisplayConnectedLocked connected display management" - + " flag is off"); - } - return; - } - if (!mIsBootCompleted) { mDisplayIdsWaitingForBootCompletion.add(logicalDisplay.getDisplayIdLocked()); return; @@ -251,10 +227,6 @@ class ExternalDisplayPolicy { void handleLogicalDisplayDisconnectedLocked(@NonNull final LogicalDisplay logicalDisplay) { // Type of the display here is always UNKNOWN, so we can't verify it is an external display - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - var displayId = logicalDisplay.getDisplayIdLocked(); if (mDisplayIdsWaitingForBootCompletion.remove(displayId)) { return; @@ -271,10 +243,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - mExternalDisplayStatsService.onDisplayAdded(logicalDisplay.getDisplayIdLocked()); } @@ -289,10 +257,6 @@ class ExternalDisplayPolicy { } } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - return; - } - if (isShown) { mExternalDisplayStatsService.onPresentationWindowAdded(displayId); } else { @@ -306,12 +270,6 @@ class ExternalDisplayPolicy { return; } - if (!mFlags.isConnectedDisplayManagementEnabled()) { - Slog.e(TAG, "disableExternalDisplayLocked shouldn't be called when the" - + " connected display management flag is off"); - return; - } - if (!mFlags.isConnectedDisplayErrorHandlingEnabled()) { if (DEBUG) { Slog.d(TAG, "disableExternalDisplayLocked shouldn't be called when the" diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 006921572977..ecc8896b69c6 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -823,18 +823,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (wasPreviouslyUpdated) { // The display isn't actually removed from our internal data structures until // after the notification is sent; see {@link #sendUpdatesForDisplaysLocked}. - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (mDisplaysEnabledCache.get(displayId)) { - // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED - reloop = true; - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; - } else { - mUpdatedLogicalDisplays.delete(displayId); - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; - } + if (mDisplaysEnabledCache.get(displayId)) { + // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED + reloop = true; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; } else { mUpdatedLogicalDisplays.delete(displayId); - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; } } else { // This display never left this class, safe to remove without notification @@ -845,20 +840,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // The display is new. } else if (!wasPreviouslyUpdated) { - if (mFlags.isConnectedDisplayManagementEnabled()) { - // We still need to send LOGICAL_DISPLAY_EVENT_ADDED - reloop = true; - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; - } else { - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_ADDED; - } + // We still need to send LOGICAL_DISPLAY_EVENT_ADDED + reloop = true; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; // Underlying displays device has changed to a different one. } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) { logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_SWAPPED; // Something about the display device has changed. - } else if (mFlags.isConnectedDisplayManagementEnabled() - && wasPreviouslyEnabled != isCurrentlyEnabled) { + } else if (wasPreviouslyEnabled != isCurrentlyEnabled) { int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED : LOGICAL_DISPLAY_EVENT_REMOVED; logicalDisplayEventMask |= event; @@ -936,17 +926,13 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION); sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_ADDED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REMOVED); - if (mFlags.isConnectedDisplayManagementEnabled()) { - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); - } + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_SWAPPED); - if (mFlags.isConnectedDisplayManagementEnabled()) { - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); - } + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CONNECTED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_ADDED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED); sendUpdatesForGroupsLocked(DISPLAY_GROUP_EVENT_CHANGED); @@ -996,23 +982,15 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { + "display=" + id + " with device=" + uniqueId); } - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { - mDisplaysEnabledCache.put(id, true); - } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { - mDisplaysEnabledCache.delete(id); - } + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { + mDisplaysEnabledCache.put(id, true); + } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { + mDisplaysEnabledCache.delete(id); } mListener.onLogicalDisplayEventLocked(display, logicalDisplayEvent); - if (mFlags.isConnectedDisplayManagementEnabled()) { - if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { - mLogicalDisplays.delete(id); - } - } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { - // We wait until we sent the EVENT_REMOVED event before actually removing the - // display. + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { mLogicalDisplays.delete(id); } } diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index addfbf1833b9..43aa6f46da78 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -42,10 +42,6 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_PORT_IN_DISPLAY_LAYOUT, Flags::enablePortInDisplayLayout); - private final FlagState mConnectedDisplayManagementFlagState = new FlagState( - Flags.FLAG_ENABLE_CONNECTED_DISPLAY_MANAGEMENT, - Flags::enableConnectedDisplayManagement); - private final FlagState mAdaptiveToneImprovements1 = new FlagState( Flags.FLAG_ENABLE_ADAPTIVE_TONE_IMPROVEMENTS_1, Flags::enableAdaptiveToneImprovements1); @@ -262,6 +258,11 @@ public class DisplayManagerFlags { Flags::subscribeGranularDisplayEvents ); + private final FlagState mBaseDensityForExternalDisplays = new FlagState( + Flags.FLAG_BASE_DENSITY_FOR_EXTERNAL_DISPLAYS, + Flags::baseDensityForExternalDisplays + ); + /** * @return {@code true} if 'port' is allowed in display layout configuration file. */ @@ -269,11 +270,6 @@ public class DisplayManagerFlags { return mPortInDisplayLayoutFlagState.isEnabled(); } - /** Returns whether connected display management is enabled or not. */ - public boolean isConnectedDisplayManagementEnabled() { - return mConnectedDisplayManagementFlagState.isEnabled(); - } - /** Returns whether power throttling clamper is enabled on not. */ public boolean isPowerThrottlingClamperEnabled() { return mPowerThrottlingClamperFlagState.isEnabled(); @@ -562,6 +558,14 @@ public class DisplayManagerFlags { } /** + * @return {@code true} if the flag for base density for external displays is enabled + */ + public boolean isBaseDensityForExternalDisplaysEnabled() { + return mBaseDensityForExternalDisplays.isEnabled(); + } + + + /** * dumps all flagstates * @param pw printWriter */ @@ -572,7 +576,6 @@ public class DisplayManagerFlags { pw.println(" " + mAdaptiveToneImprovements2); pw.println(" " + mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState); pw.println(" " + mConnectedDisplayErrorHandlingFlagState); - pw.println(" " + mConnectedDisplayManagementFlagState); pw.println(" " + mDisplayOffloadFlagState); pw.println(" " + mExternalDisplayLimitModeState); pw.println(" " + mDisplayTopology); @@ -616,6 +619,7 @@ public class DisplayManagerFlags { pw.println(" " + mDisplayListenerPerformanceImprovementsFlagState); pw.println(" " + mSubscribeGranularDisplayEvents); pw.println(" " + mEnableDisplayContentModeManagementFlagState); + pw.println(" " + mBaseDensityForExternalDisplays); } private static class FlagState { diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index eccbbb14c4ea..00a9dcb71b4b 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -29,14 +29,6 @@ flag { } flag { - name: "enable_connected_display_management" - namespace: "display_manager" - description: "Feature flag for Connected Display management" - bug: "280739508" - is_fixed_read_only: true -} - -flag { name: "enable_power_throttling_clamper" namespace: "display_manager" description: "Feature flag for Power Throttling Clamper" @@ -479,3 +471,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "base_density_for_external_displays" + namespace: "lse_desktop_experience" + description: "Feature flag for setting a base density for external displays." + bug: "382954433" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/flags/services.aconfig b/services/core/java/com/android/server/flags/services.aconfig index eea5c982c537..4505d0e2d799 100644 --- a/services/core/java/com/android/server/flags/services.aconfig +++ b/services/core/java/com/android/server/flags/services.aconfig @@ -78,3 +78,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "datetime_notifications" + # "location" is used by the Android System Time team for feature flags. + namespace: "location" + description: "Enable the time notifications feature, a toggle to enable/disable time-related notifications in Date & Time Settings" + bug: "283267917" +} diff --git a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java index d05ded5367d0..a2465d1c3a5d 100644 --- a/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java +++ b/services/core/java/com/android/server/hdmi/PowerStatusMonitorActionFromPlayback.java @@ -28,21 +28,17 @@ import com.android.internal.annotations.VisibleForTesting; */ public class PowerStatusMonitorActionFromPlayback extends HdmiCecFeatureAction { private static final String TAG = "PowerStatusMonitorActionFromPlayback"; + // State that waits for next monitoring. + private static final int STATE_WAIT_FOR_NEXT_MONITORING = 1; // State that waits for <Report Power Status> once sending <Give Device Power Status> - // to all external devices. - private static final int STATE_WAIT_FOR_REPORT_POWER_STATUS = 1; - // State that waits for next monitoring. - private static final int STATE_WAIT_FOR_NEXT_MONITORING = 2; + // to the TV. + private static final int STATE_WAIT_FOR_REPORT_POWER_STATUS = 2; + // Monitoring interval (60s) @VisibleForTesting protected static final int MONITORING_INTERVAL_MS = 60000; // Timeout once sending <Give Device Power Status> - private static final int REPORT_POWER_STATUS_TIMEOUT_MS = 5000; - // Maximum number of retries in case the <Give Device Power Status> failed being sent or times - // out. - private static final int GIVE_POWER_STATUS_FOR_SOURCE_RETRIES = 5; - private int mPowerStatusRetries = 0; PowerStatusMonitorActionFromPlayback(HdmiCecLocalDevice source) { super(source); @@ -68,11 +64,10 @@ public class PowerStatusMonitorActionFromPlayback extends HdmiCecFeatureAction { private boolean handleReportPowerStatusFromTv(HdmiCecMessage cmd) { int powerStatus = cmd.getParams()[0] & 0xFF; - mState = STATE_WAIT_FOR_NEXT_MONITORING; - addTimer(mState, MONITORING_INTERVAL_MS); if (powerStatus == POWER_STATUS_STANDBY) { Slog.d(TAG, "TV reported it turned off, going to sleep."); source().getService().standby(); + finish(); return true; } return false; @@ -80,34 +75,28 @@ public class PowerStatusMonitorActionFromPlayback extends HdmiCecFeatureAction { @Override void handleTimerEvent(int state) { + if (mState != state) { + return; + } + switch (mState) { case STATE_WAIT_FOR_NEXT_MONITORING: - mPowerStatusRetries = 0; queryPowerStatus(); break; case STATE_WAIT_FOR_REPORT_POWER_STATUS: - handleTimeout(); + mState = STATE_WAIT_FOR_NEXT_MONITORING; + addTimer(mState, MONITORING_INTERVAL_MS); + break; + default: break; } } private void queryPowerStatus() { sendCommand(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(getSourceAddress(), - Constants.ADDR_TV)); + Constants.ADDR_TV)); mState = STATE_WAIT_FOR_REPORT_POWER_STATUS; - addTimer(mState, REPORT_POWER_STATUS_TIMEOUT_MS); - } - - private void handleTimeout() { - if (mState == STATE_WAIT_FOR_REPORT_POWER_STATUS) { - if (mPowerStatusRetries++ < GIVE_POWER_STATUS_FOR_SOURCE_RETRIES) { - queryPowerStatus(); - } else { - mPowerStatusRetries = 0; - mState = STATE_WAIT_FOR_NEXT_MONITORING; - addTimer(mState, MONITORING_INTERVAL_MS); - } - } + addTimer(mState, HdmiConfig.TIMEOUT_MS); } } diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 24296406da00..93fdbc787ed0 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -316,6 +316,31 @@ final class InputGestureManager { } } + @Nullable + public InputGestureData getInputGesture(int userId, InputGestureData.Trigger trigger) { + synchronized (mGestureLock) { + if (mBlockListedTriggers.contains(trigger)) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) { + if (KeyEvent.isModifierKey(keyTrigger.getKeycode()) || + KeyEvent.isSystemKey(keyTrigger.getKeycode())) { + return new InputGestureData.Builder().setTrigger(trigger).setKeyGestureType( + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_RESERVED).build(); + } + } + InputGestureData gestureData = mSystemShortcuts.get(trigger); + if (gestureData != null) { + return gestureData; + } + if (!mCustomInputGestures.contains(userId)) { + return null; + } + return mCustomInputGestures.get(userId).get(trigger); + } + } + @InputManager.CustomInputGestureResult public int addCustomInputGesture(int userId, InputGestureData newGesture) { synchronized (mGestureLock) { diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index b2c35e1f362e..2ba35d6a70d2 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3061,6 +3061,16 @@ public class InputManagerService extends IInputManager.Stub @Override @PermissionManuallyEnforced + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + enforceManageKeyGesturePermission(); + + Objects.requireNonNull(trigger); + return mKeyGestureController.getInputGesture(userId, trigger); + } + + @Override + @PermissionManuallyEnforced public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { enforceManageKeyGesturePermission(); diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 5f7ad2797368..fb5ce5b4e5fa 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -18,6 +18,8 @@ package com.android.server.input; import static android.content.pm.PackageManager.FEATURE_LEANBACK; import static android.content.pm.PackageManager.FEATURE_WATCH; +import static android.os.UserManager.isVisibleBackgroundUsersEnabled; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE; import static com.android.hardware.input.Flags.enableNew25q2Keycodes; @@ -55,7 +57,6 @@ import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; import android.util.SparseArray; -import android.view.Display; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -64,6 +65,8 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.IShortcutService; +import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; import com.android.server.policy.KeyCombinationManager; import java.util.ArrayDeque; @@ -159,6 +162,10 @@ final class KeyGestureController { /** Currently fully consumed key codes per device */ private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>(); + private final UserManagerInternal mUserManagerInternal; + + private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); + KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) { mContext = context; mHandler = new Handler(looper, this::handleMessage); @@ -180,6 +187,7 @@ final class KeyGestureController { mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext); mInputGestureManager = new InputGestureManager(mContext); mInputDataStore = inputDataStore; + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); initBehaviors(); initKeyCombinationRules(); } @@ -449,6 +457,9 @@ final class KeyGestureController { } public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { + if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) { + return false; + } final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0; if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures() && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { @@ -457,6 +468,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreKeyEventForVisibleBackgroundUser(KeyEvent event) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay( + event.getDisplayId()); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyEvent.isVisibleBackgroundUserAllowedKey(event.getKeyCode())) { + if (DEBUG) { + Slog.w(TAG, "Ignored key event [" + event + "] for visible background user [" + + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) { // TODO(b/358569822): Handle shortcuts trigger logic here and pass it to appropriate @@ -895,7 +924,7 @@ final class KeyGestureController { private void handleMultiKeyGesture(int[] keycodes, @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0, - gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags, + gestureType, action, DEFAULT_DISPLAY, /* focusedToken = */null, flags, /* appLaunchData = */null); } @@ -903,7 +932,7 @@ final class KeyGestureController { @Nullable AppLaunchData appLaunchData) { handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, new int[0], /* modifierState= */0, keyGestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, - Display.DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); + DEFAULT_DISPLAY, /* focusedToken = */null, /* flags = */0, appLaunchData); } @VisibleForTesting @@ -915,6 +944,11 @@ final class KeyGestureController { } private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { + if (mVisibleBackgroundUsersEnabled && event.displayId != DEFAULT_DISPLAY + && shouldIgnoreGestureEventForVisibleBackgroundUser(event.gestureType, + event.displayId)) { + return false; + } synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { if (handler.handleKeyGesture(event, focusedToken)) { @@ -927,6 +961,24 @@ final class KeyGestureController { return false; } + private boolean shouldIgnoreGestureEventForVisibleBackgroundUser( + @KeyGestureEvent.KeyGestureType int gestureType, int displayId) { + final int displayAssignedUserId = mUserManagerInternal.getUserAssignedToDisplay(displayId); + final int currentUserId; + synchronized (mUserLock) { + currentUserId = mCurrentUserId; + } + if (currentUserId != displayAssignedUserId + && !KeyGestureEvent.isVisibleBackgrounduserAllowedGesture(gestureType)) { + if (DEBUG) { + Slog.w(TAG, "Ignored gesture event [" + gestureType + + "] for visible background user [" + displayAssignedUserId + "]"); + } + return true; + } + return false; + } + private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) { synchronized (mKeyGestureHandlerRecords) { for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) { @@ -943,7 +995,7 @@ final class KeyGestureController { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally // should not rely on PWM to tell us about the gesture start and end. AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget(); } @@ -951,7 +1003,7 @@ final class KeyGestureController { public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, DEFAULT_DISPLAY, /* flags = */0, /* appLaunchData = */null); handleKeyGesture(event, null /*focusedToken*/); } @@ -1069,6 +1121,18 @@ final class KeyGestureController { } @BinderThread + @Nullable + public AidlInputGestureData getInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData.Trigger trigger) { + InputGestureData gestureData = mInputGestureManager.getInputGesture(userId, + InputGestureData.createTriggerFromAidlTrigger(trigger)); + if (gestureData == null) { + return null; + } + return gestureData.getAidlData(); + } + + @BinderThread @InputManager.CustomInputGestureResult public int addCustomInputGesture(@UserIdInt int userId, @NonNull AidlInputGestureData inputGestureData) { diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java index b0dff22c6f03..5fe8318dbb3f 100644 --- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -40,6 +40,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Binder; import android.os.IBinder; @@ -58,6 +59,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.server.LocalServices; +import com.android.server.pm.UserManagerInternal; import com.android.server.wm.WindowManagerInternal; import java.io.PrintWriter; @@ -78,6 +80,7 @@ public final class ImeVisibilityStateComputer { private final int mUserId; private final InputMethodManagerService mService; + private final UserManagerInternal mUserManagerInternal; private final WindowManagerInternal mWindowManagerInternal; final InputMethodManagerService.ImeDisplayValidator mImeDisplayValidator; @@ -188,6 +191,7 @@ public final class ImeVisibilityStateComputer { public ImeVisibilityStateComputer(@NonNull InputMethodManagerService service, @UserIdInt int userId) { this(service, + LocalServices.getService(UserManagerInternal.class), LocalServices.getService(WindowManagerInternal.class), LocalServices.getService(WindowManagerInternal.class)::getDisplayImePolicy, new ImeVisibilityPolicy(), userId); @@ -196,12 +200,15 @@ public final class ImeVisibilityStateComputer { @VisibleForTesting public ImeVisibilityStateComputer(@NonNull InputMethodManagerService service, @NonNull Injector injector) { - this(service, injector.getWmService(), injector.getImeValidator(), - new ImeVisibilityPolicy(), injector.getUserId()); + this(service, injector.getUserManagerService(), injector.getWmService(), + injector.getImeValidator(), new ImeVisibilityPolicy(), injector.getUserId()); } interface Injector { @NonNull + UserManagerInternal getUserManagerService(); + + @NonNull WindowManagerInternal getWmService(); @NonNull @@ -212,11 +219,13 @@ public final class ImeVisibilityStateComputer { } private ImeVisibilityStateComputer(InputMethodManagerService service, + UserManagerInternal userManagerInternal, WindowManagerInternal wmService, InputMethodManagerService.ImeDisplayValidator imeDisplayValidator, ImeVisibilityPolicy imePolicy, @UserIdInt int userId) { mUserId = userId; mService = service; + mUserManagerInternal = userManagerInternal; mWindowManagerInternal = wmService; mImeDisplayValidator = imeDisplayValidator; mPolicy = imePolicy; @@ -337,7 +346,16 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") int computeImeDisplayId(@NonNull ImeTargetWindowState state, int displayId) { - final int displayToShowIme = computeImeDisplayIdForTarget(displayId, mImeDisplayValidator); + final int displayToShowIme; + final PackageManager pm = mService.mContext.getPackageManager(); + if (pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE) + && mUserManagerInternal.isVisibleBackgroundFullUser(mUserId) + && Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + displayToShowIme = mService.computeImeDisplayIdForVisibleBackgroundUserOnAutomotive( + displayId, mUserId, mImeDisplayValidator); + } else { + displayToShowIme = computeImeDisplayIdForTarget(displayId, mImeDisplayValidator); + } state.setImeDisplayId(displayToShowIme); final boolean imeHiddenByPolicy = displayToShowIme == INVALID_DISPLAY; mPolicy.setImeHiddenByDisplayPolicy(imeHiddenByPolicy); @@ -387,8 +405,9 @@ public final class ImeVisibilityStateComputer { @GuardedBy("ImfLock.class") void setWindowState(IBinder windowToken, @NonNull ImeTargetWindowState newState) { final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); - if (state != null && newState.hasEditorFocused() - && newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS) { + if (state != null && newState.hasEditorFocused() && ( + newState.mToolType != MotionEvent.TOOL_TYPE_STYLUS + || Flags.refactorInsetsController())) { // Inherit the last requested IME visible state when the target window is still // focused with an editor. newState.setRequestedImeVisible(state.mRequestedImeVisible); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 1f414ac07ba3..45c7cffd462b 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -2345,8 +2345,32 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. * {@link WindowManager#DISPLAY_IME_POLICY_HIDE} */ static int computeImeDisplayIdForTarget(int displayId, @NonNull ImeDisplayValidator checker) { - if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) { - return FALLBACK_DISPLAY_ID; + return computeImeDisplayIdForTargetInner(displayId, checker, FALLBACK_DISPLAY_ID); + } + + /** + * Find the display where the IME should be shown for a visible background user. + * + * @param displayId the ID of the display where the IME client target is + * @param userId the ID of the user who own the IME + * @param checker instance of {@link ImeDisplayValidator} which is used for + * checking display config to adjust the final target display + * @return the ID of the display where the IME should be shown or + * {@link android.view.Display#INVALID_DISPLAY} if the display has an ImePolicy of + * {@link WindowManager#DISPLAY_IME_POLICY_HIDE} + */ + int computeImeDisplayIdForVisibleBackgroundUserOnAutomotive( + int displayId, @UserIdInt int userId, @NonNull ImeDisplayValidator checker) { + // Visible background user can be assigned to a secondary display, not the default display. + // The main display assigned to the user will be used as the fallback display. + final int mainDisplayId = mUserManagerInternal.getMainDisplayAssignedToUser(userId); + return computeImeDisplayIdForTargetInner(displayId, checker, mainDisplayId); + } + + private static int computeImeDisplayIdForTargetInner( + int displayId, @NonNull ImeDisplayValidator checker, int fallbackDisplayId) { + if (displayId == fallbackDisplayId || displayId == INVALID_DISPLAY) { + return fallbackDisplayId; } // Show IME window on fallback display when the display doesn't support system decorations @@ -2356,9 +2380,8 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. return displayId; } else if (result == DISPLAY_IME_POLICY_HIDE) { return INVALID_DISPLAY; - } else { - return FALLBACK_DISPLAY_ID; } + return fallbackDisplayId; } @GuardedBy("ImfLock.class") diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java index 87d809b5e850..1e54beeb2d64 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java @@ -32,7 +32,10 @@ import android.hardware.location.ContextHubTransaction; import android.hardware.location.IContextHubTransactionCallback; import android.os.Binder; import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; import android.os.RemoteException; +import android.os.WorkSource; import android.util.Log; import android.util.SparseArray; @@ -54,6 +57,16 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub /** Message used by noteOp when this client receives a message from an endpoint. */ private static final String RECEIVE_MSG_NOTE = "ContextHubEndpointMessageDelivery"; + /** The duration of wakelocks acquired during HAL callbacks */ + private static final long WAKELOCK_TIMEOUT_MILLIS = 5 * 1000; + + /* + * Internal interface used to invoke client callbacks. + */ + interface CallbackConsumer { + void accept(IContextHubEndpointCallback callback) throws RemoteException; + } + /** The context of the service. */ private final Context mContext; @@ -134,6 +147,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub private final int mUid; + /** Wakelock held while nanoapp message are in flight to the client */ + private final WakeLock mWakeLock; + /* package */ ContextHubEndpointBroker( Context context, IEndpointCommunication hubInterface, @@ -158,6 +174,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mAppOpsManager = context.getSystemService(AppOpsManager.class); mAppOpsManager.startWatchingMode(AppOpsManager.OP_NONE, mPackageName, this); + + PowerManager powerManager = context.getSystemService(PowerManager.class); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.setWorkSource(new WorkSource(mUid, mPackageName)); + mWakeLock.setReferenceCounted(true); } @Override @@ -227,6 +248,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } mEndpointManager.unregisterEndpoint(mEndpointInfo.getIdentifier().getEndpoint()); + releaseWakeLockOnExit(); } @Override @@ -302,6 +324,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } } + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + public void onCallbackFinished() { + super.onCallbackFinished_enforcePermission(); + releaseWakeLock(); + } + /** Invoked when the underlying binder of this broker has died at the client process. */ @Override public void binderDied() { @@ -357,15 +386,13 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true)); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenRequest( - sessionId, initiator, serviceDescriptor); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionOpenRequest", e); - cleanupSessionResources(sessionId); - return; - } + boolean success = + invokeCallback( + (consumer) -> + consumer.onSessionOpenRequest( + sessionId, initiator, serviceDescriptor)); + if (!success) { + cleanupSessionResources(sessionId); } } @@ -374,14 +401,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Log.w(TAG, "Unknown session ID in onCloseEndpointSession: id=" + sessionId); return; } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionClosed( - sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason)); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback( + (consumer) -> + consumer.onSessionClosed( + sessionId, ContextHubServiceUtil.toAppHubEndpointReason(reason))); } /* package */ void onEndpointSessionOpenComplete(int sessionId) { @@ -392,16 +416,30 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub } mSessionInfoMap.get(sessionId).setSessionState(SessionInfo.SessionState.ACTIVE); } - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onSessionOpenComplete(sessionId); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onSessionClosed", e); - } - } + + invokeCallback((consumer) -> consumer.onSessionOpenComplete(sessionId)); } /* package */ void onMessageReceived(int sessionId, HubMessage message) { + byte code = onMessageReceivedInternal(sessionId, message); + if (code != ErrorCode.OK && message.isResponseRequired()) { + sendMessageDeliveryStatus( + sessionId, message.getMessageSequenceNumber(), code); + } + } + + /* package */ void onMessageDeliveryStatusReceived( + int sessionId, int sequenceNumber, byte errorCode) { + mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); + } + + /* package */ boolean hasSessionId(int sessionId) { + synchronized (mOpenSessionLock) { + return mSessionInfoMap.contains(sessionId); + } + } + + private byte onMessageReceivedInternal(int sessionId, HubMessage message) { HubEndpointInfo remote; synchronized (mOpenSessionLock) { if (!isSessionActive(sessionId)) { @@ -411,9 +449,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + sessionId + ") with message: " + message); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMANENT_ERROR); - return; + return ErrorCode.PERMANENT_ERROR; } remote = mSessionInfoMap.get(sessionId).getRemoteEndpointInfo(); } @@ -435,31 +471,12 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub + ". " + mPackageName + " doesn't have permission"); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.PERMISSION_DENIED); - return; - } - - if (mContextHubEndpointCallback != null) { - try { - mContextHubEndpointCallback.onMessageReceived(sessionId, message); - } catch (RemoteException e) { - Log.e(TAG, "RemoteException while calling onMessageReceived", e); - sendMessageDeliveryStatus( - sessionId, message.getMessageSequenceNumber(), ErrorCode.TRANSIENT_ERROR); - } + return ErrorCode.PERMISSION_DENIED; } - } - - /* package */ void onMessageDeliveryStatusReceived( - int sessionId, int sequenceNumber, byte errorCode) { - mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK); - } - /* package */ boolean hasSessionId(int sessionId) { - synchronized (mOpenSessionLock) { - return mSessionInfoMap.contains(sessionId); - } + boolean success = + invokeCallback((consumer) -> consumer.onMessageReceived(sessionId, message)); + return success ? ErrorCode.OK : ErrorCode.TRANSIENT_ERROR; } /** @@ -520,4 +537,63 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub Collection<String> requiredPermissions = targetEndpointInfo.getRequiredPermissions(); return ContextHubServiceUtil.hasPermissions(mContext, mPid, mUid, requiredPermissions); } + + private void acquireWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mIsRegistered.get()) { + mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS); + } + }); + } + + private void releaseWakeLock() { + Binder.withCleanCallingIdentity( + () -> { + if (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e(TAG, "Releasing the wakelock fails - ", e); + } + } + }); + } + + private void releaseWakeLockOnExit() { + Binder.withCleanCallingIdentity( + () -> { + while (mWakeLock.isHeld()) { + try { + mWakeLock.release(); + } catch (RuntimeException e) { + Log.e( + TAG, + "Releasing the wakelock for all acquisitions fails - ", + e); + break; + } + } + }); + } + + /** + * Invokes a callback and acquires a wakelock. + * + * @param consumer The callback invoke + * @return false if the callback threw a RemoteException + */ + private boolean invokeCallback(CallbackConsumer consumer) { + if (mContextHubEndpointCallback != null) { + acquireWakeLock(); + try { + consumer.accept(mContextHubEndpointCallback); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while calling endpoint callback", e); + releaseWakeLock(); + return false; + } + } + return true; + } } diff --git a/services/core/java/com/android/server/location/fudger/LocationFudger.java b/services/core/java/com/android/server/location/fudger/LocationFudger.java index 27577641ad1d..39ee0cbeacce 100644 --- a/services/core/java/com/android/server/location/fudger/LocationFudger.java +++ b/services/core/java/com/android/server/location/fudger/LocationFudger.java @@ -68,6 +68,17 @@ public class LocationFudger { private static final double MAX_LATITUDE = 90.0 - (1.0 / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR); + // The average edge length in km of an S2 cell, indexed by S2 levels 0 to + // 13. Level 13 is the highest level used for coarsening. + // This approximation assumes the S2 cells are squares. + // For density-based coarsening, we use the edge to set the accuracy of the + // coarsened location. + // The values are from http://s2geometry.io/resources/s2cell_statistics.html + // We take square root of the average area. + private static final float[] S2_CELL_AVG_EDGE_PER_LEVEL = new float[] { + 9220.14f, 4610.07f, 2305.04f, 1152.52f, 576.26f, 288.13f, 144.06f, + 72.03f, 36.02f, 20.79f, 9f, 5.05f, 2.25f, 1.13f, 0.57f}; + private final float mAccuracyM; private final Clock mClock; private final Random mRandom; @@ -194,12 +205,14 @@ public class LocationFudger { // The new algorithm is applied if and only if (1) the flag is on, (2) the cache has been // set, and (3) the cache has successfully queried the provider for the default coarsening // value. + float accuracy = mAccuracyM; if (Flags.populationDensityProvider() && Flags.densityBasedCoarseLocations() && cacheCopy != null) { if (cacheCopy.hasDefaultValue()) { // New algorithm that snaps to the center of a S2 cell. int level = cacheCopy.getCoarseningLevel(latitude, longitude); coarsened = snapToCenterOfS2Cell(latitude, longitude, level); + accuracy = getS2CellApproximateEdge(level); } else { // Try to fetch the default value. The answer won't come in time, but will be used // for the next location to coarsen. @@ -214,7 +227,7 @@ public class LocationFudger { coarse.setLatitude(coarsened[LAT_INDEX]); coarse.setLongitude(coarsened[LNG_INDEX]); - coarse.setAccuracy(Math.max(mAccuracyM, coarse.getAccuracy())); + coarse.setAccuracy(Math.max(accuracy, coarse.getAccuracy())); synchronized (this) { mCachedFineLocation = fine; @@ -224,6 +237,19 @@ public class LocationFudger { return coarse; } + // Returns the average edge length in meters of an S2 cell at the given + // level. This is computed as if the S2 cell were a square. We do not need + // an exact value, only a rough approximation. + @VisibleForTesting + protected float getS2CellApproximateEdge(int level) { + if (level < 0) { + level = 0; + } else if (level >= S2_CELL_AVG_EDGE_PER_LEVEL.length) { + level = S2_CELL_AVG_EDGE_PER_LEVEL.length - 1; + } + return S2_CELL_AVG_EDGE_PER_LEVEL[level] * 1000; + } + // quantize location by snapping to a grid. this is the primary means of obfuscation. it // gives nice consistent results and is very effective at hiding the true location (as // long as you are not sitting on a grid boundary, which the random offsets mitigate). @@ -302,6 +328,15 @@ public class LocationFudger { // requires latitude since longitudinal distances change with distance from equator. private static double metersToDegreesLongitude(double distance, double lat) { - return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / Math.cos(Math.toRadians(lat)); + // Needed to convert from longitude distance to longitude degree. + // X meters near the poles is more degrees than at the equator. + double cosLat = Math.cos(Math.toRadians(lat)); + // If we are right on top of the pole, the degree is always 0. + // We return a very small value instead to avoid divide by zero errors + // later on. + if (cosLat == 0.0) { + return 0.0001; + } + return distance / APPROXIMATE_METERS_PER_DEGREE_AT_EQUATOR / cosLat; } } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 286238e7888c..a0e543300ce7 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -137,6 +137,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; +import com.android.internal.pm.RoSystemFeatures; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; @@ -438,9 +439,9 @@ public class LockSettingsService extends ILockSettings.Stub { } LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword(newPassword); - Arrays.fill(newPasswordChars, '\u0000'); - Arrays.fill(newPassword, (byte) 0); - Arrays.fill(randomLockSeed, (byte) 0); + LockPatternUtils.zeroize(newPasswordChars); + LockPatternUtils.zeroize(newPassword); + LockPatternUtils.zeroize(randomLockSeed); return credential; } @@ -1325,7 +1326,7 @@ public class LockSettingsService extends ILockSettings.Stub { mContext.enforceCallingOrSelfPermission( Manifest.permission.MANAGE_WEAK_ESCROW_TOKEN, "Requires MANAGE_WEAK_ESCROW_TOKEN permission."); - if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { + if (!RoSystemFeatures.hasFeatureAutomotive(mContext)) { throw new IllegalArgumentException( "Weak escrow token are only for automotive devices."); } @@ -1537,7 +1538,7 @@ public class LockSettingsService extends ILockSettings.Stub { + userId); } } finally { - Arrays.fill(password, (byte) 0); + LockPatternUtils.zeroize(password); } } @@ -1570,7 +1571,7 @@ public class LockSettingsService extends ILockSettings.Stub { decryptionResult = cipher.doFinal(encryptedPassword); LockscreenCredential credential = LockscreenCredential.createUnifiedProfilePassword( decryptionResult); - Arrays.fill(decryptionResult, (byte) 0); + LockPatternUtils.zeroize(decryptionResult); try { long parentSid = getGateKeeperService().getSecureUserId( mUserManager.getProfileParent(userId).id); @@ -2263,7 +2264,7 @@ public class LockSettingsService extends ILockSettings.Stub { } catch (RemoteException e) { Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId); } finally { - Arrays.fill(secret, (byte) 0); + LockPatternUtils.zeroize(secret); } } @@ -3613,7 +3614,7 @@ public class LockSettingsService extends ILockSettings.Stub { } // Escrow tokens are enabled on automotive builds. - if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { + if (RoSystemFeatures.hasFeatureAutomotive(mContext)) { return; } diff --git a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java index 21caf76d30d0..3d64f1890073 100644 --- a/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java +++ b/services/core/java/com/android/server/locksettings/UnifiedProfilePasswordCache.java @@ -26,6 +26,7 @@ import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import java.security.GeneralSecurityException; @@ -154,7 +155,7 @@ public class UnifiedProfilePasswordCache { } LockscreenCredential result = LockscreenCredential.createUnifiedProfilePassword(credential); - Arrays.fill(credential, (byte) 0); + LockPatternUtils.zeroize(credential); return result; } } @@ -175,7 +176,7 @@ public class UnifiedProfilePasswordCache { Slog.d(TAG, "Cannot delete key", e); } if (mEncryptedPasswords.contains(userId)) { - Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0); + LockPatternUtils.zeroize(mEncryptedPasswords.get(userId)); mEncryptedPasswords.remove(userId); } } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java index bf1b3c3f0b35..85dc811a7811 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java @@ -162,7 +162,7 @@ public class KeySyncTask implements Runnable { Log.e(TAG, "Unexpected exception thrown during KeySyncTask", e); } finally { if (mCredential != null) { - Arrays.fill(mCredential, (byte) 0); // no longer needed. + LockPatternUtils.zeroize(mCredential); // no longer needed. } } } @@ -506,7 +506,7 @@ public class KeySyncTask implements Runnable { try { byte[] hash = MessageDigest.getInstance(LOCK_SCREEN_HASH_ALGORITHM).digest(bytes); - Arrays.fill(bytes, (byte) 0); + LockPatternUtils.zeroize(bytes); return hash; } catch (NoSuchAlgorithmException e) { // Impossible, SHA-256 must be supported on Android. diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java index 54303c01890a..7d8300a8148a 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java @@ -1082,7 +1082,7 @@ public class RecoverableKeyStoreManager { int keyguardCredentialsType = lockPatternUtilsToKeyguardType(savedCredentialType); try (LockscreenCredential credential = createLockscreenCredential(keyguardCredentialsType, decryptedCredentials)) { - Arrays.fill(decryptedCredentials, (byte) 0); + LockPatternUtils.zeroize(decryptedCredentials); decryptedCredentials = null; VerifyCredentialResponse verifyResponse = lockSettingsService.verifyCredential(credential, userId, 0); diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java index 0e66746f4160..f1ef333d223a 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java @@ -19,8 +19,9 @@ package com.android.server.locksettings.recoverablekeystore.storage; import android.annotation.Nullable; import android.util.SparseArray; +import com.android.internal.widget.LockPatternUtils; + import java.util.ArrayList; -import java.util.Arrays; import javax.security.auth.Destroyable; @@ -187,8 +188,8 @@ public class RecoverySessionStorage implements Destroyable { */ @Override public void destroy() { - Arrays.fill(mLskfHash, (byte) 0); - Arrays.fill(mKeyClaimant, (byte) 0); + LockPatternUtils.zeroize(mLskfHash); + LockPatternUtils.zeroize(mKeyClaimant); } } } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 68e195d7f079..35bb19943a24 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -302,7 +302,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub final long token = Binder.clearCallingIdentity(); try { - mAudioService.setBluetoothA2dpOn(on); + if (!Flags.disableSetBluetoothAd2pOnCalls()) { + mAudioService.setBluetoothA2dpOn(on); + } } catch (RemoteException ex) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn. on=" + on); } finally { @@ -677,7 +679,9 @@ public final class MediaRouterService extends IMediaRouterService.Stub if (DEBUG) { Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")"); } - mAudioService.setBluetoothA2dpOn(a2dpOn); + if (!Flags.disableSetBluetoothAd2pOnCalls()) { + mAudioService.setBluetoothA2dpOn(a2dpOn); + } } } catch (RemoteException e) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn."); diff --git a/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java b/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java index 7e747ce60785..2c6cebf6feb3 100644 --- a/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java +++ b/services/core/java/com/android/server/media/MediaSessionDeviceConfig.java @@ -46,6 +46,7 @@ class MediaSessionDeviceConfig { private static volatile long sMediaSessionCallbackFgsAllowlistDurationMs = DEFAULT_MEDIA_SESSION_CALLBACK_FGS_ALLOWLIST_DURATION_MS; + /** * Denotes the duration for which an app receiving a media session callback and the FGS started * there can be temporarily allowed to have while-in-use permissions such as @@ -58,6 +59,16 @@ class MediaSessionDeviceConfig { private static volatile long sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs = DEFAULT_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS; + /** + * Denotes the duration (in milliseconds) that a media session can remain in an engaged state, + * where it is only considered engaged if transitioning from active playback. + */ + private static final String KEY_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS = + "media_session_temp_user_engaged_duration_ms"; + private static final long DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS = 600_000; + private static volatile long sMediaSessionTempUserEngagedDurationMs = + DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS; + private static void refresh(DeviceConfig.Properties properties) { final Set<String> keys = properties.getKeyset(); properties.getKeyset().forEach(key -> { @@ -73,6 +84,11 @@ class MediaSessionDeviceConfig { case KEY_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS: sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs = properties.getLong(key, DEFAULT_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS); + break; + case KEY_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS: + sMediaSessionTempUserEngagedDurationMs = properties.getLong(key, + DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS); + break; } }); } @@ -110,6 +126,15 @@ class MediaSessionDeviceConfig { return sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs; } + /** + * Returns the duration (in milliseconds) that a media session can remain in an engaged state, + * where it is only considered engaged if transitioning from active playback. After this + * duration, the session is disengaged until explicit user action triggers active playback. + */ + public static long getMediaSessionTempUserEngagedDurationMs() { + return sMediaSessionTempUserEngagedDurationMs; + } + public static void dump(PrintWriter pw, String prefix) { pw.println("Media session config:"); final String dumpFormat = prefix + " %s: [cur: %s, def: %s]"; @@ -125,5 +150,9 @@ class MediaSessionDeviceConfig { KEY_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS, sMediaSessionCallbackFgsWhileInUseTempAllowDurationMs, DEFAULT_MEDIA_SESSION_CALLBACK_FGS_WHILE_IN_USE_TEMP_ALLOW_DURATION_MS)); + pw.println(TextUtils.formatSimple(dumpFormat, + KEY_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS, + sMediaSessionTempUserEngagedDurationMs, + DEFAULT_MEDIA_SESSION_TEMP_USER_ENGAGED_DURATION_MS)); } } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 5f7c86f7b670..d873075a19e4 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -264,15 +264,6 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde */ private static final int USER_DISENGAGED = 2; - /** - * Indicates the duration of the temporary engaged state, in milliseconds. - * - * <p>When switching to an {@linkplain PlaybackState#isActive() inactive state}, the user is - * treated as temporarily engaged, meaning the corresponding session is only considered in an - * engaged state for the duration of this timeout, and only if coming from an engaged state. - */ - private static final int TEMP_USER_ENGAGED_TIMEOUT_MS = 600000; - public MediaSessionRecord( int ownerPid, int ownerUid, @@ -605,7 +596,8 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde if (mUserEngagementState == USER_TEMPORARILY_ENGAGED) { mHandler.postDelayed( mUserEngagementTimeoutExpirationRunnable, - TEMP_USER_ENGAGED_TIMEOUT_MS); + MediaSessionDeviceConfig + .getMediaSessionTempUserEngagedDurationMs()); } } } @@ -1079,7 +1071,8 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde mUserEngagementState = newUserEngagedState; if (newUserEngagedState == USER_TEMPORARILY_ENGAGED && !isGlobalPrioritySessionActive) { mHandler.postDelayed( - mUserEngagementTimeoutExpirationRunnable, TEMP_USER_ENGAGED_TIMEOUT_MS); + mUserEngagementTimeoutExpirationRunnable, + MediaSessionDeviceConfig.getMediaSessionTempUserEngagedDurationMs()); } else { mHandler.removeCallbacks(mUserEngagementTimeoutExpirationRunnable); } diff --git a/services/core/java/com/android/server/media/TEST_MAPPING b/services/core/java/com/android/server/media/TEST_MAPPING index 43e2afd8827d..dbf9915c6e0c 100644 --- a/services/core/java/com/android/server/media/TEST_MAPPING +++ b/services/core/java/com/android/server/media/TEST_MAPPING @@ -1,7 +1,10 @@ { "presubmit": [ { - "name": "CtsMediaBetterTogetherTestCases" + "name": "CtsMediaRouterTestCases" + }, + { + "name": "CtsMediaSessionTestCases" }, { "name": "MediaRouterServiceTests" diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 86fc732e9d04..d9df09dad853 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.hardware.tv.mediaquality.IMediaQuality; import android.media.quality.AmbientBacklightSettings; import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IMediaQualityManager; @@ -35,9 +36,11 @@ import android.media.quality.SoundProfile; import android.media.quality.SoundProfileHandle; import android.os.Binder; import android.os.Bundle; +import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.util.Log; import android.util.Pair; @@ -45,6 +48,7 @@ import android.util.Slog; import android.util.SparseArray; import com.android.server.SystemService; +import com.android.server.utils.Slogf; import org.json.JSONException; import org.json.JSONObject; @@ -74,6 +78,7 @@ public class MediaQualityService extends SystemService { private final BiMap<Long, String> mSoundProfileTempIdMap; private final PackageManager mPackageManager; private final SparseArray<UserState> mUserStates = new SparseArray<>(); + private IMediaQuality mMediaQuality; public MediaQualityService(Context context) { super(context); @@ -88,6 +93,12 @@ public class MediaQualityService extends SystemService { @Override public void onStart() { + IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default"); + if (binder != null) { + Slogf.d(TAG, "binder is not null"); + mMediaQuality = IMediaQuality.Stub.asInterface(binder); + } + publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService()); } @@ -99,7 +110,7 @@ public class MediaQualityService extends SystemService { if ((pp.getPackageName() != null && !pp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(pp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { - notifyError(null, PictureProfile.ERROR_NO_PERMISSION, + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -125,7 +136,7 @@ public class MediaQualityService extends SystemService { public void updatePictureProfile(String id, PictureProfile pp, UserHandle user) { Long dbId = mPictureProfileTempIdMap.getKey(id); if (!hasPermissionToUpdatePictureProfile(dbId, pp)) { - notifyError(id, PictureProfile.ERROR_NO_PERMISSION, + notifyPictureProfileError(id, PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -154,7 +165,7 @@ public class MediaQualityService extends SystemService { Long dbId = mPictureProfileTempIdMap.getKey(id); if (!hasPermissionToRemovePictureProfile(dbId)) { - notifyError(id, PictureProfile.ERROR_NO_PERMISSION, + notifyPictureProfileError(id, PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -165,7 +176,7 @@ public class MediaQualityService extends SystemService { int result = db.delete(mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME, selection, selectionArgs); if (result == 0) { - notifyError(id, PictureProfile.ERROR_INVALID_ARGUMENT, + notifyPictureProfileError(id, PictureProfile.ERROR_INVALID_ARGUMENT, Binder.getCallingUid(), Binder.getCallingPid()); } mPictureProfileTempIdMap.remove(dbId); @@ -235,7 +246,7 @@ public class MediaQualityService extends SystemService { public List<PictureProfile> getPictureProfilesByPackage( String packageName, Bundle options, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyError(null, PictureProfile.ERROR_NO_PERMISSION, + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } @@ -248,8 +259,7 @@ public class MediaQualityService extends SystemService { } @Override - public List<PictureProfile> getAvailablePictureProfiles( - Bundle options, UserHandle user) { + public List<PictureProfile> getAvailablePictureProfiles(Bundle options, UserHandle user) { String packageName = getPackageOfCallingUid(); if (packageName != null) { return getPictureProfilesByPackage(packageName, options, user); @@ -260,7 +270,7 @@ public class MediaQualityService extends SystemService { @Override public boolean setDefaultPictureProfile(String profileId, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyError(profileId, PictureProfile.ERROR_NO_PERMISSION, + notifyPictureProfileError(profileId, PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } // TODO: pass the profile ID to MediaQuality HAL when ready. @@ -270,7 +280,7 @@ public class MediaQualityService extends SystemService { @Override public List<String> getPictureProfilePackageNames(UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - notifyError(null, PictureProfile.ERROR_NO_PERMISSION, + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, Binder.getCallingUid(), Binder.getCallingPid()); } String [] column = {BaseParameters.PARAMETER_PACKAGE}; @@ -283,13 +293,31 @@ public class MediaQualityService extends SystemService { } @Override - public List<PictureProfileHandle> getPictureProfileHandle(String[] id, UserHandle user) { - return new ArrayList<>(); + public List<PictureProfileHandle> getPictureProfileHandle(String[] ids, UserHandle user) { + List<PictureProfileHandle> toReturn = new ArrayList<>(); + for (String id : ids) { + Long key = mPictureProfileTempIdMap.getKey(id); + if (key != null) { + toReturn.add(new PictureProfileHandle(key)); + } else { + toReturn.add(null); + } + } + return toReturn; } @Override - public List<SoundProfileHandle> getSoundProfileHandle(String[] id, UserHandle user) { - return new ArrayList<>(); + public List<SoundProfileHandle> getSoundProfileHandle(String[] ids, UserHandle user) { + List<SoundProfileHandle> toReturn = new ArrayList<>(); + for (String id : ids) { + Long key = mSoundProfileTempIdMap.getKey(id); + if (key != null) { + toReturn.add(new SoundProfileHandle(key)); + } else { + toReturn.add(null); + } + } + return toReturn; } @Override @@ -297,8 +325,8 @@ public class MediaQualityService extends SystemService { if ((sp.getPackageName() != null && !sp.getPackageName().isEmpty() && !incomingPackageEqualsCallingUidPackage(sp.getPackageName())) && !hasGlobalPictureQualityServicePermission()) { - //TODO: error handling - return null; + notifySoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); @@ -321,10 +349,9 @@ public class MediaQualityService extends SystemService { @Override public void updateSoundProfile(String id, SoundProfile sp, UserHandle user) { Long dbId = mSoundProfileTempIdMap.getKey(id); - if (!hasPermissionToUpdateSoundProfile(dbId, sp)) { - //TODO: error handling - return; + notifySoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } ContentValues values = getContentValues(dbId, @@ -348,22 +375,23 @@ public class MediaQualityService extends SystemService { @Override public void removeSoundProfile(String id, UserHandle user) { - Long intId = mSoundProfileTempIdMap.getKey(id); - if (!hasPermissionToRemoveSoundProfile(intId)) { - //TODO: error handling - return; + Long dbId = mSoundProfileTempIdMap.getKey(id); + if (!hasPermissionToRemoveSoundProfile(dbId)) { + notifySoundProfileError(id, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } - if (intId != null) { + if (dbId != null) { SQLiteDatabase db = mMediaQualityDbHelper.getWritableDatabase(); String selection = BaseParameters.PARAMETER_ID + " = ?"; - String[] selectionArgs = {Long.toString(intId)}; + String[] selectionArgs = {Long.toString(dbId)}; int result = db.delete(mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME, selection, selectionArgs); if (result == 0) { - //TODO: error handling + notifySoundProfileError(id, SoundProfile.ERROR_INVALID_ARGUMENT, + Binder.getCallingUid(), Binder.getCallingPid()); } - mSoundProfileTempIdMap.remove(intId); + mSoundProfileTempIdMap.remove(dbId); } } @@ -392,7 +420,7 @@ public class MediaQualityService extends SystemService { return null; } if (count > 1) { - Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%s" + Log.wtf(TAG, String.format(Locale.US, "%d entries found for name=%s" + " in %s. Should only ever be 0 or 1.", count, name, mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME)); return null; @@ -430,8 +458,8 @@ public class MediaQualityService extends SystemService { public List<SoundProfile> getSoundProfilesByPackage( String packageName, Bundle options, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - //TODO: error handling - return new ArrayList<>(); + notifySoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } boolean includeParams = @@ -454,8 +482,8 @@ public class MediaQualityService extends SystemService { @Override public boolean setDefaultSoundProfile(String profileId, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - //TODO: error handling - return false; + notifySoundProfileError(profileId, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } // TODO: pass the profile ID to MediaQuality HAL when ready. return false; @@ -464,8 +492,8 @@ public class MediaQualityService extends SystemService { @Override public List<String> getSoundProfilePackageNames(UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - //TODO: error handling - return new ArrayList<>(); + notifySoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } String [] column = {BaseParameters.PARAMETER_NAME}; List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column, @@ -707,23 +735,48 @@ public class MediaQualityService extends SystemService { } } - private void notifyError(String profileId, int errorCode, int uid, int pid) { + private void notifyPictureProfileError(String profileId, int errorCode, int uid, int pid) { UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); - int n = userState.mCallbacks.beginBroadcast(); + int n = userState.mPictureProfileCallbacks.beginBroadcast(); for (int i = 0; i < n; ++i) { try { - IPictureProfileCallback callback = userState.mCallbacks.getBroadcastItem(i); - Pair<Integer, Integer> pidUid = userState.mCallbackPidUidMap.get(callback); + IPictureProfileCallback callback = userState.mPictureProfileCallbacks + .getBroadcastItem(i); + Pair<Integer, Integer> pidUid = userState.mPictureProfileCallbackPidUidMap + .get(callback); if (pidUid.first == pid && pidUid.second == uid) { - userState.mCallbacks.getBroadcastItem(i).onError(profileId, errorCode); + userState.mPictureProfileCallbacks.getBroadcastItem(i) + .onError(profileId, errorCode); } } catch (RemoteException e) { Slog.e(TAG, "failed to report added input to callback", e); } } - userState.mCallbacks.finishBroadcast(); + userState.mPictureProfileCallbacks.finishBroadcast(); + } + + private void notifySoundProfileError(String profileId, int errorCode, int uid, int pid) { + UserState userState = getOrCreateUserStateLocked(UserHandle.USER_SYSTEM); + int n = userState.mSoundProfileCallbacks.beginBroadcast(); + + for (int i = 0; i < n; ++i) { + try { + ISoundProfileCallback callback = userState.mSoundProfileCallbacks + .getBroadcastItem(i); + Pair<Integer, Integer> pidUid = userState.mSoundProfileCallbackPidUidMap + .get(callback); + + if (pidUid.first == pid && pidUid.second == uid) { + userState.mSoundProfileCallbacks.getBroadcastItem(i) + .onError(profileId, errorCode); + } + } catch (RemoteException e) { + Slog.e(TAG, "failed to report added input to callback", e); + } + } + userState.mSoundProfileCallbacks.finishBroadcast(); } @Override @@ -732,11 +785,18 @@ public class MediaQualityService extends SystemService { int callingUid = Binder.getCallingUid(); UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); - userState.mCallbackPidUidMap.put(callback, Pair.create(callingPid, callingUid)); + userState.mPictureProfileCallbackPidUidMap.put(callback, + Pair.create(callingPid, callingUid)); } @Override public void registerSoundProfileCallback(final ISoundProfileCallback callback) { + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + + UserState userState = getOrCreateUserStateLocked(Binder.getCallingUid()); + userState.mSoundProfileCallbackPidUidMap.put(callback, + Pair.create(callingPid, callingUid)); } @Override @@ -770,8 +830,8 @@ public class MediaQualityService extends SystemService { @Override public List<String> getPictureProfileAllowList(UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - //TODO: error handling - return new ArrayList<>(); + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } return new ArrayList<>(); } @@ -779,15 +839,16 @@ public class MediaQualityService extends SystemService { @Override public void setPictureProfileAllowList(List<String> packages, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - //TODO: error handling + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } } @Override public List<String> getSoundProfileAllowList(UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - //TODO: error handling - return new ArrayList<>(); + notifySoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } return new ArrayList<>(); } @@ -795,7 +856,8 @@ public class MediaQualityService extends SystemService { @Override public void setSoundProfileAllowList(List<String> packages, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - //TODO: error handling + notifySoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); } } @@ -807,36 +869,96 @@ public class MediaQualityService extends SystemService { @Override public void setAutoPictureQualityEnabled(boolean enabled, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - //TODO: error handling + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoPqEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto picture quality", e); } } @Override public boolean isAutoPictureQualityEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoPqEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto picture quality", e); + } return false; } @Override public void setSuperResolutionEnabled(boolean enabled, UserHandle user) { if (!hasGlobalPictureQualityServicePermission()) { - //TODO: error handling + notifyPictureProfileError(null, PictureProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoSrEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto super resolution", e); } } @Override public boolean isSuperResolutionEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoSrEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto super resolution", e); + } return false; } @Override public void setAutoSoundQualityEnabled(boolean enabled, UserHandle user) { if (!hasGlobalSoundQualityServicePermission()) { - //TODO: error handling + notifySoundProfileError(null, SoundProfile.ERROR_NO_PERMISSION, + Binder.getCallingUid(), Binder.getCallingPid()); + } + + try { + if (mMediaQuality != null) { + mMediaQuality.setAutoAqEnabled(enabled); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to set auto audio quality", e); } } @Override public boolean isAutoSoundQualityEnabled(UserHandle user) { + try { + if (mMediaQuality != null) { + return mMediaQuality.getAutoAqEnabled(); + } + } catch (UnsupportedOperationException e) { + Slog.e(TAG, "The current device is not supported"); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get auto audio quality", e); + } return false; } @@ -846,7 +968,7 @@ public class MediaQualityService extends SystemService { } } - private class MediaQualityManagerCallbackList extends + private class MediaQualityManagerPictureProfileCallbackList extends RemoteCallbackList<IPictureProfileCallback> { @Override public void onCallbackDied(IPictureProfileCallback callback) { @@ -854,13 +976,27 @@ public class MediaQualityService extends SystemService { } } + private class MediaQualityManagerSoundProfileCallbackList extends + RemoteCallbackList<ISoundProfileCallback> { + @Override + public void onCallbackDied(ISoundProfileCallback callback) { + //todo + } + } + private final class UserState { // A list of callbacks. - private final MediaQualityManagerCallbackList mCallbacks = - new MediaQualityManagerCallbackList(); + private final MediaQualityManagerPictureProfileCallbackList mPictureProfileCallbacks = + new MediaQualityManagerPictureProfileCallbackList(); + + private final MediaQualityManagerSoundProfileCallbackList mSoundProfileCallbacks = + new MediaQualityManagerSoundProfileCallbackList(); + + private final Map<IPictureProfileCallback, Pair<Integer, Integer>> + mPictureProfileCallbackPidUidMap = new HashMap<>(); - private final Map<IPictureProfileCallback, Pair<Integer, Integer>> mCallbackPidUidMap = - new HashMap<>(); + private final Map<ISoundProfileCallback, Pair<Integer, Integer>> + mSoundProfileCallbackPidUidMap = new HashMap<>(); private UserState(Context context, int userId) { diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 4b41696a4390..e47f8ae9d3a5 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -583,6 +583,15 @@ public class GroupHelper { final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey( record.getUserId(), pkgName, sectioner); + // The notification was part of a different section => trigger regrouping + final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); + if (prevSectionKey != null && !fullAggregateGroupKey.equals(prevSectionKey)) { + if (DEBUG) { + Slog.i(TAG, "Section changed for: " + record); + } + maybeUngroupOnSectionChanged(record, prevSectionKey); + } + // This notification is already aggregated if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) { return false; @@ -652,10 +661,33 @@ public class GroupHelper { } /** + * A notification was added that was previously part of a different section and needs to trigger + * GH state cleanup. + */ + private void maybeUngroupOnSectionChanged(NotificationRecord record, + FullyQualifiedGroupKey prevSectionKey) { + maybeUngroupWithSections(record, prevSectionKey); + if (record.getGroupKey().equals(prevSectionKey.toString())) { + record.setOverrideGroupKey(null); + } + } + + /** * A notification was added that is app-grouped. */ private void maybeUngroupOnAppGrouped(NotificationRecord record) { - maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record)); + FullyQualifiedGroupKey currentSectionKey = getSectionGroupKeyWithFallback(record); + + // The notification was part of a different section => trigger regrouping + final FullyQualifiedGroupKey prevSectionKey = getPreviousValidSectionKey(record); + if (prevSectionKey != null && !prevSectionKey.equals(currentSectionKey)) { + if (DEBUG) { + Slog.i(TAG, "Section changed for: " + record); + } + currentSectionKey = prevSectionKey; + } + + maybeUngroupWithSections(record, currentSectionKey); } /** diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index 7cbbe2938fd5..5a425057ea89 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -107,4 +107,9 @@ public interface NotificationDelegate { * @param key the notification key */ void unbundleNotification(String key); + /** + * Called when the notification should be rebundled. + * @param key the notification key + */ + void rebundleNotification(String key); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9567c818fa18..05aa4134cbd5 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -158,6 +158,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.contentprotection.flags.Flags.rapidClearNotificationsByListenerAppOpEnabled; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; +import static com.android.internal.util.FrameworkStatsLog.NOTIFICATION_BUNDLE_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES; @@ -362,6 +363,7 @@ import com.android.server.lights.LightsManager; import com.android.server.notification.GroupHelper.NotificationAttributes; import com.android.server.notification.ManagedServices.ManagedServiceInfo; import com.android.server.notification.ManagedServices.UserProfiles; +import com.android.server.notification.NotificationRecordLogger.NotificationPullStatsEvent; import com.android.server.notification.NotificationRecordLogger.NotificationReportedEvent; import com.android.server.notification.toast.CustomToastRecord; import com.android.server.notification.toast.TextToastRecord; @@ -1888,6 +1890,36 @@ public class NotificationManagerService extends SystemService { } } } + + @Override + public void rebundleNotification(String key) { + if (!(notificationClassification() && notificationRegroupOnClassification())) { + return; + } + synchronized (mNotificationLock) { + NotificationRecord r = mNotificationsByKey.get(key); + if (r == null) { + return; + } + + if (DBG) { + Slog.v(TAG, "rebundleNotification: " + r); + } + + if (r.getBundleType() != Adjustment.TYPE_OTHER) { + final Bundle classifBundle = new Bundle(); + classifBundle.putInt(KEY_TYPE, r.getBundleType()); + Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(), + classifBundle, "rebundle", r.getUserId()); + applyAdjustmentLocked(r, adj, /* isPosted= */ true); + mRankingHandler.requestSort(); + } else { + if (DBG) { + Slog.w(TAG, "Can't rebundle. No valid bundle type for: " + r); + } + } + } + } }; NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() { @@ -2826,6 +2858,7 @@ public class NotificationManagerService extends SystemService { mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_PREFERENCES); mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES); mStatsManager.clearPullAtomCallback(PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES); + mStatsManager.clearPullAtomCallback(NOTIFICATION_BUNDLE_PREFERENCES); mStatsManager.clearPullAtomCallback(DND_MODE_RULE); } if (mAppOps != null) { @@ -2930,6 +2963,12 @@ public class NotificationManagerService extends SystemService { ConcurrentUtils.DIRECT_EXECUTOR, mPullAtomCallback ); + mStatsManager.setPullAtomCallback( + NOTIFICATION_BUNDLE_PREFERENCES, + null, // use default PullAtomMetadata values + ConcurrentUtils.DIRECT_EXECUTOR, + mPullAtomCallback + ); } private class StatsPullAtomCallbackImpl implements StatsManager.StatsPullAtomCallback { @@ -2939,6 +2978,7 @@ public class NotificationManagerService extends SystemService { case PACKAGE_NOTIFICATION_PREFERENCES: case PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES: case PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES: + case NOTIFICATION_BUNDLE_PREFERENCES: case DND_MODE_RULE: return pullNotificationStates(atomTag, data); default: @@ -2950,8 +2990,15 @@ public class NotificationManagerService extends SystemService { private int pullNotificationStates(int atomTag, List<StatsEvent> data) { switch(atomTag) { case PACKAGE_NOTIFICATION_PREFERENCES: - mPreferencesHelper.pullPackagePreferencesStats(data, - getAllUsersNotificationPermissions()); + if (notificationClassificationUi()) { + Set<String> pkgs = mAssistants.getPackagesWithKeyTypeAdjustmentSettings(); + mPreferencesHelper.pullPackagePreferencesStats(data, + getAllUsersNotificationPermissions(), + getPackageSpecificAdjustmentKeyTypes(pkgs)); + } else { + mPreferencesHelper.pullPackagePreferencesStats(data, + getAllUsersNotificationPermissions()); + } break; case PACKAGE_NOTIFICATION_CHANNEL_PREFERENCES: mPreferencesHelper.pullPackageChannelPreferencesStats(data); @@ -2959,6 +3006,11 @@ public class NotificationManagerService extends SystemService { case PACKAGE_NOTIFICATION_CHANNEL_GROUP_PREFERENCES: mPreferencesHelper.pullPackageChannelGroupPreferencesStats(data); break; + case NOTIFICATION_BUNDLE_PREFERENCES: + if (notificationClassification() && notificationClassificationUi()) { + mAssistants.pullBundlePreferencesStats(data); + } + break; case DND_MODE_RULE: mZenModeHelper.pullRules(data); break; @@ -3162,6 +3214,7 @@ public class NotificationManagerService extends SystemService { mAssistants.onBootPhaseAppsCanStart(); mConditionProviders.onBootPhaseAppsCanStart(); mHistoryManager.onBootPhaseAppsCanStart(); + mPreferencesHelper.onBootPhaseAppsCanStart(); migrateDefaultNAS(); maybeShowInitialReviewPermissionsNotification(); @@ -7133,6 +7186,7 @@ public class NotificationManagerService extends SystemService { adjustments.putParcelable(KEY_TYPE, newChannel); logClassificationChannelAdjustmentReceived(r, isPosted, classification); + r.setBundleType(classification); } } r.addAdjustment(adjustment); @@ -7449,6 +7503,24 @@ public class NotificationManagerService extends SystemService { return allPermissions; } + @VisibleForTesting + @FlaggedApi(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) + protected @NonNull Map<String, Set<Integer>> getPackageSpecificAdjustmentKeyTypes( + Set<String> pkgs) { + ArrayMap<String, Set<Integer>> pkgToAllowedTypes = new ArrayMap<>(); + for (String pkg : pkgs) { + int[] allowedTypesArray = mAssistants.getAllowedAdjustmentKeyTypesForPackage(pkg); + if (allowedTypesArray != null) { + Set<Integer> allowedTypes = new ArraySet<Integer>(); + for (int i : allowedTypesArray) { + allowedTypes.add(i); + } + pkgToAllowedTypes.append(pkg, allowedTypes); + } + } + return pkgToAllowedTypes; + } + private void dumpJson(PrintWriter pw, @NonNull DumpFilter filter, ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) { JSONObject dump = new JSONObject(); @@ -9536,7 +9608,8 @@ public class NotificationManagerService extends SystemService { || !Objects.equals(oldSbn.getNotification().getGroup(), n.getNotification().getGroup()) || oldSbn.getNotification().flags - != n.getNotification().flags) { + != n.getNotification().flags + || !old.getChannel().getId().equals(r.getChannel().getId())) { synchronized (mNotificationLock) { final String autogroupName = notificationForceGrouping() ? @@ -12023,6 +12096,22 @@ public class NotificationManagerService extends SystemService { } @FlaggedApi(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) + protected @NonNull Set<String> getPackagesWithKeyTypeAdjustmentSettings() { + if (notificationClassificationUi()) { + Set<String> packagesWithModifications = new ArraySet<String>(); + synchronized (mLock) { + for (String pkg : mClassificationTypePackagesEnabledTypes.keySet()) { + if (mClassificationTypePackagesEnabledTypes.get(pkg) != null) { + packagesWithModifications.add(pkg); + } + } + } + return packagesWithModifications; + } + return new ArraySet<String>(); + } + + @FlaggedApi(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) protected @NonNull int[] getAllowedAdjustmentKeyTypesForPackage(String pkg) { synchronized (mLock) { if (notificationClassificationUi()) { @@ -12623,6 +12712,32 @@ public class NotificationManagerService extends SystemService { Slog.e(TAG, "unable to notify assistant (capabilities): " + info, ex); } } + + /** + * Fills out {@link BundlePreferences} proto and wraps it in a {@link StatsEvent}. + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) + protected void pullBundlePreferencesStats(List<StatsEvent> events) { + boolean bundlesAllowed = true; + synchronized (mLock) { + List<String> unsupportedAdjustments = new ArrayList( + mNasUnsupported.getOrDefault( + UserHandle.getUserId(Binder.getCallingUid()), + new HashSet<>()) + ); + bundlesAllowed = !unsupportedAdjustments.contains(Adjustment.KEY_TYPE); + } + + int[] allowedBundleTypes = getAllowedAdjustmentKeyTypes(); + + events.add(FrameworkStatsLog.buildStatsEvent( + NOTIFICATION_BUNDLE_PREFERENCES, + /* optional int32 event_id = 1 */ + NotificationPullStatsEvent.NOTIFICATION_BUNDLE_PREFERENCES_PULLED.getId(), + /* optional bool bundles_allowed = 2 */ bundlesAllowed, + /* repeated android.stats.notification.BundleTypes allowed_bundle_types = 3 */ + allowedBundleTypes)); + } } /** diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 93f512bc7e17..81af0d8a6d80 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -36,10 +36,7 @@ import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.Person; -import android.content.ContentProvider; -import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ShortcutInfo; @@ -48,7 +45,6 @@ import android.media.AudioAttributes; import android.media.AudioSystem; import android.metrics.LogMaker; import android.net.Uri; -import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -226,6 +222,9 @@ public final class NotificationRecord { // lifetime extended. private boolean mCanceledAfterLifetimeExtension = false; + // type of the bundle if the notification was classified + private @Adjustment.Types int mBundleType = Adjustment.TYPE_OTHER; + public NotificationRecord(Context context, StatusBarNotification sbn, NotificationChannel channel) { this.sbn = sbn; @@ -471,6 +470,10 @@ public final class NotificationRecord { } } + if (android.service.notification.Flags.notificationClassification()) { + mBundleType = previous.mBundleType; + } + // Don't copy importance information or mGlobalSortKey, recompute them. } @@ -1493,23 +1496,14 @@ public final class NotificationRecord { final Notification notification = getNotification(); notification.visitUris((uri) -> { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(uri, false, false); - } else { - oldVisitGrantableUri(uri, false, false); - } + visitGrantableUri(uri, false, false); }); if (notification.getChannelId() != null) { NotificationChannel channel = getChannel(); if (channel != null) { - if (com.android.server.notification.Flags.notificationVerifyChannelSoundUri()) { - visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } else { - oldVisitGrantableUri(channel.getSound(), (channel.getUserLockedFields() - & NotificationChannel.USER_LOCKED_SOUND) != 0, true); - } + visitGrantableUri(channel.getSound(), (channel.getUserLockedFields() + & NotificationChannel.USER_LOCKED_SOUND) != 0, true); } } } finally { @@ -1525,53 +1519,6 @@ public final class NotificationRecord { * {@link #mGrantableUris}. Otherwise, this will either log or throw * {@link SecurityException} depending on target SDK of enqueuing app. */ - private void oldVisitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { - if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; - - if (mGrantableUris != null && mGrantableUris.contains(uri)) { - return; // already verified this URI - } - - final int sourceUid = getSbn().getUid(); - final long ident = Binder.clearCallingIdentity(); - try { - // This will throw a SecurityException if the caller can't grant. - mUgmInternal.checkGrantUriPermission(sourceUid, null, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); - - if (mGrantableUris == null) { - mGrantableUris = new ArraySet<>(); - } - mGrantableUris.add(uri); - } catch (SecurityException e) { - if (!userOverriddenUri) { - if (isSound) { - mSound = Settings.System.DEFAULT_NOTIFICATION_URI; - Log.w(TAG, "Replacing " + uri + " from " + sourceUid + ": " + e.getMessage()); - } else { - if (mTargetSdkVersion >= Build.VERSION_CODES.P) { - throw e; - } else { - Log.w(TAG, - "Ignoring " + uri + " from " + sourceUid + ": " + e.getMessage()); - } - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - } - - /** - * Note the presence of a {@link Uri} that should have permission granted to - * whoever will be rendering it. - * <p> - * If the enqueuing app has the ability to grant access, it will be added to - * {@link #mGrantableUris}. Otherwise, this will either log or throw - * {@link SecurityException} depending on target SDK of enqueuing app. - */ private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) { if (mGrantableUris != null && mGrantableUris.contains(uri)) { @@ -1689,6 +1636,14 @@ public final class NotificationRecord { mCanceledAfterLifetimeExtension = canceledAfterLifetimeExtension; } + public @Adjustment.Types int getBundleType() { + return mBundleType; + } + + public void setBundleType(@Adjustment.Types int bundleType) { + mBundleType = bundleType; + } + /** * Whether this notification is a conversation notification. */ diff --git a/services/core/java/com/android/server/notification/NotificationRecordLogger.java b/services/core/java/com/android/server/notification/NotificationRecordLogger.java index 3943aa583fee..6c0035b82a86 100644 --- a/services/core/java/com/android/server/notification/NotificationRecordLogger.java +++ b/services/core/java/com/android/server/notification/NotificationRecordLogger.java @@ -32,8 +32,6 @@ import android.service.notification.NotificationListenerService; import android.service.notification.NotificationStats; import android.util.Log; -import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; -import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags; import com.android.internal.logging.InstanceId; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; @@ -368,6 +366,19 @@ interface NotificationRecordLogger { } } + enum NotificationPullStatsEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Notification Bundle Preferences pulled.") + NOTIFICATION_BUNDLE_PREFERENCES_PULLED(2072); + + private final int mId; + NotificationPullStatsEvent(int id) { + mId = id; + } + @Override public int getId() { + return mId; + } + } + /** * A helper for extracting logging information from one or two NotificationRecords. */ diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 15377d6b269a..45b155049c72 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -16,6 +16,7 @@ package com.android.server.notification; +import static android.app.Flags.notificationClassificationUi; import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; import static android.app.NotificationChannel.DEFAULT_CHANNEL_ID; import static android.app.NotificationChannel.NEWS_ID; @@ -82,7 +83,6 @@ import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; -import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseBooleanArray; @@ -272,6 +272,15 @@ public class PreferencesHelper implements RankingConfig { updateMediaNotificationFilteringEnabled(); } + void onBootPhaseAppsCanStart() { + // IpcDataCaches must be invalidated once data becomes available, as queries will only + // begin to be cached after the first invalidation signal. At this point, we know about all + // notification channels. + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } + } + public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId) throws XmlPullParserException, IOException { int type = parser.getEventType(); @@ -531,12 +540,14 @@ public class PreferencesHelper implements RankingConfig { private PackagePreferences getOrCreatePackagePreferencesLocked(String pkg, @UserIdInt int userId, int uid, int importance, int priority, int visibility, boolean showBadge, int bubblePreference, long creationTime) { + boolean created = false; final String key = packagePreferencesKey(pkg, uid); PackagePreferences r = (uid == UNKNOWN_UID) ? mRestoredWithoutUids.get(unrestoredPackageKey(pkg, userId)) : mPackagePreferences.get(key); if (r == null) { + created = true; r = new PackagePreferences(); r.pkg = pkg; r.uid = uid; @@ -572,6 +583,9 @@ public class PreferencesHelper implements RankingConfig { mRestoredWithoutUids.remove(unrestoredPackageKey(pkg, userId)); } } + if (android.app.Flags.nmBinderPerfCacheChannels() && created) { + invalidateNotificationChannelCache(); + } return r; } @@ -664,6 +678,9 @@ public class PreferencesHelper implements RankingConfig { } NotificationChannel channel = new NotificationChannel(channelId, label, IMPORTANCE_LOW); p.channels.put(channelId, channel); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } return channel; } @@ -1171,9 +1188,7 @@ public class PreferencesHelper implements RankingConfig { // Verify that the app has permission to read the sound Uri // Only check for new channels, as regular apps can only set sound // before creating. See: {@link NotificationChannel#setSound} - if (Flags.notificationVerifyChannelSoundUri()) { - PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); - } + PermissionHelper.grantUriPermission(mUgmInternal, channel.getSound(), uid); channel.setImportanceLockedByCriticalDeviceFunction( r.defaultAppLockedImportance || r.fixedImportance); @@ -1208,6 +1223,10 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && needsPolicyFileChange) { + invalidateNotificationChannelCache(); + } + return needsPolicyFileChange; } @@ -1229,6 +1248,9 @@ public class PreferencesHelper implements RankingConfig { } channel.unlockFields(USER_LOCKED_IMPORTANCE); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @@ -1301,6 +1323,9 @@ public class PreferencesHelper implements RankingConfig { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } if (changed) { + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } updateConfig(); } } @@ -1537,6 +1562,10 @@ public class PreferencesHelper implements RankingConfig { if (channelBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannel) { + invalidateNotificationChannelCache(); + } return deletedChannel; } @@ -1566,6 +1595,9 @@ public class PreferencesHelper implements RankingConfig { } r.channels.remove(channelId); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } @Override @@ -1576,13 +1608,18 @@ public class PreferencesHelper implements RankingConfig { if (r == null) { return; } + boolean deleted = false; int N = r.channels.size() - 1; for (int i = N; i >= 0; i--) { String key = r.channels.keyAt(i); if (!DEFAULT_CHANNEL_ID.equals(key)) { r.channels.remove(key); + deleted = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && deleted) { + invalidateNotificationChannelCache(); + } } } @@ -1613,6 +1650,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void updateDefaultApps(int userId, ArraySet<String> toRemove, @@ -1642,6 +1682,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public NotificationChannelGroup getNotificationChannelGroupWithChannels(String pkg, @@ -1757,6 +1800,9 @@ public class PreferencesHelper implements RankingConfig { if (groupBypassedDnd) { updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); } + if (android.app.Flags.nmBinderPerfCacheChannels() && deletedChannels.size() > 0) { + invalidateNotificationChannelCache(); + } return deletedChannels; } @@ -1902,8 +1948,13 @@ public class PreferencesHelper implements RankingConfig { } } } - if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) { - updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + if (!deletedChannelIds.isEmpty()) { + if (mCurrentUserHasChannelsBypassingDnd) { + updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi); + } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return deletedChannelIds; } @@ -2196,6 +2247,11 @@ public class PreferencesHelper implements RankingConfig { PackagePreferences prefs = getOrCreatePackagePreferencesLocked(sourcePkg, sourceUid); prefs.delegate = new Delegate(delegatePkg, delegateUid, true); } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + // If package delegates change, then which packages can get what channel information + // also changes, so we need to clear the cache. + invalidateNotificationChannelCache(); + } } /** @@ -2208,6 +2264,9 @@ public class PreferencesHelper implements RankingConfig { prefs.delegate.mEnabled = false; } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } /** @@ -2465,6 +2524,25 @@ public class PreferencesHelper implements RankingConfig { */ public void pullPackagePreferencesStats(List<StatsEvent> events, ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions) { + pullPackagePreferencesStats(events, pkgPermissions, new ArrayMap<String, Set<Integer>>()); + } + + + /** + * Fills out {@link PackageNotificationPreferences} proto and wraps it in a {@link StatsEvent}. + * @param events Newly filled out StatsEvent protos are added to this list as output. + * @param pkgPermissions Maps from a pair representing a uid and package to a pair of booleans, + * where the first represents whether the notification permission was + * granted to that package, and the second represents whether the + * permission was user-set. + * @param pkgAdjustmentKeyTypes A map of package names that are not allowed to have their + * notifications classified into differently typed notification + * channels, and the channels that they're allowed to be + * classified into. + */ + public void pullPackagePreferencesStats(List<StatsEvent> events, + ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions, + @NonNull Map<String, Set<Integer>> pkgAdjustmentKeyTypes) { Set<Pair<Integer, String>> pkgsWithPermissionsToHandle = null; if (pkgPermissions != null) { pkgsWithPermissionsToHandle = pkgPermissions.keySet(); @@ -2510,6 +2588,14 @@ public class PreferencesHelper implements RankingConfig { isFsiPermissionUserSet(r.pkg, r.uid, fsiState, currentPermissionFlags); + if (!notificationClassificationUi() + && pkgAdjustmentKeyTypes.keySet().size() > 0) { + Slog.w(TAG, "Pkg adjustment types improperly allowed without flag set"); + } + + int[] allowedBundleTypes = + getAllowedTypesForPackage(pkgAdjustmentKeyTypes, r.pkg); + events.add(FrameworkStatsLog.buildStatsEvent( PACKAGE_NOTIFICATION_PREFERENCES, /* optional int32 uid = 1 [(is_uid) = true] */ r.uid, @@ -2518,7 +2604,9 @@ public class PreferencesHelper implements RankingConfig { /* optional int32 user_locked_fields = 4 */ r.lockedAppFields, /* optional bool user_set_importance = 5 */ importanceIsUserSet, /* optional FsiState fsi_state = 6 */ fsiState, - /* optional bool is_fsi_permission_user_set = 7 */ fsiIsUserSet)); + /* optional bool is_fsi_permission_user_set = 7 */ fsiIsUserSet, + /* repeated int32 allowed_bundle_types = 8 */ allowedBundleTypes + )); } } @@ -2529,6 +2617,10 @@ public class PreferencesHelper implements RankingConfig { break; } pulledEvents++; + + int[] allowedBundleTypes = + getAllowedTypesForPackage(pkgAdjustmentKeyTypes, p.second); + // Because all fields are required in FrameworkStatsLog.buildStatsEvent, we have // to fill in default values for all the unspecified fields. events.add(FrameworkStatsLog.buildStatsEvent( @@ -2540,11 +2632,31 @@ public class PreferencesHelper implements RankingConfig { /* optional int32 user_locked_fields = 4 */ DEFAULT_LOCKED_APP_FIELDS, /* optional bool user_set_importance = 5 */ pkgPermissions.get(p).second, /* optional FsiState fsi_state = 6 */ 0, - /* optional bool is_fsi_permission_user_set = 7 */ false)); + /* optional bool is_fsi_permission_user_set = 7 */ false, + /* repeated BundleTypes allowed_bundle_types = 8 */ allowedBundleTypes)); } } } + private int[] getAllowedTypesForPackage(@NonNull + Map<String, Set<Integer>> pkgAdjustmentKeyTypes, + String pkg) { + int[] allowedBundleTypes = new int[]{}; + if (notificationClassificationUi()) { + if (pkgAdjustmentKeyTypes.containsKey(pkg)) { + // Convert from set to int[] + Set<Integer> types = pkgAdjustmentKeyTypes.get(pkg); + allowedBundleTypes = new int[types.size()]; + int i = 0; + for (int val : types) { + allowedBundleTypes[i] = val; + i++; + } + } + } + return allowedBundleTypes; + } + /** * Fills out {@link PackageNotificationChannelPreferences} proto and wraps it in a * {@link StatsEvent}. @@ -2811,18 +2923,24 @@ public class PreferencesHelper implements RankingConfig { public void onUserRemoved(int userId) { synchronized (mLock) { + boolean removed = false; int N = mPackagePreferences.size(); for (int i = N - 1; i >= 0; i--) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); if (UserHandle.getUserId(PackagePreferences.uid) == userId) { mPackagePreferences.removeAt(i); + removed = true; } } + if (android.app.Flags.nmBinderPerfCacheChannels() && removed) { + invalidateNotificationChannelCache(); + } } } protected void onLocaleChanged(Context context, int userId) { synchronized (mLock) { + boolean updated = false; int N = mPackagePreferences.size(); for (int i = 0; i < N; i++) { PackagePreferences PackagePreferences = mPackagePreferences.valueAt(i); @@ -2833,10 +2951,14 @@ public class PreferencesHelper implements RankingConfig { DEFAULT_CHANNEL_ID).setName( context.getResources().getString( R.string.default_notification_channel_label)); + updated = true; } // TODO (b/346396459): Localize all reserved channels } } + if (android.app.Flags.nmBinderPerfCacheChannels() && updated) { + invalidateNotificationChannelCache(); + } } } @@ -2884,7 +3006,7 @@ public class PreferencesHelper implements RankingConfig { channel.getAudioAttributes().getUsage()); if (Settings.System.DEFAULT_NOTIFICATION_URI.equals( restoredUri)) { - Log.w(TAG, + Slog.w(TAG, "Could not restore sound: " + uri + " for channel: " + channel); } @@ -2922,6 +3044,9 @@ public class PreferencesHelper implements RankingConfig { if (updated) { updateConfig(); + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } return updated; } @@ -2939,6 +3064,9 @@ public class PreferencesHelper implements RankingConfig { p.priority = DEFAULT_PRIORITY; p.visibility = DEFAULT_VISIBILITY; p.showBadge = DEFAULT_SHOW_BADGE; + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } } } @@ -3123,6 +3251,9 @@ public class PreferencesHelper implements RankingConfig { } } } + if (android.app.Flags.nmBinderPerfCacheChannels()) { + invalidateNotificationChannelCache(); + } } public void migrateNotificationPermissions(List<UserInfo> users) { @@ -3154,6 +3285,12 @@ public class PreferencesHelper implements RankingConfig { mRankingHandler.requestSort(); } + @VisibleForTesting + // Utility method for overriding in tests to confirm that the cache gets cleared. + protected void invalidateNotificationChannelCache() { + NotificationManager.invalidateNotificationChannelCache(); + } + private static String packagePreferencesKey(String pkg, int uid) { return pkg + "|" + uid; } diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index 2b4d71e85dc0..c1ca9c23aef5 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -172,16 +172,6 @@ flag { } flag { - name: "notification_verify_channel_sound_uri" - namespace: "systemui" - description: "Verify Uri permission for sound when creating a notification channel" - bug: "337775777" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notification_vibration_in_sound_uri_for_channel" namespace: "systemui" description: "Enables sound uri with vibration source in notification channel" diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4c70d2347fb7..4cca85590967 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -125,7 +125,6 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.VerifierInfo; -import android.content.pm.dex.DexMetadataHelper; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.net.Uri; @@ -171,7 +170,6 @@ import com.android.internal.pm.pkg.component.ParsedIntentInfo; import com.android.internal.pm.pkg.component.ParsedPermission; import com.android.internal.pm.pkg.component.ParsedPermissionGroup; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; -import com.android.internal.security.VerityUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.server.EventLogTags; @@ -186,7 +184,6 @@ import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.SharedLibraryWrapper; import com.android.server.rollback.RollbackManagerInternal; -import com.android.server.security.FileIntegrityService; import com.android.server.utils.WatchedArrayMap; import com.android.server.utils.WatchedLongSparseArray; @@ -195,7 +192,6 @@ import dalvik.system.VMRuntime; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.security.DigestException; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -853,7 +849,9 @@ final class InstallPackageHelper { int token; if (mPm.mNextInstallToken < 0) mPm.mNextInstallToken = 1; token = mPm.mNextInstallToken++; - mPm.mRunningInstalls.put(token, request); + synchronized (mPm.mRunningInstalls) { + mPm.mRunningInstalls.put(token, request); + } if (DEBUG_INSTALL) Log.v(TAG, "+ starting restore round-trip " + token); @@ -1165,11 +1163,8 @@ final class InstallPackageHelper { } try { doRenameLI(request, parsedPackage); - setUpFsVerity(parsedPackage); - } catch (Installer.InstallerException | IOException | DigestException - | NoSuchAlgorithmException | PrepareFailure e) { - request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP, - "Failed to set up verity: " + e); + } catch (PrepareFailure e) { + request.setError(e); return false; } @@ -2322,68 +2317,6 @@ final class InstallPackageHelper { } } - /** - * Set up fs-verity for the given package. For older devices that do not support fs-verity, - * this is a no-op. - */ - private void setUpFsVerity(AndroidPackage pkg) throws Installer.InstallerException, - PrepareFailure, IOException, DigestException, NoSuchAlgorithmException { - if (!PackageManagerServiceUtils.isApkVerityEnabled()) { - return; - } - - if (isIncrementalPath(pkg.getPath()) && IncrementalManager.getVersion() - < IncrementalManager.MIN_VERSION_TO_SUPPORT_FSVERITY) { - return; - } - - // Collect files we care for fs-verity setup. - ArrayMap<String, String> fsverityCandidates = new ArrayMap<>(); - fsverityCandidates.put(pkg.getBaseApkPath(), - VerityUtils.getFsveritySignatureFilePath(pkg.getBaseApkPath())); - - final String dmPath = DexMetadataHelper.buildDexMetadataPathForApk( - pkg.getBaseApkPath()); - if (new File(dmPath).exists()) { - fsverityCandidates.put(dmPath, VerityUtils.getFsveritySignatureFilePath(dmPath)); - } - - for (String path : pkg.getSplitCodePaths()) { - fsverityCandidates.put(path, VerityUtils.getFsveritySignatureFilePath(path)); - - final String splitDmPath = DexMetadataHelper.buildDexMetadataPathForApk(path); - if (new File(splitDmPath).exists()) { - fsverityCandidates.put(splitDmPath, - VerityUtils.getFsveritySignatureFilePath(splitDmPath)); - } - } - - var fis = FileIntegrityService.getService(); - for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) { - try { - final String filePath = entry.getKey(); - if (VerityUtils.hasFsverity(filePath)) { - continue; - } - - final String signaturePath = entry.getValue(); - if (new File(signaturePath).exists()) { - // If signature is provided, enable fs-verity first so that the file can be - // measured for signature check below. - VerityUtils.setUpFsverity(filePath); - - if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "fs-verity signature does not verify against a known key"); - } - } - } catch (IOException e) { - throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, - "Failed to enable fs-verity: " + e); - } - } - } - private PackageFreezer freezePackageForInstall(String packageName, int userId, int installFlags, String killReason, int exitInfoReason, InstallRequest request) { if ((installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { @@ -3060,6 +2993,8 @@ final class InstallPackageHelper { } if (succeeded) { + Slog.i(TAG, "installation completed:" + packageName); + if (Flags.aslInApkAppMetadataSource() && pkgSetting.getAppMetadataSource() == APP_METADATA_SOURCE_APK) { if (!extractAppMetadataFromApk(request.getPkg(), @@ -3104,13 +3039,14 @@ final class InstallPackageHelper { if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled() && android.security.Flags.extendEcmToAllSettings()) { final int appId = request.getAppId(); - mPm.mHandler.post(() -> { + // TODO: b/388960315 - Implement a long-term solution to race condition + mPm.mHandler.postDelayed(() -> { for (int userId : firstUserIds) { // MODE_DEFAULT means that the app's guardedness will be decided lazily setAccessRestrictedSettingsMode(packageName, appId, userId, AppOpsManager.MODE_DEFAULT); } - }); + }, 1000L); } else { // Apply restricted settings on potentially dangerous packages. Needs to happen // after appOpsManager is notified of the new package diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java index bc03b10b41b4..9916be680374 100644 --- a/services/core/java/com/android/server/pm/PackageHandler.java +++ b/services/core/java/com/android/server/pm/PackageHandler.java @@ -82,14 +82,20 @@ final class PackageHandler extends Handler { case POST_INSTALL: { if (DEBUG_INSTALL) Log.v(TAG, "Handling post-install for " + msg.arg1); - InstallRequest request = mPm.mRunningInstalls.get(msg.arg1); - final boolean didRestore = (msg.arg2 != 0); - mPm.mRunningInstalls.delete(msg.arg1); + final InstallRequest request; + final int token; + final boolean didRestore; + synchronized (mPm.mRunningInstalls) { + request= mPm.mRunningInstalls.get(msg.arg1); + token = msg.arg1; + didRestore = (msg.arg2 != 0); + mPm.mRunningInstalls.delete(token); + } if (request == null) { if (DEBUG_INSTALL) { Slog.i(TAG, "InstallRequest is null. Nothing to do for post-install " - + "token " + msg.arg1); + + "token " + token); } break; } @@ -100,10 +106,10 @@ final class PackageHandler extends Handler { mPm.handlePackagePostInstall(request, didRestore); } else if (DEBUG_INSTALL) { // No post-install when we run restore from installExistingPackageForUser - Slog.i(TAG, "Nothing to do for post-install token " + msg.arg1); + Slog.i(TAG, "Nothing to do for post-install token " + token); } - Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "postInstall", msg.arg1); + Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "postInstall", token); } break; case DEFERRED_NO_KILL_POST_DELETE: { CleanUpArgs args = (CleanUpArgs) msg.obj; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index c6760431116e..1b41c3617a05 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -26,7 +26,6 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; -import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; @@ -824,8 +823,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private File mInheritedFilesBase; - @GuardedBy("mLock") - private boolean mVerityFoundForApks; /** * Both flags should be guarded with mLock whenever changes need to be in lockstep. @@ -864,7 +861,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } else { if (DexMetadataHelper.isDexMetadataFile(file)) return false; } - if (VerityUtils.isFsveritySignatureFile(file)) return false; if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false; return true; } @@ -3565,13 +3561,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "Missing existing base package"); } - // Default to require only if existing base apk has fs-verity signature. - mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled() - && params.mode == SessionParams.MODE_INHERIT_EXISTING - && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath()) - && (new File(VerityUtils.getFsveritySignatureFilePath( - pkgInfo.applicationInfo.getBaseCodePath()))).exists(); - final List<File> removedFiles = getRemovedFilesLocked(); final List<String> removeSplitList = new ArrayList<>(); if (!removedFiles.isEmpty()) { @@ -3972,24 +3961,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeStageFsveritySignatureLocked(File origFile, File targetFile, - boolean fsVerityRequired) throws PackageManagerException { - if (android.security.Flags.deprecateFsvSig()) { - return; - } - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (originalSignature.exists()) { - final File stagedSignature = new File( - VerityUtils.getFsveritySignatureFilePath(targetFile.getPath())); - stageFileLocked(originalSignature, stagedSignature); - } else if (fsVerityRequired) { - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Missing corresponding fs-verity signature to " + origFile); - } - } - - @GuardedBy("mLock") private void maybeStageV4SignatureLocked(File origFile, File targetFile) throws PackageManagerException { final File originalSignature = new File(origFile.getPath() + V4Signature.EXT); @@ -4015,11 +3986,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName())); stageFileLocked(dexMetadataFile, targetDexMetadataFile); - - // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on - // supported on older devices. - maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile, - DexMetadataHelper.isFsVerityRequired()); } @FlaggedApi(com.android.art.flags.Flags.FLAG_ART_SERVICE_V3) @@ -4105,44 +4071,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private boolean isFsVerityRequiredForApk(File origFile, File targetFile) - throws PackageManagerException { - if (mVerityFoundForApks) { - return true; - } - - // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one. - final File originalSignature = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (!originalSignature.exists()) { - return false; - } - mVerityFoundForApks = true; - - // When a signature is found, also check any previous staged APKs since they also need to - // have fs-verity signature consistently. - for (File file : mResolvedStagedFiles) { - if (!file.getName().endsWith(".apk")) { - continue; - } - // Ignore the current targeting file. - if (targetFile.getName().equals(file.getName())) { - continue; - } - throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, - "Previously staged apk is missing fs-verity signature"); - } - return true; - } - - @GuardedBy("mLock") private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName, List<String> artManagedFilePaths) throws PackageManagerException { stageFileLocked(origFile, targetFile); - // Stage APK's fs-verity signature if present. - maybeStageFsveritySignatureLocked(origFile, targetFile, - isFsVerityRequiredForApk(origFile, targetFile)); // Stage APK's v4 signature if present, and fs-verity is supported. if (android.security.Flags.extendVbChainToUpdatedApk() && VerityUtils.isFsVeritySupported()) { @@ -4160,16 +4092,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } @GuardedBy("mLock") - private void maybeInheritFsveritySignatureLocked(File origFile) { - // Inherit the fsverity signature file if present. - final File fsveritySignatureFile = new File( - VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); - if (fsveritySignatureFile.exists()) { - mResolvedInheritedFiles.add(fsveritySignatureFile); - } - } - - @GuardedBy("mLock") private void maybeInheritV4SignatureLocked(File origFile) { // Inherit the v4 signature file if present. final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT); @@ -4182,7 +4104,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void inheritFileLocked(File origFile, List<String> artManagedFilePaths) { mResolvedInheritedFiles.add(origFile); - maybeInheritFsveritySignatureLocked(origFile); if (android.security.Flags.extendVbChainToUpdatedApk()) { maybeInheritV4SignatureLocked(origFile); } @@ -4193,13 +4114,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { artManagedFilePaths, origFile.getPath())) { File artManagedFile = new File(path); mResolvedInheritedFiles.add(artManagedFile); - maybeInheritFsveritySignatureLocked(artManagedFile); } } else { final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile); if (dexMetadataFile != null) { mResolvedInheritedFiles.add(dexMetadataFile); - maybeInheritFsveritySignatureLocked(dexMetadataFile); } } // Inherit the digests if present. diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index aaa4fdf12411..f60e086e7c5d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -960,6 +960,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService // Stores a list of users whose package restrictions file needs to be updated final ArraySet<Integer> mDirtyUsers = new ArraySet<>(); + @GuardedBy("mRunningInstalls") final SparseArray<InstallRequest> mRunningInstalls = new SparseArray<>(); int mNextInstallToken = 1; // nonzero; will be wrapped back to 1 when ++ overflows @@ -3291,20 +3292,23 @@ public class PackageManagerService implements PackageSender, TestUtilityService // and been launched through some other means, so it is not in a problematic // state for observers to see the FIRST_LAUNCH signal. mHandler.post(() -> { - for (int i = 0; i < mRunningInstalls.size(); i++) { - final InstallRequest installRequest = mRunningInstalls.valueAt(i); - if (installRequest.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) { - continue; - } - if (packageName.equals(installRequest.getPkg().getPackageName())) { - // right package; but is it for the right user? - for (int uIndex = 0; uIndex < installRequest.getNewUsers().length; uIndex++) { - if (userId == installRequest.getNewUsers()[uIndex]) { - if (DEBUG_BACKUP) { - Slog.i(TAG, "Package " + packageName - + " being restored so deferring FIRST_LAUNCH"); + synchronized (mRunningInstalls) { + for (int i = 0; i < mRunningInstalls.size(); i++) { + final InstallRequest installRequest = mRunningInstalls.valueAt(i); + if (installRequest.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) { + continue; + } + final int[] newUsers = installRequest.getNewUsers(); + if (packageName.equals(installRequest.getPkg().getPackageName())) { + // right package; but is it for the right user? + for (int uIndex = 0; uIndex < newUsers.length; uIndex++) { + if (userId == newUsers[uIndex]) { + if (DEBUG_BACKUP) { + Slog.i(TAG, "Package " + packageName + + " being restored so deferring FIRST_LAUNCH"); + } + return; } - return; } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index 7af39f74d0d6..3e376b6958ec 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -520,22 +520,6 @@ public class PackageManagerServiceUtils { } } - /** Default is to not use fs-verity since it depends on kernel support. */ - private static final int FSVERITY_DISABLED = 0; - - /** Standard fs-verity. */ - private static final int FSVERITY_ENABLED = 2; - - /** Returns true if standard APK Verity is enabled. */ - static boolean isApkVerityEnabled() { - if (android.security.Flags.deprecateFsvSig()) { - return false; - } - return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED) - == FSVERITY_ENABLED; - } - /** * Verifies that signatures match. * @returns {@code true} if the compat signatures were matched; otherwise, {@code false}. diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index fb16b862b275..a902f5ff372f 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -1848,8 +1848,10 @@ public class PackageSetting extends SettingBase implements PackageStateInternal boolean manifestOverrideEnabled = (mPageSizeAppCompatFlags & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED) != 0; boolean settingsOverrideEnabled = (mPageSizeAppCompatFlags - & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED) != 0; - if (manifestOverrideEnabled || settingsOverrideEnabled) { + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED) != 0; + boolean settingsOverrideDisabled = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED) != 0; + if (manifestOverrideEnabled || settingsOverrideEnabled || settingsOverrideDisabled) { return null; } diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index 17d7a14d9129..e1b76222072e 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -612,7 +612,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable final PackageSetting staticLibPkgSetting = mPm.getPackageSettingForMutation(sharedLibraryInfo.getPackageName()); if (staticLibPkgSetting == null) { - Slog.wtf(TAG, "Shared lib without setting: " + sharedLibraryInfo); + Slog.w(TAG, "Shared lib without setting: " + sharedLibraryInfo); continue; } for (int u = 0; u < installedUserCount; u++) { diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 8249d65868cd..f4d4c5be035e 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -3312,13 +3312,18 @@ public class UserManagerService extends IUserManager.Stub { } } - - private void sendUserInfoChangedBroadcast(@UserIdInt int userId) { Intent changedIntent = new Intent(Intent.ACTION_USER_INFO_CHANGED); changedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); changedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); mContext.sendBroadcastAsUser(changedIntent, UserHandle.ALL); + + // This intent allow system UI apps to refresh the content even if process was freezed. + Intent bgIntent = new Intent(Intent.ACTION_USER_INFO_CHANGED_BACKGROUND); + bgIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId); + bgIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); + mContext.sendBroadcastAsUser(bgIntent, UserHandle.ALL, + Manifest.permission.MANAGE_USERS); } @Override @@ -6661,7 +6666,7 @@ public class UserManagerService extends IUserManager.Stub { + userId); } new Thread(() -> { - getActivityManagerInternal().onUserRemoved(userId); + getActivityManagerInternal().onUserRemoving(userId); removeUserState(userId); }).start(); } @@ -6701,6 +6706,7 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { removeUserDataLU(userId); mIsUserManaged.delete(userId); + getActivityManagerInternal().onUserRemoved(userId); } synchronized (mUserStates) { mUserStates.delete(userId); diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java index e75f852eb437..a755ee1cd0fe 100644 --- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java +++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java @@ -45,6 +45,7 @@ import com.android.server.devicestate.DeviceStateProvider; import com.android.server.input.InputManagerInternal; import com.android.server.policy.devicestate.config.Conditions; import com.android.server.policy.devicestate.config.DeviceStateConfig; +import com.android.server.policy.devicestate.config.Flags; import com.android.server.policy.devicestate.config.LidSwitchCondition; import com.android.server.policy.devicestate.config.NumericRange; import com.android.server.policy.devicestate.config.Properties; @@ -140,7 +141,16 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, private static final String PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT = "com.android.server.policy.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT"; - + // Deprecated flag definitions to maintain backwards compatibility. + private static final String FLAG_CANCEL_OVERRIDE_REQUESTS = "FLAG_CANCEL_OVERRIDE_REQUESTS"; + private static final String FLAG_APP_INACCESSIBLE = "FLAG_APP_INACCESSIBLE"; + private static final String FLAG_EMULATED_ONLY = "FLAG_EMULATED_ONLY"; + private static final String FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP = + "FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP"; + private static final String FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL = + "FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL"; + private static final String FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE = + "FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE"; /** Interface that allows reading the device state configuration. */ interface ReadableConfig { @@ -185,15 +195,29 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, new HashSet<>(); Set<@DeviceState.DeviceStateProperties Integer> physicalProperties = new HashSet<>(); - final Properties configFlags = stateConfig.getProperties(); - if (configFlags != null) { - List<String> configPropertyStrings = configFlags.getProperty(); + final Properties configProperties = stateConfig.getProperties(); + if (configProperties != null) { + List<String> configPropertyStrings = configProperties.getProperty(); for (int i = 0; i < configPropertyStrings.size(); i++) { final String configPropertyString = configPropertyStrings.get(i); addPropertyByString(configPropertyString, systemProperties, physicalProperties); } } + + if (android.hardware.devicestate.feature.flags + .Flags.deviceStateConfigurationFlag()) { + // Parse through the deprecated flag configuration to keep compatibility. + final Flags configFlags = stateConfig.getFlags(); + if (configFlags != null) { + List<String> configFlagStrings = configFlags.getFlag(); + for (int i = 0; i < configFlagStrings.size(); i++) { + final String configFlagString = configFlagStrings.get(i); + addFlagByString(configFlagString, systemProperties); + } + } + } + DeviceState.Configuration deviceStateConfiguration = new DeviceState.Configuration.Builder(state, name) .setSystemProperties(systemProperties) @@ -292,6 +316,34 @@ public final class DeviceStateProviderImpl implements DeviceStateProvider, } } + private static void addFlagByString(String flagString, + Set<@DeviceState.SystemDeviceStateProperties Integer> systemProperties) { + switch (flagString) { + case FLAG_APP_INACCESSIBLE: + systemProperties.add(DeviceState.PROPERTY_APP_INACCESSIBLE); + break; + case FLAG_EMULATED_ONLY: + systemProperties.add(DeviceState.PROPERTY_EMULATED_ONLY); + break; + case FLAG_CANCEL_OVERRIDE_REQUESTS: + systemProperties.add(DeviceState.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS); + break; + case FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP: + systemProperties.add(DeviceState.PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP); + break; + case FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE: + systemProperties.add(DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE); + break; + case FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL: + systemProperties.add( + DeviceState.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL); + break; + default: + Slog.w(TAG, "Parsed unknown flag with name: " + flagString); + break; + } + } + // Lock for internal state. private final Object mLock = new Object(); private final Context mContext; diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 23383a9c55c0..f9e4022f04a0 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -1395,7 +1395,9 @@ public final class PowerManagerService extends SystemService DisplayGroupPowerChangeListener displayGroupPowerChangeListener = new DisplayGroupPowerChangeListener(); mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener); - mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); + if (mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); + } if(mDreamManager != null){ // This DreamManager method does not acquire a lock, so it should be safe to call. @@ -3852,6 +3854,10 @@ public final class PowerManagerService extends SystemService @GuardedBy("mLock") private void notifyScreenTimeoutPolicyChangesLocked() { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + return; + } + for (int idx = 0; idx < mPowerGroups.size(); idx++) { final int powerGroupId = mPowerGroups.keyAt(idx); final PowerGroup powerGroup = mPowerGroups.valueAt(idx); @@ -6011,6 +6017,11 @@ public final class PowerManagerService extends SystemService @Override // Binder call public void addScreenTimeoutPolicyListener(int displayId, IScreenTimeoutPolicyListener listener) { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + throw new IllegalStateException("Screen timeout policy listener API flag " + + "is not enabled"); + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); @@ -6042,6 +6053,11 @@ public final class PowerManagerService extends SystemService @Override // Binder call public void removeScreenTimeoutPolicyListener(int displayId, IScreenTimeoutPolicyListener listener) { + if (!mFeatureFlags.isScreenTimeoutPolicyListenerApiEnabled()) { + throw new IllegalStateException("Screen timeout policy listener API flag " + + "is not enabled"); + } + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null); diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java index 42dbb7974fe2..f46fa446a0ba 100644 --- a/services/core/java/com/android/server/power/ThermalManagerService.java +++ b/services/core/java/com/android/server/power/ThermalManagerService.java @@ -155,6 +155,9 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting final TemperatureWatcher mTemperatureWatcher; + @VisibleForTesting + final AtomicBoolean mIsHalSkinForecastSupported = new AtomicBoolean(false); + private final ThermalHalWrapper.WrapperThermalChangedCallback mWrapperCallback = new ThermalHalWrapper.WrapperThermalChangedCallback() { @Override @@ -254,6 +257,18 @@ public class ThermalManagerService extends SystemService { } onTemperatureMapChangedLocked(); mTemperatureWatcher.getAndUpdateThresholds(); + // we only check forecast if a single SKIN sensor threshold is reported + synchronized (mTemperatureWatcher.mSamples) { + if (mTemperatureWatcher.mSevereThresholds.size() == 1) { + try { + mIsHalSkinForecastSupported.set( + Flags.allowThermalHalSkinForecast() + && !Float.isNaN(mHalWrapper.forecastSkinTemperature(10))); + } catch (UnsupportedOperationException e) { + Slog.i(TAG, "Thermal HAL does not support forecastSkinTemperature"); + } + } + } mHalReady.set(true); } } @@ -1092,6 +1107,8 @@ public class ThermalManagerService extends SystemService { protected abstract List<TemperatureThreshold> getTemperatureThresholds(boolean shouldFilter, int type); + protected abstract float forecastSkinTemperature(int forecastSeconds); + protected abstract boolean connectToHal(); protected abstract void dump(PrintWriter pw, String prefix); @@ -1124,8 +1141,16 @@ public class ThermalManagerService extends SystemService { @VisibleForTesting static class ThermalHalAidlWrapper extends ThermalHalWrapper implements IBinder.DeathRecipient { /* Proxy object for the Thermal HAL AIDL service. */ + + @GuardedBy("mHalLock") private IThermal mInstance = null; + private IThermal getHalInstance() { + synchronized (mHalLock) { + return mInstance; + } + } + /** Callback for Thermal HAL AIDL. */ private final IThermalChangedCallback mThermalCallbackAidl = new IThermalChangedCallback.Stub() { @@ -1169,154 +1194,183 @@ public class ThermalManagerService extends SystemService { @Override protected List<Temperature> getCurrentTemperatures(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<Temperature> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<Temperature> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.Temperature[] halRet = + shouldFilter ? instance.getTemperaturesWithType(type) + : instance.getTemperatures(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.Temperature[] halRet = - shouldFilter ? mInstance.getTemperaturesWithType(type) - : mInstance.getTemperatures(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.Temperature t : halRet) { + if (!Temperature.isValidStatus(t.throttlingStatus)) { + Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus + + " received from AIDL HAL"); + t.throttlingStatus = Temperature.THROTTLING_NONE; } - for (android.hardware.thermal.Temperature t : halRet) { - if (!Temperature.isValidStatus(t.throttlingStatus)) { - Slog.e(TAG, "Invalid temperature status " + t.throttlingStatus - + " received from AIDL HAL"); - t.throttlingStatus = Temperature.THROTTLING_NONE; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); - connectToHal(); + ret.add(new Temperature(t.value, t.type, t.name, t.throttlingStatus)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentTemperatures, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override protected List<CoolingDevice> getCurrentCoolingDevices(boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<CoolingDevice> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<CoolingDevice> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter + ? instance.getCoolingDevicesWithType(type) + : instance.getCoolingDevices(); + if (halRet == null) { return ret; } - try { - final android.hardware.thermal.CoolingDevice[] halRet = shouldFilter - ? mInstance.getCoolingDevicesWithType(type) - : mInstance.getCoolingDevices(); - if (halRet == null) { - return ret; + for (android.hardware.thermal.CoolingDevice t : halRet) { + if (!CoolingDevice.isValidType(t.type)) { + Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); + continue; } - for (android.hardware.thermal.CoolingDevice t : halRet) { - if (!CoolingDevice.isValidType(t.type)) { - Slog.e(TAG, "Invalid cooling device type " + t.type + " from AIDL HAL"); - continue; - } - if (shouldFilter && t.type != type) { - continue; - } - ret.add(new CoolingDevice(t.value, t.type, t.name)); + if (shouldFilter && t.type != type) { + continue; } - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); - connectToHal(); + ret.add(new CoolingDevice(t.value, t.type, t.name)); + } + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getCurrentCoolingDevices, reconnecting", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; } @Override @NonNull protected List<TemperatureThreshold> getTemperatureThresholds( boolean shouldFilter, int type) { - synchronized (mHalLock) { - final List<TemperatureThreshold> ret = new ArrayList<>(); - if (mInstance == null) { + final IThermal instance = getHalInstance(); + final List<TemperatureThreshold> ret = new ArrayList<>(); + if (instance == null) { + return ret; + } + try { + final TemperatureThreshold[] halRet = + shouldFilter ? instance.getTemperatureThresholdsWithType(type) + : instance.getTemperatureThresholds(); + if (halRet == null) { return ret; } - try { - final TemperatureThreshold[] halRet = - shouldFilter ? mInstance.getTemperatureThresholdsWithType(type) - : mInstance.getTemperatureThresholds(); - if (halRet == null) { - return ret; - } - if (shouldFilter) { - return Arrays.stream(halRet).filter(t -> t.type == type).collect( - Collectors.toList()); - } - return Arrays.asList(halRet); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); - } catch (RemoteException e) { - Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); - connectToHal(); + if (shouldFilter) { + return Arrays.stream(halRet).filter(t -> t.type == type).collect( + Collectors.toList()); + } + return Arrays.asList(halRet); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds due to invalid status", e); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't getTemperatureThresholds, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); } - return ret; } + return ret; + } + + @Override + protected float forecastSkinTemperature(int forecastSeconds) { + final IThermal instance = getHalInstance(); + if (instance == null) { + return Float.NaN; + } + try { + return instance.forecastSkinTemperature(forecastSeconds); + } catch (RemoteException e) { + Slog.e(TAG, "Couldn't forecastSkinTemperature, reconnecting...", e); + synchronized (mHalLock) { + connectToHalIfNeededLocked(instance); + } + } + return Float.NaN; } @Override protected boolean connectToHal() { synchronized (mHalLock) { - IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( - IThermal.DESCRIPTOR + "/default")); - initProxyAndRegisterCallback(binder); + return connectToHalIfNeededLocked(mInstance); } + } + + @GuardedBy("mHalLock") + protected boolean connectToHalIfNeededLocked(IThermal instance) { + if (instance != mInstance) { + // instance has been updated since last used + return true; + } + IBinder binder = Binder.allowBlocking(ServiceManager.waitForDeclaredService( + IThermal.DESCRIPTOR + "/default")); + initProxyAndRegisterCallbackLocked(binder); return mInstance != null; } @VisibleForTesting void initProxyAndRegisterCallback(IBinder binder) { synchronized (mHalLock) { - if (binder != null) { - mInstance = IThermal.Stub.asInterface(binder); + initProxyAndRegisterCallbackLocked(binder); + } + } + + @GuardedBy("mHalLock") + protected void initProxyAndRegisterCallbackLocked(IBinder binder) { + if (binder != null) { + mInstance = IThermal.Stub.asInterface(binder); + try { + binder.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); + } + if (mInstance != null) { try { - binder.linkToDeath(this, 0); + Slog.i(TAG, "Thermal HAL AIDL service connected with version " + + mInstance.getInterfaceVersion()); } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); connectToHal(); + return; } - if (mInstance != null) { - try { - Slog.i(TAG, "Thermal HAL AIDL service connected with version " - + mInstance.getInterfaceVersion()); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to read interface version from Thermal HAL", e); - connectToHal(); - return; - } - registerThermalChangedCallback(); + try { + mInstance.registerThermalChangedCallback(mThermalCallbackAidl); + } catch (IllegalArgumentException | IllegalStateException e) { + Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", + e); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); + connectToHal(); } } } } - @VisibleForTesting - void registerThermalChangedCallback() { - try { - mInstance.registerThermalChangedCallback(mThermalCallbackAidl); - } catch (IllegalArgumentException | IllegalStateException e) { - Slog.e(TAG, "Couldn't registerThermalChangedCallback due to invalid status", - e); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to connect IThermal AIDL instance", e); - connectToHal(); - } - } - @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { @@ -1445,6 +1499,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1583,6 +1642,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 1.1"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1749,6 +1813,11 @@ public class ThermalManagerService extends SystemService { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + throw new UnsupportedOperationException("Not supported in Thermal HAL 2.0"); + } + + @Override protected void dump(PrintWriter pw, String prefix) { synchronized (mHalLock) { pw.print(prefix); @@ -1977,6 +2046,39 @@ public class ThermalManagerService extends SystemService { float getForecast(int forecastSeconds) { synchronized (mSamples) { + // If we don't have any thresholds, we can't normalize the temperatures, + // so return early + if (mSevereThresholds.isEmpty()) { + Slog.e(TAG, "No temperature thresholds found"); + FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, + Binder.getCallingUid(), + THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, + Float.NaN, forecastSeconds); + return Float.NaN; + } + } + if (mIsHalSkinForecastSupported.get()) { + float threshold = -1f; + synchronized (mSamples) { + // we only do forecast if a single SKIN sensor threshold is reported + if (mSevereThresholds.size() == 1) { + threshold = mSevereThresholds.valueAt(0); + } + } + if (threshold > 0) { + try { + final float forecastTemperature = + mHalWrapper.forecastSkinTemperature(forecastSeconds); + return normalizeTemperature(forecastTemperature, threshold); + } catch (UnsupportedOperationException e) { + Slog.wtf(TAG, "forecastSkinTemperature returns unsupported"); + } catch (Exception e) { + Slog.e(TAG, "forecastSkinTemperature fails"); + } + return Float.NaN; + } + } + synchronized (mSamples) { mLastForecastCallTimeMillis = SystemClock.elapsedRealtime(); if (mSamples.isEmpty()) { getAndUpdateTemperatureSamples(); @@ -1993,17 +2095,6 @@ public class ThermalManagerService extends SystemService { return Float.NaN; } - // If we don't have any thresholds, we can't normalize the temperatures, - // so return early - if (mSevereThresholds.isEmpty()) { - Slog.e(TAG, "No temperature thresholds found"); - FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED, - Binder.getCallingUid(), - THERMAL_HEADROOM_CALLED__API_STATUS__NO_TEMPERATURE_THRESHOLD, - Float.NaN, forecastSeconds); - return Float.NaN; - } - if (mCachedHeadrooms.contains(forecastSeconds)) { // TODO(b/360486877): replace with metrics Slog.d(TAG, diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java index 5cd7dee35e5f..42b44013bea2 100644 --- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java +++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java @@ -37,6 +37,11 @@ public class PowerManagerFlags { Flags.FLAG_ENABLE_EARLY_SCREEN_TIMEOUT_DETECTOR, Flags::enableEarlyScreenTimeoutDetector); + private final FlagState mEnableScreenTimeoutPolicyListenerApi = new FlagState( + Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API, + Flags::enableScreenTimeoutPolicyListenerApi + ); + private final FlagState mImproveWakelockLatency = new FlagState( Flags.FLAG_IMPROVE_WAKELOCK_LATENCY, Flags::improveWakelockLatency @@ -63,6 +68,11 @@ public class PowerManagerFlags { return mEarlyScreenTimeoutDetectorFlagState.isEnabled(); } + /** Returns whether screen timeout policy listener APIs are enabled on not. */ + public boolean isScreenTimeoutPolicyListenerApiEnabled() { + return mEnableScreenTimeoutPolicyListenerApi.isEnabled(); + } + /** * @return Whether to improve the wakelock acquire/release latency or not */ diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index a975da32f2fd..613daf820e34 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -12,6 +12,17 @@ flag { } flag { + name: "enable_screen_timeout_policy_listener_api" + namespace: "power" + description: "Enables APIs that allow to listen to screen timeout policy changes" + bug: "363174979" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "improve_wakelock_latency" namespace: "power" description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock." diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index a6f2a3757dcb..1cf24fcd8594 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -20,7 +20,6 @@ import static android.os.Flags.adpfUseFmqChannel; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; import static com.android.server.power.hint.Flags.adpfSessionTag; -import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck; import static com.android.server.power.hint.Flags.powerhintThreadCleanup; import static com.android.server.power.hint.Flags.resetOnForkEnabled; @@ -1604,8 +1603,7 @@ public final class HintManagerService extends SystemService { } } } - if (cpuHeadroomAffinityCheck() && mCheckHeadroomAffinity - && params.tids.length > 1) { + if (mCheckHeadroomAffinity && params.tids.length > 1) { checkThreadAffinityForTids(params.tids); } halParams.tids = params.tids; diff --git a/services/core/java/com/android/server/powerstats/PowerStatsService.java b/services/core/java/com/android/server/powerstats/PowerStatsService.java index 89fa9b61b745..b723da3c7769 100644 --- a/services/core/java/com/android/server/powerstats/PowerStatsService.java +++ b/services/core/java/com/android/server/powerstats/PowerStatsService.java @@ -19,6 +19,7 @@ package com.android.server.powerstats; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.pm.PackageManager; import android.hardware.power.stats.Channel; import android.hardware.power.stats.EnergyConsumer; import android.hardware.power.stats.EnergyConsumerResult; @@ -37,6 +38,7 @@ import android.os.IPowerStatsService; import android.os.Looper; import android.os.PowerMonitor; import android.os.PowerMonitorReadings; +import android.os.Process; import android.os.ResultReceiver; import android.os.UserHandle; import android.power.PowerStatsInternal; @@ -83,7 +85,8 @@ public class PowerStatsService extends SystemService { private static final String METER_CACHE_FILENAME = "meterCache"; private static final String MODEL_CACHE_FILENAME = "modelCache"; private static final String RESIDENCY_CACHE_FILENAME = "residencyCache"; - private static final long MAX_POWER_MONITOR_AGE_MILLIS = 30_000; + private static final long MAX_POWER_MONITOR_AGE_MILLIS = 20_000; + private static final long MAX_FINE_POWER_MONITOR_AGE_MILLIS = 250; static final String KEY_POWER_MONITOR_API_ENABLED = "power_monitor_api_enabled"; @@ -203,6 +206,11 @@ public class PowerStatsService extends SystemService { IntervalRandomNoiseGenerator createIntervalRandomNoiseGenerator() { return new IntervalRandomNoiseGenerator(INTERVAL_RANDOM_NOISE_GENERATION_ALPHA); } + + boolean checkFinePowerMonitorsPermission(Context context, int callingUid) { + return context.checkPermission(android.Manifest.permission.ACCESS_FINE_POWER_MONITORS, + Process.INVALID_PID, callingUid) == PackageManager.PERMISSION_GRANTED; + } } private final IBinder mService = new IPowerStatsService.Stub() { @@ -571,6 +579,7 @@ public class PowerStatsService extends SystemService { private boolean mPowerMonitorApiEnabled = true; private volatile PowerMonitor[] mPowerMonitors; private PowerMonitorState[] mPowerMonitorStates; + private PowerMonitorState[] mFinePowerMonitorStates; private IntervalRandomNoiseGenerator mIntervalRandomNoiseGenerator; private void setPowerMonitorApiEnabled(boolean powerMonitorApiEnabled) { @@ -578,6 +587,7 @@ public class PowerStatsService extends SystemService { mPowerMonitorApiEnabled = powerMonitorApiEnabled; mPowerMonitors = null; mPowerMonitorStates = null; + mFinePowerMonitorStates = null; } } @@ -598,6 +608,7 @@ public class PowerStatsService extends SystemService { if (!mPowerMonitorApiEnabled) { mPowerMonitors = new PowerMonitor[0]; mPowerMonitorStates = new PowerMonitorState[0]; + mFinePowerMonitorStates = new PowerMonitorState[0]; return; } @@ -628,6 +639,7 @@ public class PowerStatsService extends SystemService { } mPowerMonitors = monitors.toArray(new PowerMonitor[monitors.size()]); mPowerMonitorStates = states.toArray(new PowerMonitorState[monitors.size()]); + mFinePowerMonitorStates = states.toArray(new PowerMonitorState[monitors.size()]); } } @@ -710,24 +722,38 @@ public class PowerStatsService extends SystemService { ResultReceiver resultReceiver, int callingUid) { ensurePowerMonitors(); + @PowerMonitorReadings.PowerMonitorGranularity int granularity = + mInjector.checkFinePowerMonitorsPermission(mContext, callingUid) + ? PowerMonitorReadings.GRANULARITY_FINE + : PowerMonitorReadings.GRANULARITY_UNSPECIFIED; + + PowerMonitorState[] allPowerMonitorStates; + long maxAge; + if (granularity == PowerMonitorReadings.GRANULARITY_FINE) { + allPowerMonitorStates = mFinePowerMonitorStates; + maxAge = MAX_FINE_POWER_MONITOR_AGE_MILLIS; + } else { + allPowerMonitorStates = mPowerMonitorStates; + maxAge = MAX_POWER_MONITOR_AGE_MILLIS; + } + long earliestTimestamp = Long.MAX_VALUE; PowerMonitorState[] powerMonitorStates = new PowerMonitorState[powerMonitorIndices.length]; for (int i = 0; i < powerMonitorIndices.length; i++) { int index = powerMonitorIndices[i]; - if (index < 0 || index >= mPowerMonitorStates.length) { + if (index < 0 || index >= allPowerMonitorStates.length) { resultReceiver.send(IPowerStatsService.RESULT_UNSUPPORTED_POWER_MONITOR, null); return; } - powerMonitorStates[i] = mPowerMonitorStates[index]; - if (mPowerMonitorStates[index] != null - && mPowerMonitorStates[index].timestampMs < earliestTimestamp) { - earliestTimestamp = mPowerMonitorStates[index].timestampMs; + powerMonitorStates[i] = allPowerMonitorStates[index]; + if (allPowerMonitorStates[index] != null + && allPowerMonitorStates[index].timestampMs < earliestTimestamp) { + earliestTimestamp = allPowerMonitorStates[index].timestampMs; } } - if (earliestTimestamp == 0 - || mClock.elapsedRealtime() - earliestTimestamp > MAX_POWER_MONITOR_AGE_MILLIS) { + if (earliestTimestamp == 0 || mClock.elapsedRealtime() - earliestTimestamp > maxAge) { updateEnergyConsumers(powerMonitorStates); updateEnergyMeasurements(powerMonitorStates); mIntervalRandomNoiseGenerator.refresh(); @@ -765,6 +791,7 @@ public class PowerStatsService extends SystemService { Bundle result = new Bundle(); result.putLongArray(IPowerStatsService.KEY_ENERGY, energy); result.putLongArray(IPowerStatsService.KEY_TIMESTAMPS, timestamps); + result.putInt(IPowerStatsService.KEY_GRANULARITY, granularity); resultReceiver.send(IPowerStatsService.RESULT_SUCCESS, result); } diff --git a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java index 17739712d65a..a75d110e3cd1 100644 --- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java +++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java @@ -88,5 +88,6 @@ public class ResourcesManagerShellCommand extends ShellCommand { out.println(" Print this help text."); out.println(" dump <PROCESS>"); out.println(" Dump the Resources objects in use as well as the history of Resources"); + } } diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index 14539d544bf9..50db1e4ac30e 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -84,8 +84,12 @@ class RollbackStore { */ private static List<Rollback> loadRollbacks(File rollbackDataDir) { List<Rollback> rollbacks = new ArrayList<>(); - rollbackDataDir.mkdirs(); - for (File rollbackDir : rollbackDataDir.listFiles()) { + File[] rollbackDirs = rollbackDataDir.listFiles(); + if (rollbackDirs == null) { + Slog.e(TAG, "Folder doesn't exist: " + rollbackDataDir); + return rollbacks; + } + for (File rollbackDir : rollbackDirs) { if (rollbackDir.isDirectory()) { try { rollbacks.add(loadRollback(rollbackDir)); diff --git a/services/core/java/com/android/server/selinux/QuotaExceededException.java b/services/core/java/com/android/server/selinux/QuotaExceededException.java new file mode 100644 index 000000000000..26d4d827af6b --- /dev/null +++ b/services/core/java/com/android/server/selinux/QuotaExceededException.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +/** An exception raised when the quota has been reached. + * + * This exception is raised in EventLogCollection.add(). See QuotaLimiter + * for the implementation details. + */ +class QuotaExceededException extends Exception {} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java index d69150d88e4f..a1f72be7a039 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -15,7 +15,6 @@ */ package com.android.server.selinux; -import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.Slog; @@ -34,10 +33,6 @@ class SelinuxAuditLogBuilder { private static final String TAG = "SelinuxAuditLogs"; - // This config indicates which Selinux logs for source domains to collect. The string will be - // inserted into a regex, so it must follow the regex syntax. For example, a valid value would - // be "system_server|untrusted_app". - @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher(""); private static final String TCONTEXT_PATTERN = "u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*"; @@ -50,7 +45,7 @@ class SelinuxAuditLogBuilder { private Iterator<String> mTokens; private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); - SelinuxAuditLogBuilder() { + SelinuxAuditLogBuilder(String auditDomain) { Matcher scontextMatcher = NO_OP_MATCHER; Matcher tcontextMatcher = NO_OP_MATCHER; Matcher pathMatcher = NO_OP_MATCHER; @@ -59,10 +54,7 @@ class SelinuxAuditLogBuilder { Pattern.compile( TextUtils.formatSimple( "u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*", - DeviceConfig.getString( - DeviceConfig.NAMESPACE_ADSERVICES, - CONFIG_SELINUX_AUDIT_DOMAIN, - "no_match^"))) + auditDomain)) .matcher(""); tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher(""); pathMatcher = Pattern.compile(PATH_PATTERN).matcher(""); diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java index c655d46eb9f4..54365ff03db0 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -15,6 +15,7 @@ */ package com.android.server.selinux; +import android.provider.DeviceConfig; import android.util.EventLog; import android.util.EventLog.Event; import android.util.Log; @@ -27,11 +28,10 @@ import com.android.server.utils.Slogf; import java.io.IOException; import java.time.Instant; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; +import java.util.AbstractCollection; +import java.util.Iterator; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,93 +43,115 @@ class SelinuxAuditLogsCollector { private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + // This config indicates which Selinux logs for source domains to collect. The string will be + // inserted into a regex, so it must follow the regex syntax. For example, a valid value would + // be "system_server|untrusted_app". + @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain"; + @VisibleForTesting static final String DEFAULT_SELINUX_AUDIT_DOMAIN = "no_match^"; + @VisibleForTesting static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + private final Supplier<String> mAuditDomainSupplier; private final RateLimiter mRateLimiter; private final QuotaLimiter mQuotaLimiter; + private EventLogCollection mEventCollection; @VisibleForTesting Instant mLastWrite = Instant.MIN; AtomicBoolean mStopRequested = new AtomicBoolean(false); - SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + SelinuxAuditLogsCollector( + Supplier<String> auditDomainSupplier, + RateLimiter rateLimiter, + QuotaLimiter quotaLimiter) { + mAuditDomainSupplier = auditDomainSupplier; mRateLimiter = rateLimiter; mQuotaLimiter = quotaLimiter; + mEventCollection = new EventLogCollection(); + } + + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + this( + () -> + DeviceConfig.getString( + DeviceConfig.NAMESPACE_ADSERVICES, + CONFIG_SELINUX_AUDIT_DOMAIN, + DEFAULT_SELINUX_AUDIT_DOMAIN), + rateLimiter, + quotaLimiter); } public void setStopRequested(boolean stopRequested) { mStopRequested.set(stopRequested); } - /** - * Collect and push SELinux audit logs for the provided {@code tagCode}. + /** A Collection to work around EventLog.readEvents() constraints. + * + * This collection only supports add(). Any other method inherited from + * Collection will throw an UnsupportedOperationException exception. * - * @return true if the job was completed. If the job was interrupted, return false. + * This collection ensures that we are processing one event at a time and + * avoid collecting all the event objects before processing (e.g., + * ArrayList), which could lead to an OOM situation. */ - boolean collect(int tagCode) { - Queue<Event> logLines = new ArrayDeque<>(); - Instant latestTimestamp = collectLogLines(tagCode, logLines); - - boolean quotaExceeded = writeAuditLogs(logLines); - if (quotaExceeded) { - Slog.w(TAG, "Too many SELinux logs in the queue, I am giving up."); - mLastWrite = latestTimestamp; // next run we will ignore all these logs. - logLines.clear(); + class EventLogCollection extends AbstractCollection<Event> { + + SelinuxAuditLogBuilder mAuditLogBuilder; + int mAuditsWritten = 0; + Instant mLatestTimestamp; + + void reset() { + mAuditsWritten = 0; + mLatestTimestamp = mLastWrite; + mAuditLogBuilder = new SelinuxAuditLogBuilder(mAuditDomainSupplier.get()); } - return logLines.isEmpty(); - } + int getAuditsWritten() { + return mAuditsWritten; + } - private Instant collectLogLines(int tagCode, Queue<Event> logLines) { - List<Event> events = new ArrayList<>(); - try { - EventLog.readEvents(new int[] {tagCode}, events); - } catch (IOException e) { - Slog.e(TAG, "Error reading event logs", e); + Instant getLatestTimestamp() { + return mLatestTimestamp; } - Instant latestTimestamp = mLastWrite; - for (Event event : events) { - Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos()); - if (eventTime.isAfter(latestTimestamp)) { - latestTimestamp = eventTime; + @Override + public Iterator<Event> iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean add(Event event) { + if (mStopRequested.get()) { + throw new IllegalStateException(new InterruptedException()); } + + Instant eventTime = Instant.ofEpochSecond(/* epochSecond= */ 0, event.getTimeNanos()); if (eventTime.compareTo(mLastWrite) <= 0) { - continue; + return true; } Object eventData = event.getData(); if (!(eventData instanceof String)) { - continue; + return true; } - logLines.add(event); - } - return latestTimestamp; - } - - private boolean writeAuditLogs(Queue<Event> logLines) { - final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); - int auditsWritten = 0; - - while (!mStopRequested.get() && !logLines.isEmpty()) { - Event event = logLines.poll(); - String logLine = (String) event.getData(); - Instant logTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + String logLine = (String) eventData; if (!SELINUX_MATCHER.reset(logLine).matches()) { - continue; + return true; } - auditLogBuilder.reset(SELINUX_MATCHER.group("denial")); - final SelinuxAuditLog auditLog = auditLogBuilder.build(); + mAuditLogBuilder.reset(SELINUX_MATCHER.group("denial")); + final SelinuxAuditLog auditLog = mAuditLogBuilder.build(); if (auditLog == null) { - continue; + return true; } if (!mQuotaLimiter.acquire()) { - if (DEBUG) { - Slogf.d(TAG, "Running out of quota after %d logs.", auditsWritten); - } - return true; + throw new IllegalStateException(new QuotaExceededException()); } mRateLimiter.acquire(); @@ -144,16 +166,50 @@ class SelinuxAuditLogsCollector { auditLog.mTClass, auditLog.mPath, auditLog.mPermissive); - auditsWritten++; - if (logTime.isAfter(mLastWrite)) { - mLastWrite = logTime; + mAuditsWritten++; + if (eventTime.isAfter(mLatestTimestamp)) { + mLatestTimestamp = eventTime; + } + + return true; + } + } + + /** + * Collect and push SELinux audit logs for the provided {@code tagCode}. + * + * @return true if the job was completed. If the job was interrupted or + * failed because of IOException, return false. + * @throws QuotaExceededException if it ran out of quota. + */ + boolean collect(int tagCode) throws QuotaExceededException { + mEventCollection.reset(); + try { + EventLog.readEvents(new int[] {tagCode}, mEventCollection); + } catch (IllegalStateException e) { + if (e.getCause() instanceof QuotaExceededException) { + if (DEBUG) { + Slogf.d(TAG, "Running out of quota after %d logs.", + mEventCollection.getAuditsWritten()); + } + // next run we will ignore all these logs. + mLastWrite = mEventCollection.getLatestTimestamp(); + throw (QuotaExceededException) e.getCause(); + } else if (e.getCause() instanceof InterruptedException) { + mLastWrite = mEventCollection.getLatestTimestamp(); + return false; } + throw e; + } catch (IOException e) { + Slog.e(TAG, "Error reading event logs", e); + return false; } + mLastWrite = mEventCollection.getLatestTimestamp(); if (DEBUG) { - Slogf.d(TAG, "Written %d logs", auditsWritten); + Slogf.d(TAG, "Written %d logs", mEventCollection.getAuditsWritten()); } - return false; + return true; } } diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsJob.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsJob.java index 0092c3797156..e55e5900f265 100644 --- a/services/core/java/com/android/server/selinux/SelinuxAuditLogsJob.java +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsJob.java @@ -51,8 +51,12 @@ final class SelinuxAuditLogsJob { return; } mIsRunning.set(true); - boolean done = mAuditLogsCollector.collect(SelinuxAuditLogsService.AUDITD_TAG_CODE); - if (done) { + try { + boolean done = mAuditLogsCollector.collect(SelinuxAuditLogsService.AUDITD_TAG_CODE); + if (done) { + jobService.jobFinished(params, /* wantsReschedule= */ false); + } + } catch (QuotaExceededException e) { jobService.jobFinished(params, /* wantsReschedule= */ false); } mIsRunning.set(false); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 4ed5f90f2852..a19a3422af06 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2199,6 +2199,19 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D }); } + /** + * Called when the notification should be rebundled. + * @param key the notification key + */ + @Override + public void rebundleNotification(String key) { + enforceStatusBarService(); + enforceValidCallingUser(); + Binder.withCleanCallingIdentity(() -> { + mNotificationDelegate.rebundleNotification(key); + }); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java index 2049a0288f5a..b651c7ba34c3 100644 --- a/services/core/java/com/android/server/timedetector/ServerFlags.java +++ b/services/core/java/com/android/server/timedetector/ServerFlags.java @@ -72,8 +72,12 @@ public final class ServerFlags { KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT, KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, KEY_ENHANCED_METRICS_COLLECTION_ENABLED, + KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, + KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED }) - @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.SOURCE) @interface DeviceConfigKey {} @@ -192,6 +196,31 @@ public final class ServerFlags { "enhanced_metrics_collection_enabled"; /** + * The key to control support for time zone notifications under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED = + "time_zone_notifications_supported"; + + /** + * The key for the default value used to determine whether time zone notifications is enabled + * when the user hasn't explicitly set it yet. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT = + "time_zone_notifications_enabled_default"; + + /** + * The key to control support for time zone notifications tracking under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED = + "time_zone_notifications_tracking_supported"; + + /** + * The key to control support for time zone manual change tracking under certain circumstances. + */ + public static final @DeviceConfigKey String KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED = + "time_zone_manual_change_tracking_supported"; + + /** * The registered listeners and the keys to trigger on. The value is explicitly a HashSet to * ensure O(1) lookup performance when working out whether a listener should trigger. */ diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java index 3579246b660f..0495f54cb154 100644 --- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java @@ -286,7 +286,8 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { // This check is racey, but the whole settings update process is racey. This check prevents // a ConfigurationChangeListener callback triggering due to ContentObserver's still // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary - // for stable behavior during tests. + // for stable behavior during tests. This behavior is copied from + // setAutoDetectionEnabledIfRequired and assumed to be the correct way. if (getAutoDetectionEnabledSetting() != enabled) { Settings.Global.putInt(mCr, Settings.Global.AUTO_TIME, enabled ? 1 : 0); } diff --git a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java index fc659c5cb627..c4c86a429dd6 100644 --- a/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java +++ b/services/core/java/com/android/server/timezonedetector/ConfigurationInternal.java @@ -65,6 +65,10 @@ public final class ConfigurationInternal { private final boolean mUserConfigAllowed; private final boolean mLocationEnabledSetting; private final boolean mGeoDetectionEnabledSetting; + private final boolean mNotificationsSupported; + private final boolean mNotificationsEnabledSetting; + private final boolean mNotificationTrackingSupported; + private final boolean mManualChangeTrackingSupported; private ConfigurationInternal(Builder builder) { mTelephonyDetectionSupported = builder.mTelephonyDetectionSupported; @@ -78,6 +82,10 @@ public final class ConfigurationInternal { mUserConfigAllowed = builder.mUserConfigAllowed; mLocationEnabledSetting = builder.mLocationEnabledSetting; mGeoDetectionEnabledSetting = builder.mGeoDetectionEnabledSetting; + mNotificationsSupported = builder.mNotificationsSupported; + mNotificationsEnabledSetting = builder.mNotificationsEnabledSetting; + mNotificationTrackingSupported = builder.mNotificationsTrackingSupported; + mManualChangeTrackingSupported = builder.mManualChangeTrackingSupported; } /** Returns true if the device supports any form of auto time zone detection. */ @@ -104,6 +112,27 @@ public final class ConfigurationInternal { } /** + * Returns true if the device supports time-related notifications. + */ + public boolean areNotificationsSupported() { + return mNotificationsSupported; + } + + /** + * Returns true if the device supports tracking of time-related notifications. + */ + public boolean isNotificationTrackingSupported() { + return areNotificationsSupported() && mNotificationTrackingSupported; + } + + /** + * Returns true if the device supports tracking of time zone manual changes. + */ + public boolean isManualChangeTrackingSupported() { + return mManualChangeTrackingSupported; + } + + /** * Returns {@code true} if location time zone detection should run when auto time zone detection * is enabled on supported devices, even when the user has not enabled the algorithm explicitly * in settings. Enabled for internal testing only. See {@link #isGeoDetectionExecutionEnabled()} @@ -223,6 +252,15 @@ public final class ConfigurationInternal { && getGeoDetectionRunInBackgroundEnabledSetting(); } + /** Returns true if time-related notifications can be shown on this device. */ + public boolean getNotificationsEnabledBehavior() { + return areNotificationsSupported() && getNotificationsEnabledSetting(); + } + + private boolean getNotificationsEnabledSetting() { + return mNotificationsEnabledSetting; + } + @NonNull public TimeZoneCapabilities asCapabilities(boolean bypassUserPolicyChecks) { UserHandle userHandle = UserHandle.of(mUserId); @@ -283,6 +321,14 @@ public final class ConfigurationInternal { } builder.setSetManualTimeZoneCapability(suggestManualTimeZoneCapability); + final @CapabilityState int configureNotificationsEnabledCapability; + if (areNotificationsSupported()) { + configureNotificationsEnabledCapability = CAPABILITY_POSSESSED; + } else { + configureNotificationsEnabledCapability = CAPABILITY_NOT_SUPPORTED; + } + builder.setConfigureNotificationsEnabledCapability(configureNotificationsEnabledCapability); + return builder.build(); } @@ -291,6 +337,7 @@ public final class ConfigurationInternal { return new TimeZoneConfiguration.Builder() .setAutoDetectionEnabled(getAutoDetectionEnabledSetting()) .setGeoDetectionEnabled(getGeoDetectionEnabledSetting()) + .setNotificationsEnabled(getNotificationsEnabledSetting()) .build(); } @@ -307,6 +354,9 @@ public final class ConfigurationInternal { if (newConfiguration.hasIsGeoDetectionEnabled()) { builder.setGeoDetectionEnabledSetting(newConfiguration.isGeoDetectionEnabled()); } + if (newConfiguration.hasIsNotificationsEnabled()) { + builder.setNotificationsEnabledSetting(newConfiguration.areNotificationsEnabled()); + } return builder.build(); } @@ -328,7 +378,11 @@ public final class ConfigurationInternal { && mEnhancedMetricsCollectionEnabled == that.mEnhancedMetricsCollectionEnabled && mAutoDetectionEnabledSetting == that.mAutoDetectionEnabledSetting && mLocationEnabledSetting == that.mLocationEnabledSetting - && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting; + && mGeoDetectionEnabledSetting == that.mGeoDetectionEnabledSetting + && mNotificationsSupported == that.mNotificationsSupported + && mNotificationsEnabledSetting == that.mNotificationsEnabledSetting + && mNotificationTrackingSupported == that.mNotificationTrackingSupported + && mManualChangeTrackingSupported == that.mManualChangeTrackingSupported; } @Override @@ -336,7 +390,9 @@ public final class ConfigurationInternal { return Objects.hash(mUserId, mUserConfigAllowed, mTelephonyDetectionSupported, mGeoDetectionSupported, mTelephonyFallbackSupported, mGeoDetectionRunInBackgroundEnabled, mEnhancedMetricsCollectionEnabled, - mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting); + mAutoDetectionEnabledSetting, mLocationEnabledSetting, mGeoDetectionEnabledSetting, + mNotificationsSupported, mNotificationsEnabledSetting, + mNotificationTrackingSupported, mManualChangeTrackingSupported); } @Override @@ -352,6 +408,10 @@ public final class ConfigurationInternal { + ", mAutoDetectionEnabledSetting=" + mAutoDetectionEnabledSetting + ", mLocationEnabledSetting=" + mLocationEnabledSetting + ", mGeoDetectionEnabledSetting=" + mGeoDetectionEnabledSetting + + ", mNotificationsSupported=" + mNotificationsSupported + + ", mNotificationsEnabledSetting=" + mNotificationsEnabledSetting + + ", mNotificationTrackingSupported=" + mNotificationTrackingSupported + + ", mManualChangeTrackingSupported=" + mManualChangeTrackingSupported + '}'; } @@ -370,6 +430,10 @@ public final class ConfigurationInternal { private boolean mAutoDetectionEnabledSetting; private boolean mLocationEnabledSetting; private boolean mGeoDetectionEnabledSetting; + private boolean mNotificationsSupported; + private boolean mNotificationsEnabledSetting; + private boolean mNotificationsTrackingSupported; + private boolean mManualChangeTrackingSupported; /** * Creates a new Builder. @@ -390,6 +454,10 @@ public final class ConfigurationInternal { this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting; this.mLocationEnabledSetting = toCopy.mLocationEnabledSetting; this.mGeoDetectionEnabledSetting = toCopy.mGeoDetectionEnabledSetting; + this.mNotificationsSupported = toCopy.mNotificationsSupported; + this.mNotificationsEnabledSetting = toCopy.mNotificationsEnabledSetting; + this.mNotificationsTrackingSupported = toCopy.mNotificationTrackingSupported; + this.mManualChangeTrackingSupported = toCopy.mManualChangeTrackingSupported; } /** @@ -475,6 +543,38 @@ public final class ConfigurationInternal { return this; } + /** + * Sets the value of the time notification setting for this user. + */ + public Builder setNotificationsEnabledSetting(boolean enabled) { + mNotificationsEnabledSetting = enabled; + return this; + } + + /** + * Sets whether time zone notifications are supported on this device. + */ + public Builder setNotificationsSupported(boolean enabled) { + mNotificationsSupported = enabled; + return this; + } + + /** + * Sets whether time zone notification tracking is supported on this device. + */ + public Builder setNotificationsTrackingSupported(boolean supported) { + mNotificationsTrackingSupported = supported; + return this; + } + + /** + * Sets whether time zone manual change tracking are supported on this device. + */ + public Builder setManualChangeTrackingSupported(boolean supported) { + mManualChangeTrackingSupported = supported; + return this; + } + /** Returns a new {@link ConfigurationInternal}. */ @NonNull public ConfigurationInternal build() { diff --git a/services/core/java/com/android/server/timezonedetector/Environment.java b/services/core/java/com/android/server/timezonedetector/Environment.java new file mode 100644 index 000000000000..795fb02373ff --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/Environment.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 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.timezonedetector; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; +import android.annotation.NonNull; + +import com.android.server.SystemTimeZone; + +import java.io.PrintWriter; + +/** + * Used by the time zone detector code to interact with device state besides that available from + * {@link ServiceConfigAccessor}. It can be faked for testing. + */ +public interface Environment { + + /** + * Returns the device's currently configured time zone. May return an empty string. + */ + @NonNull + String getDeviceTimeZone(); + + /** + * Returns the confidence of the device's current time zone. + */ + @SystemTimeZone.TimeZoneConfidence + int getDeviceTimeZoneConfidence(); + + /** + * Sets the device's time zone, associated confidence, and records a debug log entry. + */ + void setDeviceTimeZoneAndConfidence( + @NonNull String zoneId, @SystemTimeZone.TimeZoneConfidence int confidence, + @NonNull String logInfo); + + /** + * Returns the time according to the elapsed realtime clock, the same as {@link + * android.os.SystemClock#elapsedRealtime()}. + */ + @ElapsedRealtimeLong + long elapsedRealtimeMillis(); + + /** + * Returns the current time in milliseconds, the same as + * {@link java.lang.System#currentTimeMillis()}. + */ + @CurrentTimeMillisLong + long currentTimeMillis(); + + /** + * Adds a standalone entry to the time zone debug log. + */ + void addDebugLogEntry(@NonNull String logMsg); + + /** + * Dumps the time zone debug log to the supplied {@link PrintWriter}. + */ + void dumpDebugLog(PrintWriter printWriter); + + /** + * Requests that the supplied runnable be invoked asynchronously. + */ + void runAsync(@NonNull Runnable runnable); +} diff --git a/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java index 449b41a09c51..8491b4818c2e 100644 --- a/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java +++ b/services/core/java/com/android/server/timezonedetector/EnvironmentImpl.java @@ -16,6 +16,7 @@ package com.android.server.timezonedetector; +import android.annotation.CurrentTimeMillisLong; import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.os.Handler; @@ -31,9 +32,9 @@ import java.io.PrintWriter; import java.util.Objects; /** - * The real implementation of {@link TimeZoneDetectorStrategyImpl.Environment}. + * The real implementation of {@link Environment}. */ -final class EnvironmentImpl implements TimeZoneDetectorStrategyImpl.Environment { +final class EnvironmentImpl implements Environment { private static final String TIMEZONE_PROPERTY = "persist.sys.timezone"; @@ -69,6 +70,11 @@ final class EnvironmentImpl implements TimeZoneDetectorStrategyImpl.Environment } @Override + public @CurrentTimeMillisLong long currentTimeMillis() { + return System.currentTimeMillis(); + } + + @Override public void addDebugLogEntry(@NonNull String logMsg) { SystemTimeZone.addDebugLogEntry(logMsg); } diff --git a/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java new file mode 100644 index 000000000000..cf85a9a6a706 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/NotifyingTimeZoneChangeListener.java @@ -0,0 +1,655 @@ +/* + * 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.timezonedetector; + +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.app.PendingIntent.FLAG_IMMUTABLE; +import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; +import static android.content.Context.RECEIVER_NOT_EXPORTED; +import static android.provider.Settings.ACTION_DATE_SETTINGS; + +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_UNKNOWN; + +import android.annotation.DurationMillisLong; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.icu.text.DateFormat; +import android.icu.text.SimpleDateFormat; +import android.icu.util.TimeZone; +import android.os.Handler; +import android.os.UserHandle; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.notification.SystemNotificationChannels; +import com.android.server.LocalServices; +import com.android.server.flags.Flags; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An implementation of {@link TimeZoneChangeListener} that fires notifications. + */ +public class NotifyingTimeZoneChangeListener implements TimeZoneChangeListener { + @IntDef({STATUS_UNKNOWN, STATUS_UNTRACKED, STATUS_REJECTED, + STATUS_ACCEPTED, STATUS_SUPERSEDED}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @interface TimeZoneChangeStatus {} + + /** Used to indicate the status could not be inferred. */ + @TimeZoneChangeStatus + static final int STATUS_UNKNOWN = 0; + /** Used to indicate the change is not one that needs to be tracked. */ + @TimeZoneChangeStatus + static final int STATUS_UNTRACKED = 1; + @TimeZoneChangeStatus + static final int STATUS_REJECTED = 2; + @TimeZoneChangeStatus + static final int STATUS_ACCEPTED = 3; + /** Used to indicate a change was superseded before its status could be determined. */ + @TimeZoneChangeStatus + static final int STATUS_SUPERSEDED = 4; + + @IntDef({SIGNAL_TYPE_UNKNOWN, SIGNAL_TYPE_NONE, SIGNAL_TYPE_NOTIFICATION, + SIGNAL_TYPE_HEURISTIC}) + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) + @interface SignalType {} + + /** Used when the signal type cannot be inferred. */ + @SignalType + static final int SIGNAL_TYPE_UNKNOWN = 0; + /** Used when the status is not one that needs a signal type. */ + @SignalType + static final int SIGNAL_TYPE_NONE = 1; + @SignalType + static final int SIGNAL_TYPE_NOTIFICATION = 2; + @SignalType + static final int SIGNAL_TYPE_HEURISTIC = 3; + + private static final int MAX_EVENTS_TO_TRACK = 10; + + @VisibleForTesting + @DurationMillisLong + static final long AUTO_REVERT_THRESHOLD = Duration.ofMinutes(15).toMillis(); + + private static final String TAG = "TimeZoneChangeTracker"; + private static final String NOTIFICATION_TAG = "TimeZoneDetector"; + private static final int TZ_CHANGE_NOTIFICATION_ID = 1001; + + private static final String ACTION_NOTIFICATION_DELETED = + "com.android.server.timezonedetector.TimeZoneNotificationDeleted"; + + private static final String NOTIFICATION_INTENT_EXTRA_USER_ID = "user_id"; + private static final String NOTIFICATION_INTENT_EXTRA_CHANGE_ID = "change_id"; + + private final Context mContext; + private final NotificationManager mNotificationManager; + private final ActivityManagerInternal mActivityManagerInternal; + + // For scheduling callbacks + private final Handler mHandler; + private final ServiceConfigAccessor mServiceConfigAccessor; + private final AtomicInteger mNextChangeEventId = new AtomicInteger(1); + + private final Resources mRes = Resources.getSystem(); + + @GuardedBy("mTimeZoneChangeRecord") + private final ReferenceWithHistory<TimeZoneChangeRecord> mTimeZoneChangeRecord = + new ReferenceWithHistory<>(MAX_EVENTS_TO_TRACK); + + private final BroadcastReceiver mNotificationReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_NOTIFICATION_DELETED: + int notifiedUserId = intent.getIntExtra( + NOTIFICATION_INTENT_EXTRA_USER_ID, UserHandle.USER_NULL); + int changeEventId = intent.getIntExtra( + NOTIFICATION_INTENT_EXTRA_CHANGE_ID, 0); + notificationSwipedAway(notifiedUserId, changeEventId); + break; + default: + Log.d(TAG, "Unknown intent action received: " + intent.getAction()); + } + } + }; + + @NonNull + private final Environment mEnvironment; + + private final Object mConfigurationLock = new Object(); + @GuardedBy("mConfigurationLock") + private ConfigurationInternal mConfigurationInternal; + @GuardedBy("mConfigurationLock") + private boolean mIsRegistered; + + private int mAcceptedManualChanges; + private int mAcceptedTelephonyChanges; + private int mAcceptedLocationChanges; + private int mAcceptedUnknownChanges; + private int mRejectedTelephonyChanges; + private int mRejectedLocationChanges; + private int mRejectedUnknownChanges; + + /** Create and initialise a new {@code TimeZoneChangeTrackerImpl} */ + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") + public static NotifyingTimeZoneChangeListener create(Handler handler, Context context, + ServiceConfigAccessor serviceConfigAccessor, + @NonNull Environment environment) { + NotifyingTimeZoneChangeListener changeTracker = + new NotifyingTimeZoneChangeListener(handler, + context, + serviceConfigAccessor, + context.getSystemService(NotificationManager.class), + environment); + + // Pretend there was an update to initialize configuration. + changeTracker.handleConfigurationUpdate(); + + return changeTracker; + } + + @VisibleForTesting + NotifyingTimeZoneChangeListener(Handler handler, Context context, + ServiceConfigAccessor serviceConfigAccessor, NotificationManager notificationManager, + @NonNull Environment environment) { + mHandler = Objects.requireNonNull(handler); + mContext = Objects.requireNonNull(context); + mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); + mServiceConfigAccessor.addConfigurationInternalChangeListener( + this::handleConfigurationUpdate); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mNotificationManager = notificationManager; + mEnvironment = Objects.requireNonNull(environment); + } + + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") + private void handleConfigurationUpdate() { + synchronized (mConfigurationLock) { + ConfigurationInternal oldConfigurationInternal = mConfigurationInternal; + mConfigurationInternal = mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + + if (areNotificationsEnabled() && isNotificationTrackingSupported()) { + if (!mIsRegistered) { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_NOTIFICATION_DELETED); + mContext.registerReceiverForAllUsers(mNotificationReceiver, intentFilter, + /* broadcastPermission= */ null, mHandler, RECEIVER_NOT_EXPORTED); + mIsRegistered = true; + } + } else if (mIsRegistered) { + mContext.unregisterReceiver(mNotificationReceiver); + mIsRegistered = false; + } + + if (oldConfigurationInternal != null) { + boolean userChanged = + oldConfigurationInternal.getUserId() != mConfigurationInternal.getUserId(); + + if (!areNotificationsEnabled() || userChanged) { + // Clear any notifications that are no longer needed. + clearNotificationForUser(oldConfigurationInternal.getUserId()); + } + } + } + } + + private void notificationSwipedAway(@UserIdInt int userId, int changeEventId) { + // User swiping away a notification is interpreted as "user accepted the change". + if (isNotificationTrackingSupported()) { + markChangeAsAccepted(changeEventId, userId, SIGNAL_TYPE_NOTIFICATION); + } + } + + private boolean areNotificationsEnabled() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.getNotificationsEnabledBehavior(); + } + } + + private boolean isNotificationTrackingSupported() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.isNotificationTrackingSupported(); + } + } + + private boolean isManualChangeTrackingSupported() { + synchronized (mConfigurationLock) { + return mConfigurationInternal.isManualChangeTrackingSupported(); + } + } + + /** + * Marks a change event as accepted by the user + * + * <p>A change event is said to be accepted when the client does not revert an automatic time + * zone change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the + * notification being received. + */ + private void markChangeAsAccepted(int changeEventId, @UserIdInt int userId, + @SignalType int signalType) { + if (!isUserIdCurrentUser(userId)) { + return; + } + + synchronized (mTimeZoneChangeRecord) { + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + if (lastTimeZoneChangeRecord != null) { + if (lastTimeZoneChangeRecord.getId() != changeEventId) { + // To be accepted, the change being accepted has to still be the latest. + return; + } + if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) { + // Change status has already been set. + return; + } + lastTimeZoneChangeRecord.setAccepted(signalType); + + switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) { + case ORIGIN_MANUAL: + mAcceptedManualChanges += 1; + break; + case ORIGIN_TELEPHONY: + mAcceptedTelephonyChanges += 1; + break; + case ORIGIN_LOCATION: + mAcceptedLocationChanges += 1; + break; + default: + mAcceptedUnknownChanges += 1; + break; + } + } + } + } + + private boolean isUserIdCurrentUser(@UserIdInt int userId) { + synchronized (mConfigurationLock) { + return userId == mConfigurationInternal.getUserId(); + } + } + + /** + * Marks a change event as rejected by the user + * + * <p>A change event is said to be rejected when the client reverts an automatic time zone + * change by manually changing the time zone within {@code AUTO_REVERT_THRESHOLD} of the + * notification being received. + */ + @GuardedBy("mTimeZoneChangeRecord") + private void markChangeAsRejected(int changeEventId, @UserIdInt int userId, + @SignalType int signalType) { + if (!isUserIdCurrentUser(userId)) { + return; + } + + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + if (lastTimeZoneChangeRecord != null) { + if (lastTimeZoneChangeRecord.getId() != changeEventId) { + // To be accepted, the change being accepted has to still be the latest. + return; + } + if (lastTimeZoneChangeRecord.getStatus() != STATUS_UNKNOWN) { + // Change status has already been set. + return; + } + lastTimeZoneChangeRecord.setRejected(signalType); + + switch (lastTimeZoneChangeRecord.getEvent().getOrigin()) { + case ORIGIN_TELEPHONY: + mRejectedTelephonyChanges += 1; + break; + case ORIGIN_LOCATION: + mRejectedLocationChanges += 1; + break; + default: + mRejectedUnknownChanges += 1; + break; + } + } + } + + @Override + public void process(TimeZoneChangeEvent changeEvent) { + final TimeZoneChangeRecord trackedChangeEvent; + + synchronized (mTimeZoneChangeRecord) { + fixPotentialHistoryCorruption(changeEvent); + + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + int changeEventId = mNextChangeEventId.getAndIncrement(); + trackedChangeEvent = new TimeZoneChangeRecord(changeEventId, changeEvent); + + if (isManualChangeTrackingSupported()) { + // Time-based heuristic for "user is undoing a mistake made by the time zone + // detector". + if (lastTimeZoneChangeRecord != null + && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent(); + + if (shouldRejectChangeEvent(changeEvent, lastChangeEvent)) { + markChangeAsRejected(lastTimeZoneChangeRecord.getId(), + changeEvent.getUserId(), SIGNAL_TYPE_HEURISTIC); + } + } + + // Schedule a callback for the new time zone so that we can implement "user accepted + // the change because they didn't revert it" + scheduleChangeAcceptedHeuristicCallback(trackedChangeEvent, AUTO_REVERT_THRESHOLD); + } + + if (lastTimeZoneChangeRecord != null + && lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + } + + if (changeEvent.getOrigin() == ORIGIN_MANUAL) { + trackedChangeEvent.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + } + + mTimeZoneChangeRecord.set(trackedChangeEvent); + } + + if (areNotificationsEnabled()) { + int currentUserId; + synchronized (mConfigurationLock) { + currentUserId = mConfigurationInternal.getUserId(); + } + + if (changeEvent.getOrigin() == ORIGIN_MANUAL) { + // Just clear any existing notification. + clearNotificationForUser(currentUserId); + } else { + notifyOfTimeZoneChange(currentUserId, trackedChangeEvent); + } + } + } + + /** + * Checks if the history of time zone change events is corrupted and fixes it, if needed + * + * <p>The history of changes is considered corrupted if a transition is missing. That is, if + * {@code events[i-1].newTimeZoneId != events[i].oldTimeZoneId}. In that case, a "synthetic" + * event is added to the history to bridge the gap between the last reported time zone ID and + * the time zone ID that the new event is replacing. + * + * <p>Note: we are not expecting this method to be required often (if ever) but in the + * eventuality that an event gets lost, we want to keep the history coherent. + */ + @GuardedBy("mTimeZoneChangeRecord") + private void fixPotentialHistoryCorruption(TimeZoneChangeEvent changeEvent) { + TimeZoneChangeRecord lastTimeZoneChangeRecord = mTimeZoneChangeRecord.get(); + + if (lastTimeZoneChangeRecord != null) { + // The below block takes care of the case where we are missing record(s) of time + // zone changes + TimeZoneChangeEvent lastChangeEvent = lastTimeZoneChangeRecord.getEvent(); + if (!changeEvent.getOldZoneId().equals(lastChangeEvent.getNewZoneId())) { + int changeEventId = mNextChangeEventId.getAndIncrement(); + TimeZoneChangeEvent syntheticChangeEvent = new TimeZoneChangeEvent( + mEnvironment.elapsedRealtimeMillis(), mEnvironment.currentTimeMillis(), + ORIGIN_UNKNOWN, UserHandle.USER_NULL, lastChangeEvent.getNewZoneId(), + changeEvent.getOldZoneId(), 0, "Synthetic"); + TimeZoneChangeRecord syntheticTrackedChangeEvent = + new TimeZoneChangeRecord(changeEventId, syntheticChangeEvent); + syntheticTrackedChangeEvent.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeRecord.set(syntheticTrackedChangeEvent); + + // Housekeeping for the last reported time zone change: try to ensure it has + // a status too. + if (lastTimeZoneChangeRecord.getStatus() == STATUS_UNKNOWN) { + lastTimeZoneChangeRecord.setStatus(STATUS_SUPERSEDED, SIGNAL_TYPE_NONE); + } + } + } + } + + private static boolean shouldRejectChangeEvent(TimeZoneChangeEvent changeEvent, + TimeZoneChangeEvent lastChangeEvent) { + return changeEvent.getOrigin() == ORIGIN_MANUAL + && lastChangeEvent.getOrigin() != ORIGIN_MANUAL + && (changeEvent.getElapsedRealtimeMillis() + - lastChangeEvent.getElapsedRealtimeMillis() < AUTO_REVERT_THRESHOLD); + } + + private void scheduleChangeAcceptedHeuristicCallback( + TimeZoneChangeRecord trackedChangeEvent, + @DurationMillisLong long delayMillis) { + mHandler.postDelayed( + () -> changeAcceptedTimeHeuristicCallback(trackedChangeEvent.getId()), delayMillis); + } + + private void changeAcceptedTimeHeuristicCallback(int changeEventId) { + if (isManualChangeTrackingSupported()) { + int currentUserId = mActivityManagerInternal.getCurrentUserId(); + markChangeAsAccepted(changeEventId, currentUserId, SIGNAL_TYPE_HEURISTIC); + } + } + + private void clearNotificationForUser(@UserIdInt int userId) { + mNotificationManager.cancelAsUser(NOTIFICATION_TAG, TZ_CHANGE_NOTIFICATION_ID, + UserHandle.of(userId)); + } + + private void notifyOfTimeZoneChange(@UserIdInt int userId, + TimeZoneChangeRecord trackedChangeEvent) { + TimeZoneChangeEvent changeEvent = trackedChangeEvent.getEvent(); + + if (!Flags.datetimeNotifications() || !areNotificationsEnabled()) { + return; + } + + TimeZone oldTimeZone = TimeZone.getTimeZone(changeEvent.getOldZoneId()); + TimeZone newTimeZone = TimeZone.getTimeZone(changeEvent.getNewZoneId()); + long unixEpochTimeMillis = changeEvent.getUnixEpochTimeMillis(); + boolean hasOffsetChanged = newTimeZone.getOffset(unixEpochTimeMillis) + == oldTimeZone.getOffset(unixEpochTimeMillis); + + if (hasOffsetChanged) { + // If the time zone ID changes but not the offset, we do not send a notification to + // the user. This is to prevent spamming users and reduce the number of notification + // we send overall. + Log.d(TAG, "The time zone ID has changed but the offset remains the same."); + return; + } + + final CharSequence title = mRes.getString(R.string.time_zone_change_notification_title); + final CharSequence body = getNotificationBody(newTimeZone, unixEpochTimeMillis); + + final Intent clickNotificationIntent = new Intent(ACTION_DATE_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + final Intent clearNotificationIntent = new Intent(ACTION_NOTIFICATION_DELETED) + .putExtra(NOTIFICATION_INTENT_EXTRA_USER_ID, userId) + .putExtra(NOTIFICATION_INTENT_EXTRA_CHANGE_ID, trackedChangeEvent.getId()); + + Notification notification = new Notification.Builder(mContext, + SystemNotificationChannels.TIME) + .setSmallIcon(R.drawable.btn_clock_material) + .setStyle(new Notification.BigTextStyle().bigText(body)) + .setOnlyAlertOnce(true) + .setColor(mContext.getColor(R.color.system_notification_accent_color)) + .setTicker(title) + .setContentTitle(title) + .setContentText(body) + .setContentIntent(PendingIntent.getActivityAsUser( + mContext, + /* requestCode= */ 0, + clickNotificationIntent, + /* flags= */ FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + /* options= */ null, + UserHandle.of(userId))) + .setDeleteIntent(PendingIntent.getBroadcast( + mContext, + /* requestCode= */ 0, + clearNotificationIntent, + /* flags= */ FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)) + .setAutoCancel(true) // auto-clear notification on selection + .build(); + + mNotificationManager.notifyAsUser(NOTIFICATION_TAG, + TZ_CHANGE_NOTIFICATION_ID, notification, UserHandle.of(userId)); + } + + private CharSequence getNotificationBody(TimeZone newTimeZone, long unixEpochTimeMillis) { + DateFormat timeFormat = SimpleDateFormat.getInstanceForSkeleton("zzzz"); + DateFormat offsetFormat = SimpleDateFormat.getInstanceForSkeleton("ZZZZ"); + + String newTime = formatInZone(timeFormat, newTimeZone, unixEpochTimeMillis); + String newOffset = formatInZone(offsetFormat, newTimeZone, unixEpochTimeMillis); + + return mRes.getString(R.string.time_zone_change_notification_body, newTime, newOffset); + } + + private static String formatInZone(DateFormat timeFormat, TimeZone timeZone, + long unixEpochTimeMillis) { + timeFormat.setTimeZone(timeZone); + return timeFormat.format(unixEpochTimeMillis); + } + + @Override + public void dump(IndentingPrintWriter pw) { + synchronized (mConfigurationLock) { + pw.println("currentUserId=" + mConfigurationInternal.getUserId()); + pw.println("notificationsEnabledBehavior=" + + mConfigurationInternal.getNotificationsEnabledBehavior()); + pw.println("notificationTrackingSupported=" + + mConfigurationInternal.isNotificationTrackingSupported()); + pw.println("manualChangeTrackingSupported=" + + mConfigurationInternal.isManualChangeTrackingSupported()); + } + + pw.println("mAcceptedLocationChanges=" + mAcceptedLocationChanges); + pw.println("mAcceptedManualChanges=" + mAcceptedManualChanges); + pw.println("mAcceptedTelephonyChanges=" + mAcceptedTelephonyChanges); + pw.println("mAcceptedUnknownChanges=" + mAcceptedUnknownChanges); + pw.println("mRejectedLocationChanges=" + mRejectedLocationChanges); + pw.println("mRejectedTelephonyChanges=" + mRejectedTelephonyChanges); + pw.println("mRejectedUnknownChanges=" + mRejectedUnknownChanges); + pw.println("mNextChangeEventId=" + mNextChangeEventId); + + pw.println("mTimeZoneChangeRecord:"); + pw.increaseIndent(); + synchronized (mTimeZoneChangeRecord) { + mTimeZoneChangeRecord.dump(pw); + } + pw.decreaseIndent(); + } + + @VisibleForTesting + static class TimeZoneChangeRecord { + + private final int mId; + private final TimeZoneChangeEvent mEvent; + private @TimeZoneChangeStatus int mStatus = STATUS_UNKNOWN; + private @SignalType int mSignalType = SIGNAL_TYPE_UNKNOWN; + + TimeZoneChangeRecord(int id, TimeZoneChangeEvent event) { + mId = id; + mEvent = Objects.requireNonNull(event); + } + + public int getId() { + return mId; + } + + public @TimeZoneChangeStatus int getStatus() { + return mStatus; + } + + public void setAccepted(int signalType) { + setStatus(STATUS_ACCEPTED, signalType); + } + + public void setRejected(int signalType) { + setStatus(STATUS_REJECTED, signalType); + } + + public void setStatus(@TimeZoneChangeStatus int status, @SignalType int signalType) { + mStatus = status; + mSignalType = signalType; + } + + public TimeZoneChangeEvent getEvent() { + return mEvent; + } + + @Override + public String toString() { + return "TrackedTimeZoneChangeEvent{" + + "mId=" + mId + + ", mEvent=" + mEvent + + ", mStatus=" + mStatus + + ", mSignalType=" + mSignalType + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeZoneChangeRecord that) { + return mId == that.mId + && mEvent.equals(that.mEvent) + && mStatus == that.mStatus + && mSignalType == that.mSignalType; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mEvent, mStatus, mSignalType); + } + } + + @VisibleForTesting + TimeZoneChangeRecord getLastTimeZoneChangeRecord() { + synchronized (mTimeZoneChangeRecord) { + return mTimeZoneChangeRecord.get(); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java index f1248a3f5f0e..d809fc6b6eea 100644 --- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessorImpl.java @@ -68,7 +68,11 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT, ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE, ServerFlags.KEY_TIME_ZONE_DETECTOR_AUTO_DETECTION_ENABLED_DEFAULT, - ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED + ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED ); /** @@ -100,11 +104,16 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Nullable private static ServiceConfigAccessor sInstance; - @NonNull private final Context mContext; - @NonNull private final ServerFlags mServerFlags; - @NonNull private final ContentResolver mCr; - @NonNull private final UserManager mUserManager; - @NonNull private final LocationManager mLocationManager; + @NonNull + private final Context mContext; + @NonNull + private final ServerFlags mServerFlags; + @NonNull + private final ContentResolver mCr; + @NonNull + private final UserManager mUserManager; + @NonNull + private final LocationManager mLocationManager; @GuardedBy("this") @NonNull @@ -193,6 +202,9 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { contentResolver.registerContentObserver( Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE_EXPLICIT), true, contentObserver); + contentResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.TIME_ZONE_NOTIFICATIONS), true, + contentObserver); // Add async callbacks for user scoped location settings being changed. contentResolver.registerContentObserver( @@ -331,6 +343,14 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { setGeoDetectionEnabledSettingIfRequired(userId, geoDetectionEnabledSetting); } } + + if (areNotificationsSupported()) { + if (requestedConfigurationUpdates.hasIsNotificationsEnabled()) { + setNotificationsEnabledSetting( + requestedConfigurationUpdates.areNotificationsEnabled()); + } + setNotificationsEnabledIfRequired(newConfiguration.areNotificationsEnabled()); + } } @Override @@ -348,6 +368,10 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { .setUserConfigAllowed(isUserConfigAllowed(userId)) .setLocationEnabledSetting(getLocationEnabledSetting(userId)) .setGeoDetectionEnabledSetting(getGeoDetectionEnabledSetting(userId)) + .setNotificationsSupported(areNotificationsSupported()) + .setNotificationsEnabledSetting(getNotificationsEnabledSetting()) + .setNotificationsTrackingSupported(isNotificationTrackingSupported()) + .setManualChangeTrackingSupported(isManualChangeTrackingSupported()) .build(); } @@ -421,6 +445,49 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { } } + private boolean areNotificationsSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneNotificationsSupported)); + } + + private boolean isNotificationTrackingSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_TRACKING_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneNotificationsTrackingSupported)); + } + + private boolean isManualChangeTrackingSupported() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_MANUAL_CHANGE_TRACKING_SUPPORTED, + getConfigBoolean(R.bool.config_enableTimeZoneManualChangeTrackingSupported)); + } + + private boolean getNotificationsEnabledSetting() { + final boolean notificationsEnabledByDefault = areNotificationsEnabledByDefault(); + return Settings.Global.getInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, + (notificationsEnabledByDefault ? 1 : 0) /* defaultValue */) != 0; + } + + private boolean areNotificationsEnabledByDefault() { + return mServerFlags.getBoolean( + ServerFlags.KEY_TIME_ZONE_NOTIFICATIONS_ENABLED_DEFAULT, true); + } + + private void setNotificationsEnabledSetting(boolean enabled) { + Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0); + } + + private void setNotificationsEnabledIfRequired(boolean enabled) { + // This check is racey, but the whole settings update process is racey. This check prevents + // a ConfigurationChangeListener callback triggering due to ContentObserver's still + // triggering *sometimes* for no-op updates. Because callbacks are async this is necessary + // for stable behavior during tests. + if (getNotificationsEnabledSetting() != enabled) { + Settings.Global.putInt(mCr, Settings.Global.TIME_ZONE_NOTIFICATIONS, enabled ? 1 : 0); + } + } + @Override public void addLocationTimeZoneManagerConfigListener( @NonNull StateChangeListener listener) { @@ -441,8 +508,7 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { @Override public boolean isGeoTimeZoneDetectionFeatureSupportedInConfig() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_enableGeolocationTimeZoneDetection); + return getConfigBoolean(R.bool.config_enableGeolocationTimeZoneDetection); } @Override @@ -660,8 +726,7 @@ public final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { private boolean isTelephonyFallbackSupported() { return mServerFlags.getBoolean( ServerFlags.KEY_TIME_ZONE_DETECTOR_TELEPHONY_FALLBACK_SUPPORTED, - getConfigBoolean( - com.android.internal.R.bool.config_supportTelephonyTimeZoneFallback)); + getConfigBoolean(R.bool.config_supportTelephonyTimeZoneFallback)); } private boolean getConfigBoolean(int providerEnabledConfigId) { diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java new file mode 100644 index 000000000000..d340ed470591 --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneChangeListener.java @@ -0,0 +1,130 @@ +/* + * 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.timezonedetector; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.util.IndentingPrintWriter; + +import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.timezonedetector.TimeZoneDetectorStrategy.Origin; + +import java.util.Objects; + +public interface TimeZoneChangeListener { + + /** Record a time zone change. */ + void process(TimeZoneChangeEvent event); + + /** Dump internal state. */ + void dump(IndentingPrintWriter ipw); + + class TimeZoneChangeEvent { + + private final @ElapsedRealtimeLong long mElapsedRealtimeMillis; + private final @CurrentTimeMillisLong long mUnixEpochTimeMillis; + private final @Origin int mOrigin; + private final @UserIdInt int mUserId; + private final String mOldZoneId; + private final String mNewZoneId; + private final @TimeZoneConfidence int mNewConfidence; + private final String mCause; + + public TimeZoneChangeEvent(@ElapsedRealtimeLong long elapsedRealtimeMillis, + @CurrentTimeMillisLong long unixEpochTimeMillis, + @Origin int origin, @UserIdInt int userId, @NonNull String oldZoneId, + @NonNull String newZoneId, int newConfidence, @NonNull String cause) { + mElapsedRealtimeMillis = elapsedRealtimeMillis; + mUnixEpochTimeMillis = unixEpochTimeMillis; + mOrigin = origin; + mUserId = userId; + mOldZoneId = Objects.requireNonNull(oldZoneId); + mNewZoneId = Objects.requireNonNull(newZoneId); + mNewConfidence = newConfidence; + mCause = Objects.requireNonNull(cause); + } + + public @ElapsedRealtimeLong long getElapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + public @CurrentTimeMillisLong long getUnixEpochTimeMillis() { + return mUnixEpochTimeMillis; + } + + public @Origin int getOrigin() { + return mOrigin; + } + + /** + * The ID of the user that triggered the change. + * + * <p>If automatic time zone is turned on, the user ID returned is the system's user id. + */ + public @UserIdInt int getUserId() { + return mUserId; + } + + public String getOldZoneId() { + return mOldZoneId; + } + + public String getNewZoneId() { + return mNewZoneId; + } + + @Override + public String toString() { + return "TimeZoneChangeEvent{" + + "mElapsedRealtimeMillis=" + mElapsedRealtimeMillis + + ", mUnixEpochTimeMillis=" + mUnixEpochTimeMillis + + ", mOrigin=" + mOrigin + + ", mUserId=" + mUserId + + ", mOldZoneId='" + mOldZoneId + '\'' + + ", mNewZoneId='" + mNewZoneId + '\'' + + ", mNewConfidence=" + mNewConfidence + + ", mCause='" + mCause + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TimeZoneChangeEvent that) { + return mElapsedRealtimeMillis == that.mElapsedRealtimeMillis + && mUnixEpochTimeMillis == that.mUnixEpochTimeMillis + && mOrigin == that.mOrigin + && mUserId == that.mUserId + && Objects.equals(mOldZoneId, that.mOldZoneId) + && Objects.equals(mNewZoneId, that.mNewZoneId) + && mNewConfidence == that.mNewConfidence + && Objects.equals(mCause, that.mCause); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mElapsedRealtimeMillis, mUnixEpochTimeMillis, mOrigin, mUserId, + mOldZoneId, mNewZoneId, mNewConfidence, mCause); + } + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java index d914544566ff..af02ad88ad6a 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorService.java @@ -18,6 +18,7 @@ package com.android.server.timezonedetector; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.time.ITimeZoneDetectorListener; import android.app.time.TimeZoneCapabilitiesAndConfig; @@ -73,6 +74,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub } @Override + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") public void onStart() { // Obtain / create the shared dependencies. Context context = getContext(); @@ -81,7 +83,7 @@ public final class TimeZoneDetectorService extends ITimeZoneDetectorService.Stub ServiceConfigAccessor serviceConfigAccessor = ServiceConfigAccessorImpl.getInstance(context); TimeZoneDetectorStrategy timeZoneDetectorStrategy = - TimeZoneDetectorStrategyImpl.create(handler, serviceConfigAccessor); + TimeZoneDetectorStrategyImpl.create(context, handler, serviceConfigAccessor); DeviceActivityMonitor deviceActivityMonitor = DeviceActivityMonitorImpl.create(context, handler); diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index 37e67c921634..8cfbe9daa970 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -15,6 +15,7 @@ */ package com.android.server.timezonedetector; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.time.TimeZoneCapabilitiesAndConfig; @@ -24,6 +25,11 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.util.IndentingPrintWriter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + /** * The interface for the class that is responsible for setting the time zone on a device, used by * {@link TimeZoneDetectorService} and {@link TimeZoneDetectorInternal}. @@ -97,6 +103,22 @@ import android.util.IndentingPrintWriter; * @hide */ public interface TimeZoneDetectorStrategy extends Dumpable { + @IntDef({ ORIGIN_UNKNOWN, ORIGIN_MANUAL, ORIGIN_TELEPHONY, ORIGIN_LOCATION }) + @Retention(RetentionPolicy.SOURCE) + @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER }) + @interface Origin {} + + /** Used when the origin of the time zone value cannot be inferred. */ + @Origin int ORIGIN_UNKNOWN = 0; + + /** Used when a time zone value originated from a user / manual settings. */ + @Origin int ORIGIN_MANUAL = 1; + + /** Used when a time zone value originated from a telephony signal. */ + @Origin int ORIGIN_TELEPHONY = 2; + + /** Used when a time zone value originated from a location signal. */ + @Origin int ORIGIN_LOCATION = 3; /** * Adds a listener that will be triggered when something changes that could affect the result diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java index dddb46f80724..042d81ab6885 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategyImpl.java @@ -25,9 +25,9 @@ import static android.app.timezonedetector.TelephonyTimeZoneSuggestion.QUALITY_S import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH; import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; -import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.time.DetectorStatusTypes; import android.app.time.LocationTimeZoneAlgorithmStatus; @@ -39,17 +39,20 @@ import android.app.time.TimeZoneDetectorStatus; import android.app.time.TimeZoneState; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; +import android.content.Context; import android.os.Handler; +import android.os.SystemClock; import android.os.TimestampedValue; +import android.os.UserHandle; import android.util.IndentingPrintWriter; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.flags.Flags; import com.android.server.timezonedetector.ConfigurationInternal.DetectionMode; -import java.io.PrintWriter; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -62,60 +65,13 @@ import java.util.Objects; */ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrategy { - /** - * Used by {@link TimeZoneDetectorStrategyImpl} to interact with device state besides that - * available from {@link #mServiceConfigAccessor}. It can be faked for testing. - */ - @VisibleForTesting - public interface Environment { - - /** - * Returns the device's currently configured time zone. May return an empty string. - */ - @NonNull String getDeviceTimeZone(); - - /** - * Returns the confidence of the device's current time zone. - */ - @TimeZoneConfidence int getDeviceTimeZoneConfidence(); - - /** - * Sets the device's time zone, associated confidence, and records a debug log entry. - */ - void setDeviceTimeZoneAndConfidence( - @NonNull String zoneId, @TimeZoneConfidence int confidence, - @NonNull String logInfo); - - /** - * Returns the time according to the elapsed realtime clock, the same as {@link - * android.os.SystemClock#elapsedRealtime()}. - */ - @ElapsedRealtimeLong - long elapsedRealtimeMillis(); - - /** - * Adds a standalone entry to the time zone debug log. - */ - void addDebugLogEntry(@NonNull String logMsg); - - /** - * Dumps the time zone debug log to the supplied {@link PrintWriter}. - */ - void dumpDebugLog(PrintWriter printWriter); - - /** - * Requests that the supplied runnable be invoked asynchronously. - */ - void runAsync(@NonNull Runnable runnable); - } - private static final String LOG_TAG = TimeZoneDetectorService.TAG; private static final boolean DBG = TimeZoneDetectorService.DBG; /** * The abstract score for an empty or invalid telephony suggestion. * - * Used to score telephony suggestions where there is no zone. + * <p>Used to score telephony suggestions where there is no zone. */ @VisibleForTesting public static final int TELEPHONY_SCORE_NONE = 0; @@ -123,11 +79,11 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a low quality telephony suggestion. * - * Used to score suggestions where: - * The suggested zone ID is one of several possibilities, and the possibilities have different - * offsets. + * <p>Used to score suggestions where: + * The suggested zone ID is one of several possibilities, + * and the possibilities have different offsets. * - * You would have to be quite desperate to want to use this choice. + * <p>You would have to be quite desperate to want to use this choice. */ @VisibleForTesting public static final int TELEPHONY_SCORE_LOW = 1; @@ -135,7 +91,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a medium quality telephony suggestion. * - * Used for: + * <p>Used for: * The suggested zone ID is one of several possibilities but at least the possibilities have the * same offset. Users would get the correct time but for the wrong reason. i.e. their device may * switch to DST at the wrong time and (for example) their calendar events. @@ -146,7 +102,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a high quality telephony suggestion. * - * Used for: + * <p>Used for: * The suggestion was for one zone ID and the answer was unambiguous and likely correct given * the info available. */ @@ -156,7 +112,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * The abstract score for a highest quality telephony suggestion. * - * Used for: + * <p>Used for: * Suggestions that must "win" because they constitute test or emulator zone ID. */ @VisibleForTesting @@ -206,7 +162,15 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat private final ServiceConfigAccessor mServiceConfigAccessor; @GuardedBy("this") - @NonNull private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); + @NonNull + private final List<StateChangeListener> mStateChangeListeners = new ArrayList<>(); + + /** + * A component adjunct to the detection behavior that tracks time zone changes and implements + * behavior associated with time zone changes. + */ + @NonNull + private final TimeZoneChangeListener mChangeTracker; /** * A snapshot of the current detector status. A local copy is cached because it is relatively @@ -244,19 +208,26 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat /** * Creates a new instance of {@link TimeZoneDetectorStrategyImpl}. */ + @RequiresPermission("android.permission.INTERACT_ACROSS_USERS_FULL") public static TimeZoneDetectorStrategyImpl create( - @NonNull Handler handler, @NonNull ServiceConfigAccessor serviceConfigAccessor) { - + @NonNull Context context, @NonNull Handler handler, + @NonNull ServiceConfigAccessor serviceConfigAccessor) { Environment environment = new EnvironmentImpl(handler); - return new TimeZoneDetectorStrategyImpl(serviceConfigAccessor, environment); + TimeZoneChangeListener changeEventTracker = + NotifyingTimeZoneChangeListener.create(handler, context, serviceConfigAccessor, + environment); + return new TimeZoneDetectorStrategyImpl( + serviceConfigAccessor, environment, changeEventTracker); } @VisibleForTesting public TimeZoneDetectorStrategyImpl( @NonNull ServiceConfigAccessor serviceConfigAccessor, - @NonNull Environment environment) { + @NonNull Environment environment, + @NonNull TimeZoneChangeListener changeEventTracker) { mEnvironment = Objects.requireNonNull(environment); mServiceConfigAccessor = Objects.requireNonNull(serviceConfigAccessor); + mChangeTracker = Objects.requireNonNull(changeEventTracker); // Start with telephony fallback enabled. mTelephonyTimeZoneFallbackEnabled = @@ -468,7 +439,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // later disables automatic time zone detection. mLatestManualSuggestion.set(suggestion); - setDeviceTimeZoneIfRequired(timeZoneId, cause); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_MANUAL, userId, cause); return true; } @@ -685,7 +656,7 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // GeolocationTimeZoneSuggestion has no measure of quality. We assume all suggestions are // reliable. - String zoneId; + String timeZoneId; // Introduce bias towards the device's current zone when there are multiple zone suggested. String deviceTimeZone = mEnvironment.getDeviceTimeZone(); @@ -694,11 +665,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat Slog.d(LOG_TAG, "Geo tz suggestion contains current device time zone. Applying bias."); } - zoneId = deviceTimeZone; + timeZoneId = deviceTimeZone; } else { - zoneId = zoneIds.get(0); + timeZoneId = zoneIds.get(0); } - setDeviceTimeZoneIfRequired(zoneId, detectionReason); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_LOCATION, UserHandle.USER_SYSTEM, + detectionReason); return true; } @@ -779,8 +751,8 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time // zone ID. - String zoneId = bestTelephonySuggestion.suggestion.getZoneId(); - if (zoneId == null) { + String timeZoneId = bestTelephonySuggestion.suggestion.getZoneId(); + if (timeZoneId == null) { Slog.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason); @@ -790,11 +762,12 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat String cause = "Found good suggestion:" + " bestTelephonySuggestion=" + bestTelephonySuggestion + ", detectionReason=" + detectionReason; - setDeviceTimeZoneIfRequired(zoneId, cause); + setDeviceTimeZoneIfRequired(timeZoneId, ORIGIN_TELEPHONY, UserHandle.USER_SYSTEM, cause); } @GuardedBy("this") - private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @NonNull String cause) { + private void setDeviceTimeZoneIfRequired(@NonNull String newZoneId, @Origin int origin, + @UserIdInt int userId, @NonNull String cause) { String currentZoneId = mEnvironment.getDeviceTimeZone(); // All manual and automatic suggestions are considered high confidence as low-quality // suggestions are not currently passed on. @@ -823,6 +796,17 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat Slog.d(LOG_TAG, logInfo); } mEnvironment.setDeviceTimeZoneAndConfidence(newZoneId, newConfidence, logInfo); + + if (Flags.datetimeNotifications()) { + // Record the fact that the time zone was changed so that it can be tracked, i.e. + // whether the device / user sticks with it. + TimeZoneChangeListener.TimeZoneChangeEvent changeEvent = + new TimeZoneChangeListener.TimeZoneChangeEvent( + SystemClock.elapsedRealtime(), System.currentTimeMillis(), origin, + userId, + currentZoneId, newZoneId, newConfidence, cause); + mChangeTracker.process(changeEvent); + } } @GuardedBy("this") @@ -937,6 +921,14 @@ public final class TimeZoneDetectorStrategyImpl implements TimeZoneDetectorStrat ipw.increaseIndent(); // level 2 mTelephonySuggestionsBySlotIndex.dump(ipw); ipw.decreaseIndent(); // level 2 + + if (Flags.datetimeNotifications()) { + ipw.println("Time zone change tracker:"); + ipw.increaseIndent(); // level 2 + mChangeTracker.dump(ipw); + ipw.decreaseIndent(); // level 2 + } + ipw.decreaseIndent(); // level 1 } diff --git a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java index 54ae047a2858..0b676ff7d590 100644 --- a/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java +++ b/services/core/java/com/android/server/vibrator/BasicToPwleSegmentAdapter.java @@ -100,6 +100,11 @@ final class BasicToPwleSegmentAdapter implements VibrationSegmentsAdapter { } VibratorInfo.FrequencyProfile frequencyProfile = info.getFrequencyProfile(); + if (frequencyProfile.isEmpty()) { + // The frequency profile has an invalid frequency range, so keep the segments unchanged. + return repeatIndex; + } + float[] frequenciesHz = frequencyProfile.getFrequenciesHz(); float[] accelerationsGs = frequencyProfile.getOutputAccelerationsGs(); diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 89c7a3d89a54..6f308aa9b706 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1631,7 +1631,7 @@ class ActivityMetricsLogger { int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION; if (isAppCompateStateChangedToLetterboxed(state)) { - positionToLog = activity.mAppCompatController.getAppCompatReachabilityOverrides() + positionToLog = activity.mAppCompatController.getReachabilityOverrides() .getLetterboxPositionForLogging(); } FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 29f1f93a844f..e52fbbfde49f 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -90,10 +90,6 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_VIA_SDK_VER import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; -import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_METADATA; -import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_OVERRIDE; -import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_METADATA; -import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_OVERRIDE; import static android.content.res.Configuration.ASSETS_SEQ_UNDEFINED; import static android.content.res.Configuration.EMPTY; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; @@ -232,6 +228,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; @@ -1271,8 +1268,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A pw.println(prefix + "manifestMinAspectRatio=" + info.getManifestMinAspectRatio()); } - pw.println(prefix + "supportsSizeChanges=" - + ActivityInfo.sizeChangesSupportModeToString(supportsSizeChanges())); + pw.println( + prefix + "supportsSizeChanges=" + ActivityInfo.sizeChangesSupportModeToString( + mAppCompatController.getSizeCompatModePolicy().supportsSizeChanges())); if (info.configChanges != 0) { pw.println(prefix + "configChanges=0x" + Integer.toHexString(info.configChanges)); } @@ -2814,9 +2812,27 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A attachStartingSurfaceToAssociatedTask(); } + /** + * If the device is locked and the app does not request showWhenLocked, + * defer removing the starting window until the transition is complete. + * This prevents briefly appearing the app context and causing secure concern. + */ + void deferStartingWindowRemovalForKeyguardUnoccluding() { + if (mStartingData.mRemoveAfterTransaction != AFTER_TRANSITION_FINISH + && isKeyguardLocked() && !canShowWhenLockedInner(this) && !isVisibleRequested() + && mTransitionController.inTransition(this)) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSITION_FINISH; + } + } + void removeStartingWindow() { boolean prevEligibleForLetterboxEducation = isEligibleForLetterboxEducation(); + if (mStartingData != null + && mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) { + return; + } + if (transferSplashScreenIfNeeded()) { return; } @@ -4261,7 +4277,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void finishRelaunching() { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(false); mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this); @@ -4655,6 +4671,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A tStartingWindow.mToken = this; tStartingWindow.mActivityRecord = this; + if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSITION_FINISH) { + mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; + } if (mStartingData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) { // The removal of starting window should wait for window drawn of current // activity. @@ -6559,7 +6578,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.mStoppingActivities.remove(this); if (getDisplayArea().allResumedActivitiesComplete()) { // Construct the compat environment at a relatively stable state if needed. - mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets(); + mAppCompatController.getSizeCompatModePolicy().updateAppCompatDisplayInsets(); mRootWindowContainer.executeAppTransitionForAllDisplay(); } @@ -8125,10 +8144,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (task != null && requestedOrientation == SCREEN_ORIENTATION_BEHIND) { // We use Task here because we want to be consistent with what happens in // multi-window mode where other tasks orientations are ignored. - final ActivityRecord belowCandidate = task.getActivity( - a -> a.canDefineOrientationForActivitiesAbove() /* callback */, - this /* boundary */, false /* includeBoundary */, - true /* traverseTopToBottom */); + final ActivityRecord belowCandidate = task.getActivityBelowForDefiningOrientation(this); if (belowCandidate != null) { return belowCandidate.getRequestedConfigurationOrientation(forDisplay); } @@ -8203,7 +8219,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A != getRequestedConfigurationOrientation(false /*forDisplay */)) { // Do not change the requested configuration now, because this will be done when setting // the orientation below with the new mAppCompatDisplayInsets - mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatModeAttributes(); + mAppCompatController.getSizeCompatModePolicy().clearSizeCompatModeAttributes(); } ProtoLog.v(WM_DEBUG_ORIENTATION, "Setting requested orientation %s for %s", @@ -8222,7 +8238,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mLastReportedConfiguration.getMergedConfiguration())) { ensureActivityConfiguration(false /* ignoreVisibility */); if (mPendingRelaunchCount > originalRelaunchingCount) { - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .setRelaunchingAfterRequestedOrientationChanged(true); } if (mTransitionController.inPlayingTransition(this)) { @@ -8347,7 +8363,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Nullable AppCompatDisplayInsets getAppCompatDisplayInsets() { - return mAppCompatController.getAppCompatSizeCompatModePolicy().getAppCompatDisplayInsets(); + return mAppCompatController.getSizeCompatModePolicy().getAppCompatDisplayInsets(); } /** @@ -8355,31 +8371,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * density than its parent or its bounds don't fit in parent naturally. */ boolean inSizeCompatMode() { - final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController - .getAppCompatSizeCompatModePolicy(); - if (scmPolicy.isInSizeCompatModeForBounds()) { - return true; - } - if (getAppCompatDisplayInsets() == null || !shouldCreateAppCompatDisplayInsets() - // The orientation is different from parent when transforming. - || isFixedRotationTransforming()) { - return false; - } - final Rect appBounds = getConfiguration().windowConfiguration.getAppBounds(); - if (appBounds == null) { - // The app bounds hasn't been computed yet. - return false; - } - final WindowContainer parent = getParent(); - if (parent == null) { - // The parent of detached Activity can be null. - return false; - } - final Configuration parentConfig = parent.getConfiguration(); - // Although colorMode, screenLayout, smallestScreenWidthDp are also fixed, generally these - // fields should be changed with density and bounds, so here only compares the most - // significant field. - return parentConfig.densityDpi != getConfiguration().densityDpi; + return mAppCompatController.getSizeCompatModePolicy().inSizeCompatMode(); } /** @@ -8393,67 +8385,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * aspect ratio. */ boolean shouldCreateAppCompatDisplayInsets() { - if (mAppCompatController.getAppCompatAspectRatioOverrides().hasFullscreenOverride()) { - // If the user has forced the applications aspect ratio to be fullscreen, don't use size - // compatibility mode in any situation. The user has been warned and therefore accepts - // the risk of the application misbehaving. - return false; - } - switch (supportsSizeChanges()) { - case SIZE_CHANGES_SUPPORTED_METADATA: - case SIZE_CHANGES_SUPPORTED_OVERRIDE: - return false; - case SIZE_CHANGES_UNSUPPORTED_OVERRIDE: - return true; - default: - // Fall through - } - // Use root activity's info for tasks in multi-window mode, or fullscreen tasks in freeform - // task display areas, to ensure visual consistency across activity launches and exits in - // the same task. - final TaskDisplayArea tda = getTaskDisplayArea(); - if (inMultiWindowMode() || (tda != null && tda.inFreeformWindowingMode())) { - final ActivityRecord root = task != null ? task.getRootActivity() : null; - if (root != null && root != this && !root.shouldCreateAppCompatDisplayInsets()) { - // If the root activity doesn't use size compatibility mode, the activities above - // are forced to be the same for consistent visual appearance. - return false; - } - } - return !isResizeable() && (info.isFixedOrientation() || hasFixedAspectRatio()) - // The configuration of non-standard type should be enforced by system. - // {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD} is set when this activity is - // added to a task, but this function is called when resolving the launch params, at - // which point, the activity type is still undefined if it will be standard. - // For other non-standard types, the type is set in the constructor, so this should - // not be a problem. - && isActivityTypeStandardOrUndefined(); - } - - /** - * Returns whether the activity supports size changes. - */ - @ActivityInfo.SizeChangesSupportMode - private int supportsSizeChanges() { - final AppCompatResizeOverrides resizeOverrides = mAppCompatController.getResizeOverrides(); - if (resizeOverrides.shouldOverrideForceNonResizeApp()) { - return SIZE_CHANGES_UNSUPPORTED_OVERRIDE; - } - - if (info.supportsSizeChanges) { - return SIZE_CHANGES_SUPPORTED_METADATA; - } - - if (resizeOverrides.shouldOverrideForceResizeApp()) { - return SIZE_CHANGES_SUPPORTED_OVERRIDE; - } - - return SIZE_CHANGES_UNSUPPORTED_METADATA; + return mAppCompatController.getSizeCompatModePolicy().shouldCreateAppCompatDisplayInsets(); } @Override boolean hasSizeCompatBounds() { - return mAppCompatController.getAppCompatSizeCompatModePolicy().hasSizeCompatBounds(); + return mAppCompatController.getSizeCompatModePolicy().hasSizeCompatBounds(); } @Override @@ -8472,7 +8409,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override float getCompatScale() { // We need to invoke {#getCompatScale()} only if the CompatScale is not available. - return mAppCompatController.getAppCompatSizeCompatModePolicy() + return mAppCompatController.getSizeCompatModePolicy() .getCompatScaleIfAvailable(ActivityRecord.super::getCompatScale); } @@ -8499,7 +8436,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newParentConfiguration = mTmpConfig; } - mAppCompatController.getAppCompatAspectRatioPolicy().reset(); + mAppCompatController.getAspectRatioPolicy().reset(); mIsEligibleForFixedOrientationLetterbox = false; mResolveConfigHint.resolveTmpOverrides(mDisplayContent, newParentConfiguration, isFixedRotationTransforming()); @@ -8530,7 +8467,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds // are already calculated in resolveFixedOrientationConfiguration. // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. - if (!mAppCompatController.getAppCompatAspectRatioPolicy() + if (!mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio() && !mAppCompatController.getAppCompatAspectRatioOverrides() .hasFullscreenOverride()) { @@ -8538,7 +8475,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets(); final AppCompatSizeCompatModePolicy scmPolicy = - mAppCompatController.getAppCompatSizeCompatModePolicy(); + mAppCompatController.getSizeCompatModePolicy(); if (appCompatDisplayInsets != null) { scmPolicy.resolveSizeCompatModeConfiguration(newParentConfiguration, appCompatDisplayInsets, mTmpBounds); @@ -8567,7 +8504,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Fixed orientation letterboxing is possible on both large screen devices // with ignoreOrientationRequest enabled and on phones in split screen even with // ignoreOrientationRequest disabled. - && (mAppCompatController.getAppCompatAspectRatioPolicy() + && (mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio() // Limiting check for aspect ratio letterboxing to devices with enabled // ignoreOrientationRequest. This avoids affecting phones where apps may @@ -8576,7 +8513,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // accurate on phones shouldn't make the big difference and is expected // to be already well-tested by apps. || (isIgnoreOrientationRequest - && mAppCompatController.getAppCompatAspectRatioPolicy().isAspectRatioApplied()))) { + && mAppCompatController.getAspectRatioPolicy().isAspectRatioApplied()))) { // TODO(b/264034555): Use mDisplayContent to calculate smallestScreenWidthDp from all // rotations and only re-calculate if parent bounds have non-orientation size change. resolvedConfig.smallestScreenWidthDp = @@ -8688,7 +8625,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return mAppCompatController.getTransparentPolicy().getInheritedAppCompatState(); } final AppCompatSizeCompatModePolicy scmPolicy = mAppCompatController - .getAppCompatSizeCompatModePolicy(); + .getSizeCompatModePolicy(); if (scmPolicy.isInSizeCompatModeForBounds()) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; } @@ -8696,13 +8633,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // letterboxed for fixed orientation. Aspect ratio restrictions are also applied if // present. But this doesn't return true when the activity is letterboxed only because // of aspect ratio restrictions. - if (mAppCompatController.getAppCompatAspectRatioPolicy() - .isLetterboxedForFixedOrientationAndAspectRatio()) { + final AppCompatAspectRatioPolicy aspectRatioPolicy = + mAppCompatController.getAspectRatioPolicy(); + if (aspectRatioPolicy.isLetterboxedForFixedOrientationAndAspectRatio()) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; } // Letterbox for limited aspect ratio. - if (mAppCompatController.getAppCompatAspectRatioPolicy() - .isLetterboxedForAspectRatioOnly()) { + if (aspectRatioPolicy.isLetterboxedForAspectRatioOnly()) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; } @@ -8726,7 +8663,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } final AppCompatSizeCompatModePolicy scmPolicy = - mAppCompatController.getAppCompatSizeCompatModePolicy(); + mAppCompatController.getSizeCompatModePolicy(); final Rect screenResolvedBounds = scmPolicy.replaceResolvedBoundsIfNeeded(resolvedBounds); final Rect parentAppBounds = mResolveConfigHint.mParentAppBoundsOverride; final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds(); @@ -8744,7 +8681,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A navBarInsets = Insets.NONE; } final AppCompatReachabilityOverrides reachabilityOverrides = - mAppCompatController.getAppCompatReachabilityOverrides(); + mAppCompatController.getReachabilityOverrides(); // Horizontal position int offsetX = 0; if (parentBounds.width() != screenResolvedBoundsWidth) { @@ -8824,7 +8761,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final Configuration resolvedConfig = getResolvedOverrideConfiguration(); final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds(); final AppCompatSizeCompatModePolicy scmPolicy = - mAppCompatController.getAppCompatSizeCompatModePolicy(); + mAppCompatController.getSizeCompatModePolicy(); return scmPolicy.replaceResolvedBoundsIfNeeded(resolvedBounds); } @@ -8976,13 +8913,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A || orientationRespectedWithInsets)) { return; } - final AppCompatDisplayInsets mAppCompatDisplayInsets = getAppCompatDisplayInsets(); + final AppCompatDisplayInsets appCompatDisplayInsets = getAppCompatDisplayInsets(); final AppCompatSizeCompatModePolicy scmPolicy = - mAppCompatController.getAppCompatSizeCompatModePolicy(); + mAppCompatController.getSizeCompatModePolicy(); - if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance() - && mAppCompatDisplayInsets != null - && !mAppCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) { + if (appCompatDisplayInsets != null + && !appCompatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) { // App prefers to keep its original size. // If the size compat is from previous fixed orientation letterboxing, we may want to // have fixed orientation letterbox again, otherwise it will show the size compat @@ -9030,12 +8966,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final Rect prevResolvedBounds = new Rect(resolvedBounds); resolvedBounds.set(containingBounds); - mAppCompatController.getAppCompatAspectRatioPolicy() - .applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds, + final AppCompatAspectRatioPolicy aspectRatioPolicy = mAppCompatController + .getAspectRatioPolicy(); + + aspectRatioPolicy.applyDesiredAspectRatio(newParentConfig, parentBounds, resolvedBounds, containingBoundsWithInsets, containingBounds); - if (scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) { - mAppCompatDisplayInsets.getBoundsByRotation(mTmpBounds, + if (appCompatDisplayInsets != null) { + appCompatDisplayInsets.getBoundsByRotation(mTmpBounds, newParentConfig.windowConfiguration.getRotation()); if (resolvedBounds.width() != mTmpBounds.width() || resolvedBounds.height() != mTmpBounds.height()) { @@ -9058,10 +8996,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Calculate app bounds using fixed orientation bounds because they will be needed later // for comparison with size compat app bounds in {@link resolveSizeCompatModeConfiguration}. - mResolveConfigHint.mTmpCompatInsets = mAppCompatDisplayInsets; + mResolveConfigHint.mTmpCompatInsets = appCompatDisplayInsets; computeConfigByResolveHint(getResolvedOverrideConfiguration(), newParentConfig); - mAppCompatController.getAppCompatAspectRatioPolicy() - .setLetterboxBoundsForFixedOrientationAndAspectRatio(new Rect(resolvedBounds)); + aspectRatioPolicy.setLetterboxBoundsForFixedOrientationAndAspectRatio( + new Rect(resolvedBounds)); } /** @@ -9078,8 +9016,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Use tmp bounds to calculate aspect ratio so we can know whether the activity should use // restricted size (resolved bounds may be the requested override bounds). mTmpBounds.setEmpty(); - mAppCompatController.getAppCompatAspectRatioPolicy() - .applyAspectRatioForLetterbox(mTmpBounds, parentAppBounds, parentBounds); + final AppCompatAspectRatioPolicy aspectRatioPolicy = mAppCompatController + .getAspectRatioPolicy(); + aspectRatioPolicy.applyAspectRatioForLetterbox(mTmpBounds, parentAppBounds, parentBounds); // If the out bounds is not empty, it means the activity cannot fill parent's app bounds, // then they should be aligned later in #updateResolvedBoundsPosition() if (!mTmpBounds.isEmpty()) { @@ -9090,8 +9029,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // restrict, the bounds should be the requested override bounds. mResolveConfigHint.mTmpOverrideDisplayInfo = getFixedRotationTransformDisplayInfo(); computeConfigByResolveHint(resolvedConfig, newParentConfiguration); - mAppCompatController.getAppCompatAspectRatioPolicy() - .setLetterboxBoundsForAspectRatio(new Rect(resolvedBounds)); + aspectRatioPolicy.setLetterboxBoundsForAspectRatio(new Rect(resolvedBounds)); } } @@ -9100,7 +9038,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // TODO(b/268458693): Refactor configuration inheritance in case of translucent activities final Rect superBounds = super.getBounds(); final AppCompatSizeCompatModePolicy scmPolicy = - mAppCompatController.getAppCompatSizeCompatModePolicy(); + mAppCompatController.getSizeCompatModePolicy(); return mAppCompatController.getTransparentPolicy().findOpaqueNotFinishingActivityBelow() .map(ActivityRecord::getBounds) .orElseGet(() -> scmPolicy.getAppSizeCompatBoundsIfAvailable(superBounds)); @@ -9341,18 +9279,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * Returns the min aspect ratio of this activity. */ float getMinAspectRatio() { - return mAppCompatController.getAppCompatAspectRatioPolicy().getMinAspectRatio(); + return mAppCompatController.getAspectRatioPolicy().getMinAspectRatio(); } float getMaxAspectRatio() { - return mAppCompatController.getAppCompatAspectRatioPolicy().getMaxAspectRatio(); - } - - /** - * Returns true if the activity has maximum or minimum aspect ratio. - */ - private boolean hasFixedAspectRatio() { - return getMaxAspectRatio() != 0 || getMinAspectRatio() != 0; + return mAppCompatController.getAspectRatioPolicy().getMaxAspectRatio(); } /** @@ -9434,7 +9365,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mVisibleRequested) { // Calling from here rather than resolveOverrideConfiguration to ensure that this is // called after full config is updated in ConfigurationContainer#onConfigurationChanged. - mAppCompatController.getAppCompatSizeCompatModePolicy().updateAppCompatDisplayInsets(); + mAppCompatController.getSizeCompatModePolicy().updateAppCompatDisplayInsets(); } // Short circuit: if the two full configurations are equal (the common case), then there is @@ -9774,7 +9705,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Reset the existing override configuration so it can be updated according to the latest // configuration. - mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); if (!attachedToProcess()) { return; @@ -10217,7 +10148,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppCompatController.getAppCompatAspectRatioOverrides() .shouldOverrideMinAspectRatio()); proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP, - mAppCompatController.getAppCompatOrientationOverrides() + mAppCompatController.getOrientationOverrides() .shouldIgnoreOrientationRequestLoop()); proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP, mAppCompatController.getResizeOverrides().shouldOverrideForceResizeApp()); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 0ab2ffe3e298..bdbd0d15d982 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1964,7 +1964,6 @@ class ActivityStarter { if (mLastStartActivityRecord != null) { targetTaskTop.mLaunchSourceType = mLastStartActivityRecord.mLaunchSourceType; } - targetTaskTop.mTransitionController.collect(targetTaskTop); recordTransientLaunchIfNeeded(targetTaskTop); // Recycle the target task for this launch. startResult = diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index ef6f92317b2c..12c8f9ccac7c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2538,7 +2538,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { void wakeUp(int displayId, String reason) { mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_APPLICATION, - "android.server.am:TURN_ON:" + reason, displayId); + "android.server.wm:TURN_ON:" + reason, displayId); } /** Starts a batch of visibility updates. */ diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index 6a0de98c0ffa..3e232ea6b612 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -215,6 +215,13 @@ class AppCompatAspectRatioPolicy { mAppCompatAspectRatioState.mLetterboxBoundsForAspectRatio = bounds; } + /** + * Returns true if the activity has maximum or minimum aspect ratio. + */ + boolean hasFixedAspectRatio() { + return getMaxAspectRatio() != 0 || getMinAspectRatio() != 0; + } + private boolean isParentFullscreenPortrait() { final WindowContainer<?> parent = mActivityRecord.getParent(); return parent != null diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index 0967078deac3..fa510dbcafae 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -31,9 +31,9 @@ class AppCompatController { @NonNull private final AppCompatOrientationPolicy mOrientationPolicy; @NonNull - private final AppCompatAspectRatioPolicy mAppCompatAspectRatioPolicy; + private final AppCompatAspectRatioPolicy mAspectRatioPolicy; @NonNull - private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy; + private final AppCompatReachabilityPolicy mReachabilityPolicy; @NonNull private final DesktopAppCompatAspectRatioPolicy mDesktopAppCompatAspectRatioPolicy; @NonNull @@ -43,7 +43,7 @@ class AppCompatController { @NonNull private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy; @NonNull - private final AppCompatSizeCompatModePolicy mAppCompatSizeCompatModePolicy; + private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -56,15 +56,15 @@ class AppCompatController { mAppCompatOverrides = new AppCompatOverrides(activityRecord, packageManager, wmService.mAppCompatConfiguration, optPropBuilder, mAppCompatDeviceStateQuery); mOrientationPolicy = new AppCompatOrientationPolicy(activityRecord, mAppCompatOverrides); - mAppCompatAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord, + mAspectRatioPolicy = new AppCompatAspectRatioPolicy(activityRecord, mTransparentPolicy, mAppCompatOverrides); - mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord, + mReachabilityPolicy = new AppCompatReachabilityPolicy(activityRecord, wmService.mAppCompatConfiguration); mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(activityRecord, wmService.mAppCompatConfiguration); mDesktopAppCompatAspectRatioPolicy = new DesktopAppCompatAspectRatioPolicy(activityRecord, mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration); - mAppCompatSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, + mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, mAppCompatOverrides); } @@ -79,8 +79,8 @@ class AppCompatController { } @NonNull - AppCompatAspectRatioPolicy getAppCompatAspectRatioPolicy() { - return mAppCompatAspectRatioPolicy; + AppCompatAspectRatioPolicy getAspectRatioPolicy() { + return mAspectRatioPolicy; } @NonNull @@ -89,8 +89,8 @@ class AppCompatController { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOverrides.getAppCompatOrientationOverrides(); + AppCompatOrientationOverrides getOrientationOverrides() { + return mAppCompatOverrides.getOrientationOverrides(); } @NonNull @@ -109,8 +109,8 @@ class AppCompatController { } @NonNull - AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { - return mAppCompatReachabilityPolicy; + AppCompatReachabilityPolicy getReachabilityPolicy() { + return mReachabilityPolicy; } @NonNull @@ -124,8 +124,8 @@ class AppCompatController { } @NonNull - AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return mAppCompatOverrides.getAppCompatReachabilityOverrides(); + AppCompatReachabilityOverrides getReachabilityOverrides() { + return mAppCompatOverrides.getReachabilityOverrides(); } @NonNull @@ -139,14 +139,14 @@ class AppCompatController { } @NonNull - AppCompatSizeCompatModePolicy getAppCompatSizeCompatModePolicy() { - return mAppCompatSizeCompatModePolicy; + AppCompatSizeCompatModePolicy getSizeCompatModePolicy() { + return mSizeCompatModePolicy; } void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getAppCompatLetterboxPolicy().dump(pw, prefix); - getAppCompatSizeCompatModePolicy().dump(pw, prefix); + getSizeCompatModePolicy().dump(pw, prefix); } } diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java index e929fb414340..449458665b63 100644 --- a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java @@ -154,7 +154,7 @@ class AppCompatLetterboxPolicy { @VisibleForTesting boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) { - if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + if (mActivityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()) { return mLastShouldShowLetterboxUi; } @@ -205,7 +205,7 @@ class AppCompatLetterboxPolicy { } pw.println(prefix + " letterboxReason=" + AppCompatUtils.getLetterboxReasonString(mActivityRecord, mainWin)); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy().dump(pw, prefix); + mActivityRecord.mAppCompatController.getReachabilityPolicy().dump(pw, prefix); final AppCompatLetterboxOverrides letterboxOverride = mActivityRecord.mAppCompatController .getAppCompatLetterboxOverrides(); pw.println(prefix + " letterboxBackgroundColor=" + Integer.toHexString( @@ -276,12 +276,12 @@ class AppCompatLetterboxPolicy { final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord .mAppCompatController.getAppCompatLetterboxOverrides(); final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord - .mAppCompatController.getAppCompatReachabilityPolicy(); + .mAppCompatController.getReachabilityPolicy(); mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null), mActivityRecord.mWmService.mTransactionFactory, reachabilityPolicy, letterboxOverrides, this::getLetterboxParentSurface); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame); } final Point letterboxPosition = new Point(); @@ -291,7 +291,7 @@ class AppCompatLetterboxPolicy { final Rect innerFrame = new Rect(); calculateLetterboxInnerBounds(mActivityRecord, w, innerFrame); mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition); - if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides() + if (mActivityRecord.mAppCompatController.getReachabilityOverrides() .isDoubleTapEvent()) { // We need to notify Shell that letterbox position has changed. mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */); @@ -321,7 +321,7 @@ class AppCompatLetterboxPolicy { mLetterbox.destroy(); mLetterbox = null; } - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(null); } @@ -415,7 +415,7 @@ class AppCompatLetterboxPolicy { calculateLetterboxPosition(mActivityRecord, mLetterboxPosition); calculateLetterboxOuterBounds(mActivityRecord, mOuterBounds); calculateLetterboxInnerBounds(mActivityRecord, w, mInnerBounds); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(() -> mInnerBounds); } @@ -438,7 +438,7 @@ class AppCompatLetterboxPolicy { mLetterboxPosition.set(0, 0); mInnerBounds.setEmpty(); mOuterBounds.setEmpty(); - mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy() + mActivityRecord.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(null); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java index c84711d4be51..a49bec0ba2f3 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationOverrides.java @@ -113,7 +113,7 @@ class AppCompatOrientationOverrides { // Task to ensure that Activity Embedding is excluded. return mActivityRecord.isVisibleRequested() && mActivityRecord.getTaskFragment() != null && mActivityRecord.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN - && mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && mActivityRecord.mAppCompatController.getOrientationOverrides() .isOverrideRespectRequestedOrientationEnabled(); } @@ -144,7 +144,7 @@ class AppCompatOrientationOverrides { mOrientationOverridesState.updateOrientationRequestLoopState(); return mOrientationOverridesState.shouldIgnoreRequestInLoop() - && !mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy() + && !mActivityRecord.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio(); } diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 16e20297dcf3..fc758ef90995 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -94,7 +94,7 @@ class AppCompatOrientationPolicy { return SCREEN_ORIENTATION_PORTRAIT; } - if (mAppCompatOverrides.getAppCompatOrientationOverrides() + if (mAppCompatOverrides.getOrientationOverrides() .isAllowOrientationOverrideOptOut()) { return candidate; } @@ -108,7 +108,7 @@ class AppCompatOrientationPolicy { } final AppCompatOrientationOverrides.OrientationOverridesState capabilityState = - mAppCompatOverrides.getAppCompatOrientationOverrides() + mAppCompatOverrides.getOrientationOverrides() .mOrientationOverridesState; if (capabilityState.mIsOverrideToReverseLandscapeOrientationEnabled @@ -170,7 +170,7 @@ class AppCompatOrientationPolicy { boolean shouldIgnoreRequestedOrientation( @ActivityInfo.ScreenOrientation int requestedOrientation) { final AppCompatOrientationOverrides orientationOverrides = - mAppCompatOverrides.getAppCompatOrientationOverrides(); + mAppCompatOverrides.getOrientationOverrides(); if (orientationOverrides.shouldEnableIgnoreOrientationRequest()) { if (orientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) { Slog.w(TAG, "Ignoring orientation update to " diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java index 58b37becc373..9fb54db23d55 100644 --- a/services/core/java/com/android/server/wm/AppCompatOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java @@ -27,7 +27,7 @@ import com.android.server.wm.utils.OptPropFactory; public class AppCompatOverrides { @NonNull - private final AppCompatOrientationOverrides mAppCompatOrientationOverrides; + private final AppCompatOrientationOverrides mOrientationOverrides; @NonNull private final AppCompatCameraOverrides mAppCompatCameraOverrides; @NonNull @@ -37,7 +37,7 @@ public class AppCompatOverrides { @NonNull private final AppCompatResizeOverrides mResizeOverrides; @NonNull - private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides; + private final AppCompatReachabilityOverrides mReachabilityOverrides; @NonNull private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides; @@ -48,13 +48,13 @@ public class AppCompatOverrides { @NonNull AppCompatDeviceStateQuery appCompatDeviceStateQuery) { mAppCompatCameraOverrides = new AppCompatCameraOverrides(activityRecord, appCompatConfiguration, optPropBuilder); - mAppCompatOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, + mOrientationOverrides = new AppCompatOrientationOverrides(activityRecord, appCompatConfiguration, optPropBuilder, mAppCompatCameraOverrides); - mAppCompatReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, + mReachabilityOverrides = new AppCompatReachabilityOverrides(activityRecord, appCompatConfiguration, appCompatDeviceStateQuery); mAppCompatAspectRatioOverrides = new AppCompatAspectRatioOverrides(activityRecord, appCompatConfiguration, optPropBuilder, appCompatDeviceStateQuery, - mAppCompatReachabilityOverrides); + mReachabilityOverrides); mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord, appCompatConfiguration, optPropBuilder); mResizeOverrides = new AppCompatResizeOverrides(activityRecord, packageManager, @@ -64,8 +64,8 @@ public class AppCompatOverrides { } @NonNull - AppCompatOrientationOverrides getAppCompatOrientationOverrides() { - return mAppCompatOrientationOverrides; + AppCompatOrientationOverrides getOrientationOverrides() { + return mOrientationOverrides; } @NonNull @@ -89,8 +89,8 @@ public class AppCompatOverrides { } @NonNull - AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return mAppCompatReachabilityOverrides; + AppCompatReachabilityOverrides getReachabilityOverrides() { + return mReachabilityOverrides; } @NonNull diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java index d03a80387657..087edc184b6f 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityPolicy.java @@ -77,7 +77,7 @@ class AppCompatReachabilityPolicy { void dump(@NonNull PrintWriter pw, @NonNull String prefix) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); pw.println(prefix + " isVerticalThinLetterboxed=" + reachabilityOverrides .isVerticalThinLetterboxed()); pw.println(prefix + " isHorizontalThinLetterboxed=" + reachabilityOverrides @@ -96,7 +96,7 @@ class AppCompatReachabilityPolicy { private void handleHorizontalDoubleTap(int x) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); if (!reachabilityOverrides.isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) { return; @@ -142,7 +142,7 @@ class AppCompatReachabilityPolicy { private void handleVerticalDoubleTap(int y) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivityRecord.mAppCompatController.getReachabilityOverrides(); if (!reachabilityOverrides.isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) { return; diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index d278dc3d1be7..98bb8e79b51f 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -18,6 +18,10 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_METADATA; +import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_OVERRIDE; +import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_METADATA; +import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_OVERRIDE; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; @@ -82,7 +86,8 @@ class AppCompatSizeCompatModePolicy { } /** - * @return The {@code true} if the current instance has {@link #mAppCompatDisplayInsets} without + * @return The {@code true} if the current instance has + * {@link AppCompatSizeCompatModePolicy#mAppCompatDisplayInsets} without * considering the inheritance implemented in {@link #getAppCompatDisplayInsets()} */ boolean hasAppCompatDisplayInsetsWithoutInheritance() { @@ -199,7 +204,7 @@ class AppCompatSizeCompatModePolicy { // activity will be displayed within them even if it is in size compat mode. They should be // saved here before resolved bounds are overridden below. final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivityRecord.mAppCompatController - .getAppCompatAspectRatioPolicy(); + .getAspectRatioPolicy(); final boolean useResolvedBounds = aspectRatioPolicy.isAspectRatioApplied(); final Rect containerBounds = useResolvedBounds ? new Rect(resolvedBounds) @@ -244,8 +249,7 @@ class AppCompatSizeCompatModePolicy { resolvedBounds.set(containingBounds); // The size of floating task is fixed (only swap), so the aspect ratio is already correct. if (!appCompatDisplayInsets.mIsFloating) { - mActivityRecord.mAppCompatController.getAppCompatAspectRatioPolicy() - .applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds, + aspectRatioPolicy.applyAspectRatioForLetterbox(resolvedBounds, containingAppBounds, containingBounds); } @@ -359,7 +363,7 @@ class AppCompatSizeCompatModePolicy { } final Rect letterboxedContainerBounds = mActivityRecord.mAppCompatController - .getAppCompatAspectRatioPolicy().getLetterboxedContainerBounds(); + .getAspectRatioPolicy().getLetterboxedContainerBounds(); // The role of AppCompatDisplayInsets is like the override bounds. mAppCompatDisplayInsets = @@ -368,6 +372,112 @@ class AppCompatSizeCompatModePolicy { .mUseOverrideInsetsForConfig); } + /** + * @return {@code true} if this activity is in size compatibility mode that uses the different + * density than its parent or its bounds don't fit in parent naturally. + */ + boolean inSizeCompatMode() { + if (isInSizeCompatModeForBounds()) { + return true; + } + if (getAppCompatDisplayInsets() == null || !shouldCreateAppCompatDisplayInsets() + // The orientation is different from parent when transforming. + || mActivityRecord.isFixedRotationTransforming()) { + return false; + } + final Rect appBounds = mActivityRecord.getConfiguration().windowConfiguration + .getAppBounds(); + if (appBounds == null) { + // The app bounds hasn't been computed yet. + return false; + } + final WindowContainer<?> parent = mActivityRecord.getParent(); + if (parent == null) { + // The parent of detached Activity can be null. + return false; + } + final Configuration parentConfig = parent.getConfiguration(); + // Although colorMode, screenLayout, smallestScreenWidthDp are also fixed, generally these + // fields should be changed with density and bounds, so here only compares the most + // significant field. + return parentConfig.densityDpi != mActivityRecord.getConfiguration().densityDpi; + } + + /** + * Indicates the activity will keep the bounds and screen configuration when it was first + * launched, no matter how its parent changes. + * + * <p>If {@true}, then {@link AppCompatDisplayInsets} will be created in {@link + * ActivityRecord#resolveOverrideConfiguration} to "freeze" activity bounds and insets. + * + * @return {@code true} if this activity is declared as non-resizable and fixed orientation or + * aspect ratio. + */ + boolean shouldCreateAppCompatDisplayInsets() { + if (mActivityRecord.mAppCompatController.getAppCompatAspectRatioOverrides() + .hasFullscreenOverride()) { + // If the user has forced the applications aspect ratio to be fullscreen, don't use size + // compatibility mode in any situation. The user has been warned and therefore accepts + // the risk of the application misbehaving. + return false; + } + switch (supportsSizeChanges()) { + case SIZE_CHANGES_SUPPORTED_METADATA: + case SIZE_CHANGES_SUPPORTED_OVERRIDE: + return false; + case SIZE_CHANGES_UNSUPPORTED_OVERRIDE: + return true; + default: + // Fall through + } + // Use root activity's info for tasks in multi-window mode, or fullscreen tasks in freeform + // task display areas, to ensure visual consistency across activity launches and exits in + // the same task. + final TaskDisplayArea tda = mActivityRecord.getTaskDisplayArea(); + if (mActivityRecord.inMultiWindowMode() || (tda != null && tda.inFreeformWindowingMode())) { + final Task task = mActivityRecord.getTask(); + final ActivityRecord root = task != null ? task.getRootActivity() : null; + if (root != null && root != mActivityRecord + && !root.shouldCreateAppCompatDisplayInsets()) { + // If the root activity doesn't use size compatibility mode, the activities above + // are forced to be the same for consistent visual appearance. + return false; + } + } + final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivityRecord.mAppCompatController + .getAspectRatioPolicy(); + return !mActivityRecord.isResizeable() && (mActivityRecord.info.isFixedOrientation() + || aspectRatioPolicy.hasFixedAspectRatio()) + // The configuration of non-standard type should be enforced by system. + // {@link WindowConfiguration#ACTIVITY_TYPE_STANDARD} is set when this activity is + // added to a task, but this function is called when resolving the launch params, at + // which point, the activity type is still undefined if it will be standard. + // For other non-standard types, the type is set in the constructor, so this should + // not be a problem. + && mActivityRecord.isActivityTypeStandardOrUndefined(); + } + + /** + * Returns whether the activity supports size changes. + */ + @ActivityInfo.SizeChangesSupportMode + int supportsSizeChanges() { + final AppCompatResizeOverrides resizeOverrides = mAppCompatOverrides.getResizeOverrides(); + if (resizeOverrides.shouldOverrideForceNonResizeApp()) { + return SIZE_CHANGES_UNSUPPORTED_OVERRIDE; + } + + if (mActivityRecord.info.supportsSizeChanges) { + return SIZE_CHANGES_SUPPORTED_METADATA; + } + + if (resizeOverrides.shouldOverrideForceResizeApp()) { + return SIZE_CHANGES_SUPPORTED_OVERRIDE; + } + + return SIZE_CHANGES_UNSUPPORTED_METADATA; + } + private boolean isInSizeCompatModeForBounds(final @NonNull Rect appBounds, final @NonNull Rect containerBounds) { diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 9f88bc952351..e54e93abfcf4 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -138,7 +138,7 @@ final class AppCompatUtils { return; } final AppCompatReachabilityOverrides reachabilityOverrides = top.mAppCompatController - .getAppCompatReachabilityOverrides(); + .getReachabilityOverrides(); final boolean isTopActivityResumed = top.getOrganizedTask() == task && top.isState(RESUMED); final boolean isTopActivityVisible = top.getOrganizedTask() == task && top.isVisible(); // Whether the direct top activity is in size compat mode. @@ -222,7 +222,7 @@ final class AppCompatUtils { return "SIZE_COMPAT_MODE"; } final AppCompatAspectRatioPolicy aspectRatioPolicy = activityRecord.mAppCompatController - .getAppCompatAspectRatioPolicy(); + .getAspectRatioPolicy(); if (aspectRatioPolicy.isLetterboxedForFixedOrientationAndAspectRatio()) { return "FIXED_ORIENTATION"; } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 37575f00363e..ab32e54b92dd 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -36,6 +36,7 @@ import static com.android.server.wm.BackNavigationProto.SHOW_WALLPAPER; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE; +import android.annotation.BinderThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -44,6 +45,7 @@ import android.content.res.ResourceId; import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; +import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemProperties; @@ -705,12 +707,34 @@ class BackNavigationController { private WindowState mNavigatingWindow; private RemoteCallback mObserver; + private final IBinder.DeathRecipient mListenerDeathRecipient = + new IBinder.DeathRecipient() { + @Override + @BinderThread + public void binderDied() { + synchronized (mWindowManagerService.mGlobalLock) { + stopMonitorForRemote(); + stopMonitorTransition(); + } + } + }; + void startMonitor(@NonNull WindowState window, @NonNull RemoteCallback observer) { mNavigatingWindow = window; mObserver = observer; + try { + mObserver.getInterface().asBinder().linkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } catch (RemoteException r) { + Slog.e(TAG, "Failed to link to death"); + } } void stopMonitorForRemote() { + if (mObserver != null) { + mObserver.getInterface().asBinder().unlinkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } mObserver = null; } diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index a4e58ef923b8..d6ae65193121 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -108,9 +108,7 @@ final class ContentRecorder implements WindowContainerListener { ContentRecorder(@NonNull DisplayContent displayContent) { this(displayContent, new RemoteMediaProjectionManagerWrapper(displayContent.mDisplayId), - new DisplayManagerFlags().isConnectedDisplayManagementEnabled() - && !new DisplayManagerFlags() - .isPixelAnisotropyCorrectionInLogicalDisplayEnabled() + !new DisplayManagerFlags().isPixelAnisotropyCorrectionInLogicalDisplayEnabled() && displayContent.getDisplayInfo().type == Display.TYPE_EXTERNAL); } diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index f40d636b522a..b932ef362aca 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -264,7 +264,7 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { // that should be respected, Check all activities in display to make sure any eligible // activity should be respected. final ActivityRecord activity = mDisplayContent.getActivity((r) -> - r.mAppCompatController.getAppCompatOrientationOverrides() + r.mAppCompatController.getOrientationOverrides() .shouldRespectRequestedOrientationDueToOverride()); return activity != null; } diff --git a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java index d49a507c9e11..5bec4424269a 100644 --- a/services/core/java/com/android/server/wm/DisplayAreaPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayAreaPolicy.java @@ -25,6 +25,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; +import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; +import static android.window.DisplayAreaOrganizer.FEATURE_APP_ZOOM_OUT; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION; import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT; @@ -151,6 +153,12 @@ public abstract class DisplayAreaPolicy { .all() .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_SECURE_SYSTEM_OVERLAY) + .build()) + .addFeature(new Feature.Builder(wmService.mPolicy, "AppZoomOut", + FEATURE_APP_ZOOM_OUT) + .all() + .except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, + TYPE_STATUS_BAR, TYPE_NOTIFICATION_SHADE, TYPE_WALLPAPER) .build()); } rootHierarchy diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 145c7b37fcdc..d32c31f1c1c7 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1822,7 +1822,13 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ private void applyFixedRotationForNonTopVisibleActivityIfNeeded(@NonNull ActivityRecord ar, @ActivityInfo.ScreenOrientation int topOrientation) { - final int orientation = ar.getRequestedOrientation(); + int orientation = ar.getRequestedOrientation(); + if (orientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { + final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(ar); + if (nextCandidate != null) { + orientation = nextCandidate.getRequestedOrientation(); + } + } if (orientation == topOrientation || ar.inMultiWindowMode() || ar.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { return; @@ -1864,9 +1870,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return ROTATION_UNDEFINED; } if (activityOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) { - final ActivityRecord nextCandidate = getActivity( - a -> a.canDefineOrientationForActivitiesAbove() /* callback */, - r /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); + final ActivityRecord nextCandidate = getActivityBelowForDefiningOrientation(r); if (nextCandidate != null) { r = nextCandidate; activityOrientation = r.getOverrideOrientation(); @@ -2964,7 +2968,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (!handlesOrientationChangeFromDescendant(orientation)) { ActivityRecord topActivity = topRunningActivity(/* considerKeyguardState= */ true); if (topActivity != null && topActivity.mAppCompatController - .getAppCompatOrientationOverrides() + .getOrientationOverrides() .shouldUseDisplayLandscapeNaturalOrientation()) { ProtoLog.v(WM_DEBUG_ORIENTATION, "Display id=%d is ignoring orientation request for %d, return %d" @@ -4291,7 +4295,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return target; } if (android.view.inputmethod.Flags.refactorInsetsController()) { - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); + final DisplayContent defaultDc = getUserMainDisplayContent(); return defaultDc.mRemoteInsetsControlTarget; } else { return getImeFallback(); @@ -4301,11 +4305,26 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp InsetsControlTarget getImeFallback() { // host is in non-default display that doesn't support system decor, default to // default display's StatusBar to control IME (when available), else let system control it. - final DisplayContent defaultDc = mWmService.getDefaultDisplayContentLocked(); - WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); + final DisplayContent defaultDc = getUserMainDisplayContent(); + final WindowState statusBar = defaultDc.getDisplayPolicy().getStatusBar(); return statusBar != null ? statusBar : defaultDc.mRemoteInsetsControlTarget; } + private DisplayContent getUserMainDisplayContent() { + final DisplayContent defaultDc; + if (android.view.inputmethod.Flags.fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + defaultDc = mWmService.getUserMainDisplayContentLocked(userId); + if (defaultDc == null) { + throw new IllegalStateException( + "No default display was assigned to user " + userId); + } + } else { + defaultDc = mWmService.getDefaultDisplayContentLocked(); + } + return defaultDc; + } + /** * Returns the corresponding IME insets control target according the IME target type. * @@ -4841,8 +4860,15 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // The control target could be the RemoteInsetsControlTarget (if the focussed // view is on a virtual display that can not show the IME (and therefore it will // be shown on the default display) - if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { - return mRemoteInsetsControlTarget; + if (android.view.inputmethod.Flags + .fallbackDisplayForSecondaryUserOnSecondaryDisplay()) { + if (isUserMainDisplay() && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } + } else { + if (isDefaultDisplay && mRemoteInsetsControlTarget != null) { + return mRemoteInsetsControlTarget; + } } } return null; @@ -4858,6 +4884,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Returns {@code true} if {@link #mDisplayId} corresponds to the user's main display. + * + * <p>Visible background users may have other than DEFAULT_DISPLAY marked as their main display. + */ + private boolean isUserMainDisplay() { + final int userId = mWmService.mUmInternal.getUserAssignedToDisplay(mDisplayId); + return mDisplayId == mWmService.mUmInternal.getMainDisplayAssignedToUser(userId); + } + + /** * Computes the window the IME should be attached to. */ @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 3c60d8296577..db058cafe5fe 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -215,7 +215,8 @@ class DragDropController { mDragState.mOriginalAlpha = alpha; mDragState.mAnimatedScale = callingWin.mGlobalScale; mDragState.mToken = dragToken; - mDragState.mDisplayContent = displayContent; + mDragState.mStartDragDisplayContent = displayContent; + mDragState.mCurrentDisplayContent = displayContent; mDragState.mData = data; mDragState.mCallingTaskIdToHide = shouldMoveCallingTaskToBack(callingWin, flags); @@ -273,7 +274,7 @@ class DragDropController { InputManagerGlobal.getInstance().setPointerIcon( PointerIcon.getSystemIcon( mService.mContext, PointerIcon.TYPE_GRABBING), - mDragState.mDisplayContent.getDisplayId(), touchDeviceId, + mDragState.mCurrentDisplayContent.getDisplayId(), touchDeviceId, touchPointerId, mDragState.getInputToken()); } // remember the thumb offsets for later diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 3a0e41a5f9f8..d48b9b4a5d10 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -45,6 +45,7 @@ import android.annotation.Nullable; import android.content.ClipData; import android.content.ClipDescription; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.os.Binder; import android.os.Build; @@ -70,6 +71,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.internal.view.IDragAndDropPermissions; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; +import com.android.window.flags.Flags; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; @@ -127,10 +129,17 @@ class DragState { */ volatile boolean mAnimationCompleted = false; /** + * The display on which the drag originally started. Note that it's possible for either/both + * mStartDragDisplayContent and mCurrentDisplayContent to be invalid if DisplayTopology was + * changed or removed in the middle of the drag. In this case, drag will also be cancelled as + * soon as listener is notified. + */ + DisplayContent mStartDragDisplayContent; + /** * The display on which the drag is happening. If it goes into a different display this will * be updated. */ - DisplayContent mDisplayContent; + DisplayContent mCurrentDisplayContent; @Nullable private ValueAnimator mAnimator; private final Interpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f); @@ -179,7 +188,7 @@ class DragState { .setContainerLayer() .setName("Drag and Drop Input Consumer") .setCallsite("DragState.showInputSurface") - .setParent(mDisplayContent.getOverlayLayer()) + .setParent(mCurrentDisplayContent.getOverlayLayer()) .build(); } final InputWindowHandle h = getInputWindowHandle(); @@ -244,7 +253,8 @@ class DragState { } } DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, - inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, + inWindowY, mThumbOffsetX, mThumbOffsetY, + mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null, dragSurface, null, mDragResult); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); @@ -542,10 +552,26 @@ class DragState { } } ClipDescription description = data != null ? data.getDescription() : mDataDescription; + + // Note this can be negative numbers if touch coords are left or top of the window. + PointF relativeToWindowCoords = new PointF(newWin.translateToWindowX(touchX), + newWin.translateToWindowY(touchY)); + if (Flags.enableConnectedDisplaysDnd() + && mCurrentDisplayContent.getDisplayId() != newWin.getDisplayId()) { + // Currently DRAG_STARTED coords are sent relative to the window target in **px** + // coordinates. However, this cannot be extended to connected displays scenario, + // as there's only global **dp** coordinates and no global **px** coordinates. + // Hence, the coords sent here will only try to indicate that drag started outside + // this window display, but relative distance should not be calculated or depended + // on. + relativeToWindowCoords = new PointF(-newWin.getBounds().left - 1, + -newWin.getBounds().top - 1); + } + DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED, - newWin.translateToWindowX(touchX), newWin.translateToWindowY(touchY), - description, data, false /* includeDragSurface */, - true /* includeDragFlags */, null /* dragAndDropPermission */); + relativeToWindowCoords.x, relativeToWindowCoords.y, description, data, + false /* includeDragSurface */, true /* includeDragFlags */, + null /* dragAndDropPermission */); try { newWin.mClient.dispatchDragEvent(event); // track each window that we've notified that the drag is starting @@ -702,6 +728,20 @@ class DragState { mCurrentDisplayX = displayX; mCurrentDisplayY = displayY; + final DisplayContent lastSetDisplayContent = mCurrentDisplayContent; + boolean cursorMovedToDifferentDisplay = false; + // Keep latest display up-to-date even when drag has stopped. + if (Flags.enableConnectedDisplaysDnd() && mCurrentDisplayContent.mDisplayId != displayId) { + final DisplayContent newDisplay = mService.mRoot.getDisplayContent(displayId); + if (newDisplay == null) { + Slog.e(TAG_WM, "Target displayId=" + displayId + " was not found, ending drag."); + endDragLocked(false /* dropConsumed */, + false /* relinquishDragSurfaceToDropTarget */); + return; + } + cursorMovedToDifferentDisplay = true; + mCurrentDisplayContent = newDisplay; + } if (!keepHandling) { return; } @@ -710,6 +750,24 @@ class DragState { if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked"); } + if (cursorMovedToDifferentDisplay) { + mAnimatedScale = mAnimatedScale * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mThumbOffsetX = mThumbOffsetX * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mThumbOffsetY = mThumbOffsetY * mCurrentDisplayContent.mBaseDisplayDensity + / lastSetDisplayContent.mBaseDisplayDensity; + mTransaction.reparent(mSurfaceControl, mCurrentDisplayContent.getSurfaceControl()); + mTransaction.setScale(mSurfaceControl, mAnimatedScale, mAnimatedScale); + + final InputWindowHandle inputWindowHandle = getInputWindowHandle(); + if (inputWindowHandle == null) { + Slog.w(TAG_WM, "Drag is in progress but there is no drag window handle."); + return; + } + inputWindowHandle.displayId = displayId; + mTransaction.setInputWindowInfo(mInputSurface, inputWindowHandle); + } mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX, displayY - mThumbOffsetY).apply(); ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl, @@ -734,10 +792,10 @@ class DragState { ClipData data, boolean includeDragSurface, boolean includeDragFlags, IDragAndDropPermissions dragAndDropPermissions) { return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY, - includeDragFlags ? mFlags : 0, + mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0, null /* localState */, description, data, - includeDragSurface ? mSurfaceControl : null, - dragAndDropPermissions, false /* result */); + includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, + false /* result */); } private ValueAnimator createReturnAnimationLocked() { diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index d3c3d2834124..ba1401ab3978 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -79,7 +79,8 @@ class LaunchParamsController { * @param result The resulting params. */ void calculate(Task task, WindowLayout layout, ActivityRecord activity, ActivityRecord source, - ActivityOptions options, @Nullable Request request, int phase, LaunchParams result) { + ActivityOptions options, @Nullable Request request, + @LaunchParamsModifier.Phase int phase, LaunchParams result) { result.reset(); if (task != null || activity != null) { diff --git a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java index 8c50913dd563..29922f0f85c5 100644 --- a/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java +++ b/services/core/java/com/android/server/wm/PageSizeMismatchDialog.java @@ -57,9 +57,11 @@ class PageSizeMismatchDialog extends AppWarnings.BaseDialog { final AlertDialog.Builder builder = new AlertDialog.Builder(context) - .setPositiveButton( - R.string.ok, - (dialog, which) -> {/* Do nothing */}) + .setPositiveButton(R.string.ok, (dialog, which) -> + manager.setPackageFlag( + mUserId, mPackageName, + AppWarnings.FLAG_HIDE_PAGE_SIZE_MISMATCH, + true)) .setMessage(Html.fromHtml(warning, FROM_HTML_MODE_COMPACT)) .setTitle(label); diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java index 9dc3d6a81338..bc16a566bfef 100644 --- a/services/core/java/com/android/server/wm/PersisterQueue.java +++ b/services/core/java/com/android/server/wm/PersisterQueue.java @@ -86,6 +86,34 @@ class PersisterQueue { mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread"); } + /** + * Busy wait until {@link #mLazyTaskWriterThread} is in {@link Thread.State#WAITING}, or + * times out. This indicates the thread is waiting for new tasks to appear. If the wait + * succeeds, this queue waits at least {@link #mPreTaskDelayMs} milliseconds before running the + * next task. + * + * <p>This is for testing purposes only. + * + * @param timeoutMillis the maximum time of waiting in milliseconds + * @return {@code true} if the thread is in {@link Thread.State#WAITING} at return + */ + @VisibleForTesting + boolean waitUntilWritingThreadIsWaiting(long timeoutMillis) { + final long timeoutTime = SystemClock.uptimeMillis() + timeoutMillis; + do { + Thread.State state; + synchronized (this) { + state = mLazyTaskWriterThread.getState(); + } + if (state == Thread.State.WAITING) { + return true; + } + Thread.yield(); + } while (SystemClock.uptimeMillis() < timeoutTime); + + return false; + } + synchronized void startPersisting() { if (!mLazyTaskWriterThread.isAlive()) { mLazyTaskWriterThread.start(); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 57fe0bb4937e..04f09d5fe627 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -152,7 +152,6 @@ import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; import com.android.server.am.AppTimeTracker; import com.android.server.am.UserState; -import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.pm.UserManagerInternal; import com.android.server.policy.PermissionPolicyInternal; import com.android.server.policy.WindowManagerPolicy; @@ -1541,20 +1540,18 @@ class RootWindowContainer extends WindowContainer<DisplayContent> ActivityInfo aInfo = resolveHomeActivity(userId, homeIntent); boolean lookForSecondaryHomeActivityInPrimaryHomePackage = aInfo != null; - if (android.companion.virtual.flags.Flags.vdmCustomHome()) { - // Resolve the externally set home activity for this display, if any. If it is unset or - // we fail to resolve it, fallback to the default secondary home activity. - final ComponentName customHomeComponent = - taskDisplayArea.getDisplayContent() != null - ? taskDisplayArea.getDisplayContent().getCustomHomeComponent() - : null; - if (customHomeComponent != null) { - homeIntent.setComponent(customHomeComponent); - ActivityInfo customHomeActivityInfo = resolveHomeActivity(userId, homeIntent); - if (customHomeActivityInfo != null) { - aInfo = customHomeActivityInfo; - lookForSecondaryHomeActivityInPrimaryHomePackage = false; - } + // Resolve the externally set home activity for this display, if any. If it is unset or + // we fail to resolve it, fallback to the default secondary home activity. + final ComponentName customHomeComponent = + taskDisplayArea.getDisplayContent() != null + ? taskDisplayArea.getDisplayContent().getCustomHomeComponent() + : null; + if (customHomeComponent != null) { + homeIntent.setComponent(customHomeComponent); + ActivityInfo customHomeActivityInfo = resolveHomeActivity(userId, homeIntent); + if (customHomeActivityInfo != null) { + aInfo = customHomeActivityInfo; + lookForSecondaryHomeActivityInPrimaryHomePackage = false; } } diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index a5454546341b..3eb13c52cca6 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -407,10 +407,8 @@ class SnapshotPersistQueue { bitmap.recycle(); final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId); - try { - FileOutputStream fos = new FileOutputStream(file); + try (FileOutputStream fos = new FileOutputStream(file)) { swBitmap.compress(JPEG, COMPRESS_QUALITY, fos); - fos.close(); } catch (IOException e) { Slog.e(TAG, "Unable to open " + file + " for persisting.", e); return false; @@ -428,10 +426,8 @@ class SnapshotPersistQueue { swBitmap.recycle(); final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId); - try { - FileOutputStream lowResFos = new FileOutputStream(lowResFile); + try (FileOutputStream lowResFos = new FileOutputStream(lowResFile)) { lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos); - lowResFos.close(); } catch (IOException e) { Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e); return false; diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java index 7349224ddcd8..1a7a6196cf85 100644 --- a/services/core/java/com/android/server/wm/StartingData.java +++ b/services/core/java/com/android/server/wm/StartingData.java @@ -31,11 +31,18 @@ public abstract class StartingData { static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1; /** Do copy splash screen to client after transaction done. */ static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2; + /** + * Remove the starting window after transition finish. + * Used when activity doesn't request show when locked, so the app window should never show to + * the user if device is locked. + **/ + static final int AFTER_TRANSITION_FINISH = 3; @IntDef(prefix = { "AFTER_TRANSACTION" }, value = { AFTER_TRANSACTION_IDLE, AFTER_TRANSACTION_REMOVE_DIRECTLY, AFTER_TRANSACTION_COPY_TO_CLIENT, + AFTER_TRANSITION_FINISH, }) @interface AfterTransaction {} diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index d92301ba4f6f..fe478c60bc32 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5230,9 +5230,15 @@ class Task extends TaskFragment { // to ensure any necessary pause logic occurs. In the case where the Activity will be // shown regardless of the lock screen, the call to // {@link ActivityTaskSupervisor#checkReadyForSleepLocked} is skipped. - final ActivityRecord next = topRunningActivity(true /* focusableOnly */); - if (next == null || !next.canTurnScreenOn()) { - checkReadyForSleep(); + if (shouldSleepActivities()) { + final ActivityRecord next = topRunningActivity(true /* focusableOnly */); + if (next != null && next.canTurnScreenOn() + && !mWmService.mPowerManager.isInteractive()) { + mTaskSupervisor.wakeUp(getDisplayId(), "resumeTop-turnScreenOnFlag"); + next.setCurrentLaunchCanTurnScreenOn(false); + } else { + checkReadyForSleep(); + } } } finally { mInResumeTopActivity = false; @@ -5255,6 +5261,10 @@ class Task extends TaskFragment { return false; } + if (!mTaskSupervisor.readyToResume()) { + return false; + } + final ActivityRecord topActivity = topRunningActivity(true /* focusableOnly */); if (topActivity == null) { // There are no activities left in this task, let's look somewhere else. diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java index c39671d76929..e3a5b66b83fd 100644 --- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java @@ -96,9 +96,10 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { } @Override + @Result public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout, @Nullable ActivityRecord activity, @Nullable ActivityRecord source, - @Nullable ActivityOptions options, @Nullable Request request, int phase, + @Nullable ActivityOptions options, @Nullable Request request, @Phase int phase, LaunchParams currentParams, LaunchParams outParams) { initLogBuilder(task, activity); final int result = calculate(task, layout, activity, source, options, request, phase, @@ -107,9 +108,10 @@ class TaskLaunchParamsModifier implements LaunchParamsModifier { return result; } + @Result private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout, @Nullable ActivityRecord activity, @Nullable ActivityRecord source, - @Nullable ActivityOptions options, @Nullable Request request, int phase, + @Nullable ActivityOptions options, @Nullable Request request, @Phase int phase, LaunchParams currentParams, LaunchParams outParams) { final ActivityRecord root; if (task != null) { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a9646783b92d..f4a455a9c2dd 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -70,6 +70,8 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN; +import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; @@ -1374,6 +1376,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { enterAutoPip = true; } } + + if (ar.mStartingData != null && ar.mStartingData.mRemoveAfterTransaction + == AFTER_TRANSITION_FINISH + && (!ar.isVisible() || !ar.mTransitionController.inTransition(ar))) { + ar.mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE; + ar.removeStartingWindow(); + } final ChangeInfo changeInfo = mChanges.get(ar); // Due to transient-hide, there may be some activities here which weren't in the // transition. diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java index edd99243c3ef..88ea0730ab00 100644 --- a/services/core/java/com/android/server/wm/TransparentPolicy.java +++ b/services/core/java/com/android/server/wm/TransparentPolicy.java @@ -204,7 +204,7 @@ class TransparentPolicy { return true; } final AppCompatSizeCompatModePolicy scmPolicy = mActivityRecord.mAppCompatController - .getAppCompatSizeCompatModePolicy(); + .getSizeCompatModePolicy(); if (mActivityRecord.getTask() == null || mActivityRecord.fillsParent() || scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance()) { return true; diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index aa60f939f9aa..e761e024b3ca 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -117,6 +117,7 @@ import com.android.server.wm.SurfaceAnimator.Animatable; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; import com.android.server.wm.utils.AlwaysTruePredicate; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.lang.ref.WeakReference; @@ -1659,6 +1660,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return ORIENTATION_UNDEFINED; } + @Nullable + ActivityRecord getActivityBelowForDefiningOrientation(ActivityRecord from) { + return getActivity(ActivityRecord::canDefineOrientationForActivitiesAbove, + from /* boundary */, false /* includeBoundary */, true /* traverseTopToBottom */); + } + /** * Calls {@link #setOrientation(int, WindowContainer)} with {@code null} to the last 2 * parameters. @@ -2730,6 +2737,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (!mTransitionController.canAssignLayers(this)) return; final boolean changed = layer != mLastLayer || mLastRelativeToLayer != null; if (mSurfaceControl != null && changed) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } setLayer(t, layer); mLastLayer = layer; mLastRelativeToLayer = null; @@ -2740,6 +2754,13 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< boolean forceUpdate) { final boolean changed = layer != mLastLayer || mLastRelativeToLayer != relativeTo; if (mSurfaceControl != null && (changed || forceUpdate)) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } setRelativeLayer(t, relativeTo, layer); mLastLayer = layer; mLastRelativeToLayer = relativeTo; diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 3c6778ecbb30..dd6e15b74a58 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -331,6 +331,7 @@ import android.window.WindowContextInfo; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.os.IResultReceiver; import com.android.internal.os.TransferPipe; import com.android.internal.policy.IKeyguardDismissCallback; @@ -600,6 +601,7 @@ public class WindowManagerService extends IWindowManager.Stub final boolean mLimitedAlphaCompositing; final int mMaxUiWidth; + @NonNull @VisibleForTesting WindowManagerPolicy mPolicy; @@ -1054,13 +1056,16 @@ public class WindowManagerService extends IWindowManager.Stub private boolean mAnimationsDisabled = false; boolean mPointerLocationEnabled = false; + @NonNull final AppCompatConfiguration mAppCompatConfiguration; private boolean mIsIgnoreOrientationRequestDisabled; + @NonNull final InputManagerService mInputManager; final DisplayManagerInternal mDisplayManagerInternal; final DisplayManager mDisplayManager; + @NonNull final ActivityTaskManagerService mAtmService; /** Indicates whether this device supports wide color gamut / HDR rendering */ @@ -1116,7 +1121,9 @@ public class WindowManagerService extends IWindowManager.Stub static WindowManagerThreadPriorityBooster sThreadPriorityBooster = new WindowManagerThreadPriorityBooster(); + @NonNull Supplier<SurfaceControl.Builder> mSurfaceControlFactory; + @NonNull Supplier<SurfaceControl.Transaction> mTransactionFactory; private final SurfaceControl.Transaction mTransaction; @@ -1188,9 +1195,11 @@ public class WindowManagerService extends IWindowManager.Stub private volatile boolean mDisableSecureWindows = false; - public static WindowManagerService main(final Context context, final InputManagerService im, - final boolean showBootMsgs, WindowManagerPolicy policy, - ActivityTaskManagerService atm) { + /** Creates an instance of the WindowManagerService for the system server. */ + public static WindowManagerService main(@NonNull final Context context, + @NonNull final InputManagerService im, final boolean showBootMsgs, + @NonNull final WindowManagerPolicy policy, + @NonNull final ActivityTaskManagerService atm) { // Using SysUI context to have access to Material colors extracted from Wallpaper. final AppCompatConfiguration appCompat = new AppCompatConfiguration( ActivityThread.currentActivityThread().getSystemUiContext()); @@ -1204,15 +1213,19 @@ public class WindowManagerService extends IWindowManager.Stub /** * Creates and returns an instance of the WindowManagerService. This call allows the caller - * to override factories that can be used to stub native calls during test. + * to override factories that can be used to stub native calls during test. Tests should use + * {@link WindowManagerServiceTestSupport} instead of calling this directly to ensure + * proper initialization and cleanup of dependencies. */ - @VisibleForTesting - public static WindowManagerService main(final Context context, final InputManagerService im, - final boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm, - DisplayWindowSettingsProvider displayWindowSettingsProvider, - Supplier<SurfaceControl.Transaction> transactionFactory, - Supplier<SurfaceControl.Builder> surfaceControlFactory, - AppCompatConfiguration appCompat) { + @VisibleForTesting(visibility = Visibility.PRIVATE) + static WindowManagerService main(@NonNull final Context context, + @NonNull final InputManagerService im, boolean showBootMsgs, + @NonNull final WindowManagerPolicy policy, + @NonNull final ActivityTaskManagerService atm, + @NonNull final DisplayWindowSettingsProvider displayWindowSettingsProvider, + @NonNull final Supplier<SurfaceControl.Transaction> transactionFactory, + @NonNull final Supplier<SurfaceControl.Builder> surfaceControlFactory, + @NonNull final AppCompatConfiguration appCompat) { final WindowManagerService[] wms = new WindowManagerService[1]; DisplayThread.getHandler().runWithScissors(() -> @@ -1238,12 +1251,13 @@ public class WindowManagerService extends IWindowManager.Stub new WindowManagerShellCommand(this).exec(this, in, out, err, args, callback, result); } - private WindowManagerService(Context context, InputManagerService inputManager, - boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm, - DisplayWindowSettingsProvider displayWindowSettingsProvider, - Supplier<SurfaceControl.Transaction> transactionFactory, - Supplier<SurfaceControl.Builder> surfaceControlFactory, - AppCompatConfiguration appCompat) { + private WindowManagerService(@NonNull Context context, + @NonNull InputManagerService inputManager, boolean showBootMsgs, + @NonNull WindowManagerPolicy policy, @NonNull ActivityTaskManagerService atm, + @NonNull DisplayWindowSettingsProvider displayWindowSettingsProvider, + @NonNull Supplier<SurfaceControl.Transaction> transactionFactory, + @NonNull Supplier<SurfaceControl.Builder> surfaceControlFactory, + @NonNull AppCompatConfiguration appCompat) { installLock(this, INDEX_WINDOW); mGlobalLock = atm.getGlobalLock(); mAtmService = atm; @@ -7473,6 +7487,23 @@ public class WindowManagerService extends IWindowManager.Stub return mRoot.getDisplayContent(DEFAULT_DISPLAY); } + /** + * Returns the main display content for the user passed as parameter. + * + * <p>Visible background users may have their own designated main display, distinct from the + * system default display (DEFAULT_DISPLAY). Visible background users operate independently + * with their own main displays. These secondary user main displays host the secondary home + * activities. + */ + @Nullable + DisplayContent getUserMainDisplayContentLocked(@UserIdInt int userId) { + final int userMainDisplayId = mUmInternal.getMainDisplayAssignedToUser(userId); + if (userMainDisplayId == -1) { + return null; + } + return mRoot.getDisplayContent(userMainDisplayId); + } + public void onOverlayChanged() { // Post to display thread so it can get the latest display info. mH.post(() -> { @@ -10177,9 +10208,10 @@ public class WindowManagerService extends IWindowManager.Stub throw new SecurityException("Access denied to process: " + pid + ", must have permission " + Manifest.permission.ACCESS_FPS_COUNTER); } - - if (mRoot.anyTaskForId(taskId) == null) { - throw new IllegalArgumentException("no task with taskId: " + taskId); + synchronized (mGlobalLock) { + if (mRoot.anyTaskForId(taskId) == null) { + throw new IllegalArgumentException("no task with taskId: " + taskId); + } } mTaskFpsCallbackController.registerListener(taskId, callback); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index b43e334d6e1a..85e3d89730a3 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -126,6 +126,7 @@ import static com.android.server.wm.IdentifierProto.USER_ID; import static com.android.server.wm.MoveAnimationSpecProto.DURATION_MS; import static com.android.server.wm.MoveAnimationSpecProto.FROM; import static com.android.server.wm.MoveAnimationSpecProto.TO; +import static com.android.server.wm.StartingData.AFTER_TRANSITION_FINISH; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL; @@ -1920,6 +1921,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } final ActivityRecord atoken = mActivityRecord; if (atoken != null) { + if (atoken.mStartingData != null && mAttrs.type != TYPE_APPLICATION_STARTING + && atoken.mStartingData.mRemoveAfterTransaction + == AFTER_TRANSITION_FINISH) { + // Preventing app window from visible during un-occluding animation playing due to + // alpha blending. + return false; + } final boolean isVisible = isStartingWindowAssociatedToTask() ? mStartingData.mAssociatedTask.isVisible() : atoken.isVisible(); return ((!isParentWindowHidden() && isVisible) @@ -2925,7 +2933,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP final int mask = FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD | FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; WindowManager.LayoutParams sa = mActivityRecord.mStartingWindow.mAttrs; + final boolean wasShowWhenLocked = (sa.flags & FLAG_SHOW_WHEN_LOCKED) != 0; + final boolean removeShowWhenLocked = (mAttrs.flags & FLAG_SHOW_WHEN_LOCKED) == 0; sa.flags = (sa.flags & ~mask) | (mAttrs.flags & mask); + if (Flags.keepAppWindowHideWhileLocked() && wasShowWhenLocked && removeShowWhenLocked) { + // Trigger unoccluding animation if needed. + mActivityRecord.checkKeyguardFlagsChanged(); + mActivityRecord.deferStartingWindowRemovalForKeyguardUnoccluding(); + } } } @@ -5424,7 +5439,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // change then delay the position update until it has redrawn to avoid any flickers. final boolean isLetterboxedAndRelaunching = activityRecord != null && activityRecord.areBoundsLetterboxed() - && activityRecord.mAppCompatController.getAppCompatOrientationOverrides() + && activityRecord.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged(); if (surfaceResizedWithoutMoveAnimation || isLetterboxedAndRelaunching) { applyWithNextDraw(mSetSurfacePositionConsumer); @@ -5547,6 +5562,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP @Override void assignLayer(Transaction t, int layer) { if (mStartingData != null) { + if (Flags.useSelfSyncTransactionForLayer() && mSyncState != SYNC_STATE_NONE) { + // When this container needs to be synced, assign layer with its own sync + // transaction to avoid out of ordering when merge. + // Still use the passed-in transaction for non-sync case, such as building finish + // transaction. + t = getSyncTransaction(); + } // The starting window should cover the task. t.setLayer(mSurfaceControl, Integer.MAX_VALUE); return; diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 0154d95d888d..d973fb014e35 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -480,18 +480,6 @@ class WindowStateAnimator { if (mLastHidden) { showRobustly(t); mLastHidden = false; - final DisplayContent displayContent = w.getDisplayContent(); - if (!displayContent.getLastHasContent()) { - // This draw means the difference between unique content and mirroring. - // Run another pass through performLayout to set mHasContent in the - // LogicalDisplay. - displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_ANIM; - if (DEBUG_LAYOUT_REPEATS) { - mService.mWindowPlacerLocked.debugLayoutRepeats( - "showSurfaceRobustlyLocked " + w, - displayContent.pendingLayoutChanges); - } - } } } } diff --git a/services/core/xsd/device-state-config/device-state-config.xsd b/services/core/xsd/device-state-config/device-state-config.xsd index 4a947327070a..324c9b422ecb 100644 --- a/services/core/xsd/device-state-config/device-state-config.xsd +++ b/services/core/xsd/device-state-config/device-state-config.xsd @@ -41,6 +41,7 @@ <xs:annotation name="nullable" /> </xs:element> <xs:element name="properties" type="properties" /> + <xs:element name="flags" type="flags" /> <xs:element name="conditions" type="conditions" /> </xs:sequence> </xs:complexType> @@ -53,6 +54,14 @@ </xs:sequence> </xs:complexType> + <xs:complexType name="flags"> + <xs:sequence> + <xs:element name="flag" type="xs:string" minOccurs="0" maxOccurs="unbounded"> + <xs:annotation name="nullable" /> + </xs:element> + </xs:sequence> + </xs:complexType> + <xs:complexType name="conditions"> <xs:sequence> <xs:element name="lid-switch" type="lidSwitchCondition" minOccurs="0"> diff --git a/services/core/xsd/device-state-config/schema/current.txt b/services/core/xsd/device-state-config/schema/current.txt index 5bb216e69e4d..c94f3a80b898 100644 --- a/services/core/xsd/device-state-config/schema/current.txt +++ b/services/core/xsd/device-state-config/schema/current.txt @@ -11,10 +11,12 @@ package com.android.server.policy.devicestate.config { public class DeviceState { ctor public DeviceState(); method public com.android.server.policy.devicestate.config.Conditions getConditions(); + method public com.android.server.policy.devicestate.config.Flags getFlags(); method public java.math.BigInteger getIdentifier(); method @Nullable public String getName(); method public com.android.server.policy.devicestate.config.Properties getProperties(); method public void setConditions(com.android.server.policy.devicestate.config.Conditions); + method public void setFlags(com.android.server.policy.devicestate.config.Flags); method public void setIdentifier(java.math.BigInteger); method public void setName(@Nullable String); method public void setProperties(com.android.server.policy.devicestate.config.Properties); @@ -25,6 +27,11 @@ package com.android.server.policy.devicestate.config { method public java.util.List<com.android.server.policy.devicestate.config.DeviceState> getDeviceState(); } + public class Flags { + ctor public Flags(); + method @Nullable public java.util.List<java.lang.String> getFlag(); + } + public class LidSwitchCondition { ctor public LidSwitchCondition(); method public boolean getOpen(); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index e69a7414dd76..0ce25db6ea55 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -8909,11 +8909,15 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (parent) { Preconditions.checkCallAuthorization( - isProfileOwnerOfOrganizationOwnedDevice(getCallerIdentity().getUserId())); + isProfileOwnerOfOrganizationOwnedDevice(caller.getUserId())); + // If a DPC is querying on the parent instance, make sure it's only querying the parent + // user of itself. Querying any other user is not allowed. + Preconditions.checkArgument(caller.getUserId() == userHandle); } + int affectedUserId = parent ? getProfileParentId(userHandle) : userHandle; Boolean disallowed = mDevicePolicyEngine.getResolvedPolicy( PolicyDefinition.SCREEN_CAPTURE_DISABLED, - userHandle); + affectedUserId); return disallowed != null && disallowed; } @@ -10466,6 +10470,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { // Reset some of the user-specific policies. final DevicePolicyData policy = getUserData(userId); policy.mPermissionPolicy = DevicePolicyManager.PERMISSION_POLICY_PROMPT; + mPolicyCache.setPermissionPolicy(userId, policy.mPermissionPolicy); // Clear delegations. policy.mDelegationMap.clear(); policy.mStatusBarDisabled = false; @@ -14669,7 +14674,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { @Override public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled, PersistableBundle options) { - if (Flags.secondaryLockscreenApiEnabled()) { + if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) { final CallerIdentity caller = getCallerIdentity(); final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller); synchronized (getLockObject()) { @@ -14680,16 +14685,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { caller.getUserId()); } - if (mSupervisionManagerInternal != null) { - mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser( - caller.getUserId(), enabled, options); - } else { - synchronized (getLockObject()) { - DevicePolicyData policy = getUserData(caller.getUserId()); - policy.mSecondaryLockscreenEnabled = enabled; - saveSettingsLocked(caller.getUserId()); - } - } + mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser( + caller.getUserId(), enabled, options); } else { Objects.requireNonNull(who, "ComponentName is null"); diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp index a8e6f689b424..dae481a3c215 100644 --- a/services/incremental/IncrementalService.cpp +++ b/services/incremental/IncrementalService.cpp @@ -1999,9 +1999,9 @@ bool IncrementalService::configureNativeBinaries(StorageId storage, std::string_ // Create new lib file without signature info incfs::NewFileParams libFileParams = { .size = entry.uncompressed_length, - .signature = {}, // Metadata of the new lib file is its relative path .metadata = {targetLibPath.c_str(), (IncFsSize)targetLibPath.size()}, + .signature = {}, }; incfs::FileId libFileId = idFromMetadata(targetLibPath); if (auto res = mIncFs->makeFile(ifs->control, targetLibPathAbsolute, 0755, libFileId, diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c5d42ad9f081..fadab1f8832e 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2714,16 +2714,18 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(AuthService.class); t.traceEnd(); - if (android.security.Flags.secureLockdown()) { - t.traceBegin("StartSecureLockDeviceService.Lifecycle"); - mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); - t.traceEnd(); - } + if (!isWatch && !isTv && !isAutomotive) { + if (android.security.Flags.secureLockdown()) { + t.traceBegin("StartSecureLockDeviceService.Lifecycle"); + mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); + t.traceEnd(); + } - if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { - t.traceBegin("StartAuthenticationPolicyService"); - mSystemServiceManager.startService(AuthenticationPolicyService.class); - t.traceEnd(); + if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { + t.traceBegin("StartAuthenticationPolicyService"); + mSystemServiceManager.startService(AuthenticationPolicyService.class); + t.traceEnd(); + } } if (!isWatch) { @@ -3128,13 +3130,8 @@ public final class SystemServer implements Dumpable { && context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_BLUETOOTH_LE))) { t.traceBegin("RangingService"); - // TODO: b/375264320 - Remove after RELEASE_RANGING_STACK is ramped to next. - try { - mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS, - RANGING_APEX_SERVICE_JAR_PATH); - } catch (Throwable e) { - Slog.d(TAG, "service-ranging.jar not found, not starting RangingService"); - } + mSystemServiceManager.startServiceFromJar(RANGING_SERVICE_CLASS, + RANGING_APEX_SERVICE_JAR_PATH); t.traceEnd(); } } diff --git a/services/print/java/com/android/server/print/flags.aconfig b/services/print/java/com/android/server/print/flags.aconfig index 0210791cfeda..42d142545660 100644 --- a/services/print/java/com/android/server/print/flags.aconfig +++ b/services/print/java/com/android/server/print/flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "do_not_include_capabilities" - namespace: "print" + namespace: "printing" description: "Do not use the flag Context.BIND_INCLUDE_CAPABILITIES when binding to the service" bug: "291281543" } diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java index 4e9fff230bac..ba64ed44cbb5 100644 --- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java +++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java @@ -37,6 +37,7 @@ import static org.testng.Assert.expectThrows; import android.annotation.UserIdInt; import android.app.Application; +import android.app.backup.BackupManagerInternal; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; import android.app.backup.IFullBackupRestoreObserver; @@ -52,6 +53,7 @@ import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.util.SparseArray; +import com.android.server.LocalServices; import com.android.server.SystemService.TargetUser; import com.android.server.backup.testing.TransportData; import com.android.server.testing.shadows.ShadowApplicationPackageManager; @@ -229,7 +231,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserOneId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserOneId, agentBinder); verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder); } @@ -242,7 +244,7 @@ public class BackupManagerServiceRoboTest { setCallerAndGrantInteractUserPermission(mUserTwoId, /* shouldGrantPermission */ false); IBinder agentBinder = mock(IBinder.class); - backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder); + backupManagerService.agentConnectedForUser(TEST_PACKAGE, mUserTwoId, agentBinder); verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE, agentBinder); @@ -1516,14 +1518,13 @@ public class BackupManagerServiceRoboTest { } /** - * Test verifying that {@link BackupManagerService#MORE_DEBUG} is set to {@code false}. This is + * Test verifying that {@link BackupManagerService#DEBUG} is set to {@code false}. This is * specifically to prevent overloading the logs in production. */ @Test - public void testMoreDebug_isFalse() throws Exception { - boolean moreDebug = BackupManagerService.MORE_DEBUG; - - assertThat(moreDebug).isFalse(); + public void testDebug_isFalse() { + boolean debug = BackupManagerService.DEBUG; + assertThat(debug).isFalse(); } /** Test that the constructor handles {@code null} parameters. */ @@ -1549,6 +1550,7 @@ public class BackupManagerServiceRoboTest { @Test public void testOnStart_publishesService() { BackupManagerService backupManagerService = mock(BackupManagerService.class); + LocalServices.removeServiceForTest(BackupManagerInternal.class); BackupManagerService.Lifecycle lifecycle = spy(new BackupManagerService.Lifecycle(mContext, backupManagerService)); doNothing().when(lifecycle).publishService(anyString(), any()); diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java index 2db2438b9a21..2749b0acf193 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupReporterTest.java @@ -57,8 +57,8 @@ public class KeyValueBackupReporterTest { } @Test - public void testMoreDebug_isFalse() { - assertThat(KeyValueBackupReporter.MORE_DEBUG).isFalse(); + public void testDebug_isFalse() { + assertThat(KeyValueBackupReporter.DEBUG).isFalse(); } @Test diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index de16b7ee8126..2dd16f68dc56 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -109,6 +109,7 @@ import com.android.server.EventLogTags; import com.android.server.LocalServices; import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.BackupRestoreTask; +import com.android.server.backup.BackupWakeLock; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.KeyValueBackupJob; import com.android.server.backup.PackageManagerBackupAgent; @@ -201,7 +202,7 @@ public class KeyValueBackupTaskTest { private TransportData mTransport; private ShadowLooper mShadowBackupLooper; private Handler mBackupHandler; - private UserBackupManagerService.BackupWakeLock mWakeLock; + private BackupWakeLock mWakeLock; private KeyValueBackupReporter mReporter; private PackageManager mPackageManager; private ShadowPackageManager mShadowPackageManager; @@ -238,7 +239,7 @@ public class KeyValueBackupTaskTest { mPackageManager = mApplication.getPackageManager(); mShadowPackageManager = shadowOf(mPackageManager); - mWakeLock = createBackupWakeLock(mApplication); + mWakeLock = spy(createBackupWakeLock(mApplication)); mBackupManager = spy(FakeIBackupManager.class); // Needed to be able to use a real BMS instead of a mock @@ -737,17 +738,16 @@ public class KeyValueBackupTaskTest { // In production (for non-system agents) the call is asynchronous, but here is // synchronous, so it's fine to verify here. // Verify has set work source and hasn't unset yet. - verify(mBackupManagerService) - .setWorkSource( - argThat(workSource -> workSource.getUid(0) == PACKAGE_1.uid)); - verify(mBackupManagerService, never()).setWorkSource(null); + verify(mWakeLock).setWorkSource( + argThat(workSource -> workSource.getUid(0) == PACKAGE_1.uid)); + verify(mWakeLock, never()).setWorkSource(null); }); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); runTask(task); // More verifications inside agent call above - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); } /** @@ -765,7 +765,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); verify(mObserver).backupFinished(SUCCESS); assertBackupPendingFor(PACKAGE_1); @@ -798,7 +798,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); verify(mObserver).backupFinished(SUCCESS); assertBackupPendingFor(PACKAGE_1); @@ -815,7 +815,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); verify(mObserver).backupFinished(SUCCESS); assertBackupPendingFor(PACKAGE_1); @@ -833,7 +833,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); verify(mObserver).onResult(PACKAGE_1.packageName, ERROR_AGENT_FAILURE); verify(mObserver).backupFinished(SUCCESS); assertBackupPendingFor(PACKAGE_1); @@ -864,7 +864,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); verify(mBackupAgentConnectionManager).unbindAgent(argThat(applicationInfo(PACKAGE_1)), eq(false)); } @@ -918,7 +918,7 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService).setWorkSource(null); + verify(mWakeLock).setWorkSource(null); } @Test diff --git a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java index 73ddbe8cec7c..ffec68aa4082 100644 --- a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java +++ b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java @@ -51,6 +51,7 @@ import android.platform.test.annotations.Presubmit; import com.android.server.EventLogTags; import com.android.server.backup.BackupAgentTimeoutParameters; +import com.android.server.backup.BackupWakeLock; import com.android.server.backup.OperationStorage; import com.android.server.backup.TransportManager; import com.android.server.backup.UserBackupManagerService; @@ -103,7 +104,7 @@ public class ActiveRestoreSessionTest { @Mock private OperationStorage mOperationStorage; private ShadowLooper mShadowBackupLooper; private ShadowApplication mShadowApplication; - private UserBackupManagerService.BackupWakeLock mWakeLock; + private BackupWakeLock mWakeLock; private TransportData mTransport; private RestoreSet mRestoreSet1; private RestoreSet mRestoreSet2; diff --git a/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java b/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java index 4d04c8b76fc7..10d23dc18e58 100644 --- a/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java +++ b/services/robotests/backup/src/com/android/server/backup/testing/BackupManagerServiceTestUtils.java @@ -39,6 +39,7 @@ import android.util.Log; import com.android.server.backup.BackupAgentTimeoutParameters; import com.android.server.backup.BackupManagerConstants; import com.android.server.backup.BackupManagerService; +import com.android.server.backup.BackupWakeLock; import com.android.server.backup.TransportManager; import com.android.server.backup.UserBackupManagerService; @@ -114,7 +115,7 @@ public class BackupManagerServiceTestUtils { TransportManager transportManager, PackageManager packageManager, Handler backupHandler, - UserBackupManagerService.BackupWakeLock wakeLock, + BackupWakeLock wakeLock, BackupAgentTimeoutParameters agentTimeoutParameters) { when(backupManagerService.getContext()).thenReturn(application); @@ -123,7 +124,7 @@ public class BackupManagerServiceTestUtils { when(backupManagerService.getBackupHandler()).thenReturn(backupHandler); when(backupManagerService.getQueueLock()).thenReturn(new Object()); when(backupManagerService.getActivityManager()).thenReturn(mock(IActivityManager.class)); - when(backupManagerService.getWakelock()).thenReturn(wakeLock); + when(backupManagerService.getWakeLock()).thenReturn(wakeLock); when(backupManagerService.getAgentTimeoutParameters()).thenReturn(agentTimeoutParameters); AccessorMock backupEnabled = mockAccessor(false); @@ -161,10 +162,12 @@ public class BackupManagerServiceTestUtils { }); } - public static UserBackupManagerService.BackupWakeLock createBackupWakeLock( - Application application) { + /** + * Creates a wakelock for testing. + */ + public static BackupWakeLock createBackupWakeLock(Application application) { PowerManager powerManager = application.getSystemService(PowerManager.class); - return new UserBackupManagerService.BackupWakeLock( + return new BackupWakeLock( powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"), 0, new BackupManagerConstants(Handler.getMain(), application.getContentResolver())); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java index 8ce2422563a3..70eeae648dd0 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java @@ -50,6 +50,7 @@ import android.view.inputmethod.InputMethodManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.annotations.GuardedBy; +import com.android.server.pm.UserManagerInternal; import com.android.server.wm.WindowManagerInternal; import org.junit.Before; @@ -74,6 +75,11 @@ public class ImeVisibilityStateComputerTest extends InputMethodManagerServiceTes super.setUp(); ImeVisibilityStateComputer.Injector injector = new ImeVisibilityStateComputer.Injector() { @Override + public UserManagerInternal getUserManagerService() { + return mMockUserManagerInternal; + } + + @Override public WindowManagerInternal getWmService() { return mMockWindowManagerInternal; } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java index b984624b3e9b..6adb01ccf11f 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/UserDataRepositoryTest.java @@ -23,6 +23,7 @@ import android.view.WindowManager; import androidx.annotation.NonNull; +import com.android.server.pm.UserManagerInternal; import com.android.server.wm.WindowManagerInternal; import org.junit.After; @@ -49,6 +50,9 @@ public final class UserDataRepositoryTest { private InputMethodManagerService mMockInputMethodManagerService; @Mock + private UserManagerInternal mMockUserManagerInternal; + + @Mock private WindowManagerInternal mMockWindowManagerInternal; @NonNull @@ -70,6 +74,12 @@ public final class UserDataRepositoryTest { new ImeVisibilityStateComputer.Injector() { @NonNull @Override + public UserManagerInternal getUserManagerService() { + return mMockUserManagerInternal; + } + + @NonNull + @Override public WindowManagerInternal getWmService() { return mMockWindowManagerInternal; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index a9ad435762ad..02e5470e8673 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -415,7 +415,6 @@ public class DisplayManagerServiceTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); mLocalServiceKeeperRule.overrideLocalService( InputManagerInternal.class, mMockInputManagerInternal); @@ -2797,30 +2796,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withoutDisplayManagement_shouldAddDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); - manageDisplaysPermission(/* granted= */ true); - DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); - DisplayManagerService.BinderService bs = displayManager.new BinderService(); - LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); - FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); - bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); - callback.expectsEvent(EVENT_DISPLAY_ADDED); - - FakeDisplayDevice displayDevice = - createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_EXTERNAL); - callback.waitForExpectedEvent(); - - LogicalDisplay display = - logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true); - assertThat(display.isEnabledLocked()).isTrue(); - assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_ADDED); - - } - - @Test - public void testConnectExternalDisplay_withDisplayManagement_shouldDisableDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectExternalDisplay_shouldDisableDisplay() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -2849,9 +2825,8 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withDisplayManagementAndSysprop_shouldEnableDisplay() { + public void testConnectExternalDisplay_withSysprop_shouldEnableDisplay() { Assume.assumeTrue(Build.IS_ENG || Build.IS_USERDEBUG); - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); doAnswer((Answer<Boolean>) invocationOnMock -> true) .when(() -> SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)); manageDisplaysPermission(/* granted= */ true); @@ -2883,8 +2858,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectExternalDisplay_withDisplayManagement_allowsEnableAndDisableDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectExternalDisplay_allowsEnableAndDisableDisplay() { when(mMockFlags.isApplyDisplayChangedDuringDisplayAddedEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); LocalServices.addService(WindowManagerPolicy.class, mMockedWindowManagerPolicy); @@ -2955,8 +2929,7 @@ public class DisplayManagerServiceTest { } @Test - public void testConnectInternalDisplay_withDisplayManagement_shouldConnectAndAddDisplay() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testConnectInternalDisplay_shouldConnectAndAddDisplay() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); @@ -3011,7 +2984,7 @@ public class DisplayManagerServiceTest { DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); - bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS); + bs.registerCallbackWithEventMask(callback, STANDARD_DISPLAY_EVENTS); callback.expectsEvent(EVENT_DISPLAY_ADDED); FakeDisplayDevice displayDevice = @@ -3032,8 +3005,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableExternalDisplay_shouldSignalDisplayAdded() { manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3062,8 +3034,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableExternalDisplay_withoutPermission_shouldThrowException() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableExternalDisplay_shouldThrowException() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3087,8 +3058,7 @@ public class DisplayManagerServiceTest { } @Test - public void testEnableInternalDisplay_withManageDisplays_shouldSignalAdded() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testEnableInternalDisplay_shouldSignalAdded() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3115,8 +3085,7 @@ public class DisplayManagerServiceTest { } @Test - public void testDisableInternalDisplay_withDisplayManagement_shouldSignalRemove() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisableInternalDisplay_shouldSignalRemove() { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3140,7 +3109,6 @@ public class DisplayManagerServiceTest { @Test public void testDisableExternalDisplay_shouldSignalDisplayRemoved() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3181,7 +3149,6 @@ public class DisplayManagerServiceTest { @Test public void testDisableExternalDisplay_withoutPermission_shouldThrowException() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper(); @@ -3207,7 +3174,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveExternalDisplay_whenDisabled_shouldSignalDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3244,7 +3210,6 @@ public class DisplayManagerServiceTest { @Test public void testRegisterCallback_withoutPermission_shouldThrow() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); @@ -3255,7 +3220,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveExternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); displayManager.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); @@ -3288,7 +3252,6 @@ public class DisplayManagerServiceTest { @Test public void testRemoveInternalDisplay_whenEnabled_shouldSignalRemovedAndDisconnected() { - when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); manageDisplaysPermission(/* granted= */ true); DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerService.BinderService bs = displayManager.new BinderService(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java index 782262d3f7c9..a48a88cecbc2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java @@ -22,7 +22,6 @@ import static android.view.Display.TYPE_INTERNAL; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -47,7 +46,6 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager; import com.android.server.testutils.TestHandler; -import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; @@ -124,7 +122,6 @@ public class ExternalDisplayPolicyTest { public void setup() throws Exception { MockitoAnnotations.initMocks(this); mHandler = new TestHandler(/*callback=*/ null); - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(true); when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn(true); when(mMockedInjector.getFlags()).thenReturn(mMockedFlags); when(mMockedInjector.getLogicalDisplayMapper()).thenReturn(mMockedLogicalDisplayMapper); @@ -173,16 +170,6 @@ public class ExternalDisplayPolicyTest { } @Test - public void testTryEnableExternalDisplay_featureDisabled(@TestParameter final boolean enable) { - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn(false); - mExternalDisplayPolicy.setExternalDisplayEnabledLocked(mMockedLogicalDisplay, enable); - mHandler.flush(); - verify(mMockedLogicalDisplayMapper, never()).setDisplayEnabledLocked(any(), anyBoolean()); - verify(mMockedDisplayNotificationManager, never()) - .onHighTemperatureExternalDisplayNotAllowed(); - } - - @Test public void testTryDisableExternalDisplay_criticalThermalCondition() throws RemoteException { // Disallow external displays due to thermals. setTemperature(registerThermalListener(), List.of(CRITICAL_TEMPERATURE)); @@ -278,21 +265,6 @@ public class ExternalDisplayPolicyTest { } @Test - public void testNoThermalListenerRegistered_featureDisabled( - @TestParameter final boolean isConnectedDisplayManagementEnabled, - @TestParameter final boolean isErrorHandlingEnabled) throws RemoteException { - assumeFalse(isConnectedDisplayManagementEnabled && isErrorHandlingEnabled); - when(mMockedFlags.isConnectedDisplayManagementEnabled()).thenReturn( - isConnectedDisplayManagementEnabled); - when(mMockedFlags.isConnectedDisplayErrorHandlingEnabled()).thenReturn( - isErrorHandlingEnabled); - - mExternalDisplayPolicy.onBootCompleted(); - verify(mMockedThermalService, never()).registerThermalEventListenerWithType( - any(), anyInt()); - } - - @Test public void testOnCriticalTemperature_disallowAndAllowExternalDisplay() throws RemoteException { final var thermalListener = registerThermalListener(); diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index 0dbb6ba58b3c..7d3cd8a8a9ae 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -222,7 +222,6 @@ public class LogicalDisplayMapperTest { when(mSyntheticModeManagerMock.createAppSupportedModes(any(), any(), anyBoolean())) .thenAnswer(AdditionalAnswers.returnsSecondArg()); - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false); mLooper = new TestLooper(); mHandler = new Handler(mLooper.getLooper()); mLogicalDisplayMapper = new LogicalDisplayMapper(mContextMock, mFoldSettingProviderMock, @@ -351,8 +350,7 @@ public class LogicalDisplayMapperTest { } @Test - public void testDisplayDeviceAddAndRemove_withDisplayManagement() { - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisplayDeviceAddAndRemove() { DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); @@ -390,8 +388,7 @@ public class LogicalDisplayMapperTest { } @Test - public void testDisplayDisableEnable_withDisplayManagement() { - when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(true); + public void testDisplayDisableEnable() { DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); LogicalDisplay displayAdded = add(device); @@ -1350,9 +1347,14 @@ public class LogicalDisplayMapperTest { ArgumentCaptor<LogicalDisplay> displayCaptor = ArgumentCaptor.forClass(LogicalDisplay.class); verify(mListenerMock).onLogicalDisplayEventLocked( - displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_ADDED)); + displayCaptor.capture(), eq(LOGICAL_DISPLAY_EVENT_CONNECTED)); + LogicalDisplay display = displayCaptor.getValue(); + if (display.isEnabledLocked()) { + verify(mListenerMock).onLogicalDisplayEventLocked( + eq(display), eq(LOGICAL_DISPLAY_EVENT_ADDED)); + } clearInvocations(mListenerMock); - return displayCaptor.getValue(); + return display; } private void testDisplayDeviceAddAndRemove_NonInternal(int type) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 6defadf44d05..35ab2d233563 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -1454,6 +1454,18 @@ public class ActivityManagerServiceTest { assertThat(tokenForFullIntent.getKeyFields()).isEqualTo(tokenForCloneIntent.getKeyFields()); } + @Test + public void testCanLaunchClipDataIntent() { + ClipData clipData = ClipData.newIntent("test", new Intent("test")); + clipData.prepareToLeaveProcess(true); + // skip mimicking sending clipData to another app because it will just be parceled and + // un-parceled. + Intent intent = clipData.getItemAt(0).getIntent(); + // default intent redirect protection won't block an intent nested in a top level ClipData. + assertThat(intent.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); + } + private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq, long lastNetworkUpdatedProcStateSeq, final long procStateSeqToWait, boolean expectWait) throws Exception { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java index 4a09802fc822..fe7cc923d3d1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java @@ -743,6 +743,43 @@ public class MockingOomAdjusterTests { @SuppressWarnings("GuardedBy") @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_receivers() { + final ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, + MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true); + + updateOomAdj(app); + assertNoCpuTime(app); + + app.mReceivers.incrementCurReceivers(); + updateOomAdj(app); + assertCpuTime(app); + + app.mReceivers.decrementCurReceivers(); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test + @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY) + public void testUpdateOomAdjFreezeState_activeInstrumentation() { + ProcessRecord app = makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, + MOCKAPP_PACKAGENAME, true); + updateOomAdj(app); + assertNoCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, mock(ActiveInstrumentation.class)); + updateOomAdj(app); + assertCpuTime(app); + + mProcessStateController.setActiveInstrumentation(app, null); + updateOomAdj(app); + assertNoCpuTime(app); + } + + @SuppressWarnings("GuardedBy") + @Test public void testUpdateOomAdj_DoOne_OverlayUi() { ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID, MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true)); diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java index c4a042370de5..f1f4a0e83a79 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupManagerServiceTest.java @@ -39,6 +39,7 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.app.backup.BackupManager; +import android.app.backup.BackupManagerInternal; import android.app.backup.ISelectBackupTransportCallback; import android.app.job.JobScheduler; import android.content.ComponentName; @@ -59,6 +60,7 @@ import androidx.test.filters.SmallTest; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.internal.util.DumpUtils; +import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.backup.utils.RandomAccessFileUtils; @@ -716,7 +718,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -730,7 +732,7 @@ public class BackupManagerServiceTest { // Create BMS *before* setting a main user to simulate the main user being created after // BMS, which can happen for the first ever boot of a new device. mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(NON_SYSTEM_USER)); assertFalse(mService.isBackupServiceActive(NON_SYSTEM_USER)); @@ -754,7 +756,7 @@ public class BackupManagerServiceTest { private void createBackupManagerServiceAndUnlockSystemUser() { mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); simulateUserUnlocked(UserHandle.USER_SYSTEM); } @@ -765,7 +767,15 @@ public class BackupManagerServiceTest { private void setMockMainUserAndCreateBackupManagerService(int userId) { when(mUserManagerMock.getMainUser()).thenReturn(UserHandle.of(userId)); mService = new BackupManagerServiceTestable(mContextMock); - mServiceLifecycle = new BackupManagerService.Lifecycle(mContextMock, mService); + createBackupServiceLifecycle(mContextMock, mService); + } + + private void createBackupServiceLifecycle(Context context, BackupManagerService service) { + // Anytime we manually create the Lifecycle, we need to remove the internal BMS because + // it would've been added already at boot time and LocalServices does not allow + // overriding an existing service. + LocalServices.removeServiceForTest(BackupManagerInternal.class); + mServiceLifecycle = new BackupManagerService.Lifecycle(context, service); } private void simulateUserUnlocked(int userId) { diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java index e618433862f2..2b7a62a9f9fe 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java @@ -35,6 +35,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.BackupAgentTimeoutParameters; +import com.android.server.backup.BackupWakeLock; import com.android.server.backup.OperationStorage; import com.android.server.backup.TransportManager; import com.android.server.backup.UserBackupManagerService; @@ -81,7 +82,7 @@ public class PerformFullTransportBackupTaskTest { @Mock TransportManager mTransportManager; @Mock - UserBackupManagerService.BackupWakeLock mWakeLock; + BackupWakeLock mWakeLock; private final List<String> mEligiblePackages = new ArrayList<>(); @@ -94,7 +95,7 @@ public class PerformFullTransportBackupTaskTest { when(mBackupManagerService.getPackageManager()).thenReturn(mPackageManager); when(mBackupManagerService.getQueueLock()).thenReturn("something!"); when(mBackupManagerService.isEnabled()).thenReturn(true); - when(mBackupManagerService.getWakelock()).thenReturn(mWakeLock); + when(mBackupManagerService.getWakeLock()).thenReturn(mWakeLock); when(mBackupManagerService.isSetupComplete()).thenReturn(true); when(mBackupManagerService.getAgentTimeoutParameters()).thenReturn( mBackupAgentTimeoutParameters); diff --git a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java index f442eb69594e..ebaa2e84c044 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/fudger/LocationFudgerTest.java @@ -271,4 +271,31 @@ public class LocationFudgerTest { assertThat(center[0]).isEqualTo(expected[0]); assertThat(center[1]).isEqualTo(expected[1]); } + + @Test + public void getS2CellApproximateEdge_returnsCorrectRadius() { + int level = 10; + + float radius = mFudger.getS2CellApproximateEdge(level); + + assertThat(radius).isEqualTo(9000); // in meters + } + + @Test + public void getS2CellApproximateEdge_doesNotThrow() { + int level = -1; + + mFudger.getS2CellApproximateEdge(level); + + // No exception thrown. + } + + @Test + public void getS2CellApproximateEdge_doesNotThrow2() { + int level = 14; + + mFudger.getS2CellApproximateEdge(level); + + // No exception thrown. + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java index f1072da4161f..6d91bee6d3f6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/power/ThermalManagerServiceMockingTest.java @@ -573,4 +573,14 @@ public class ThermalManagerServiceMockingTest { assertNotNull(ret); assertEquals(0, ret.size()); } + + @Test + public void forecastSkinTemperature() throws RemoteException { + Mockito.when(mAidlHalMock.forecastSkinTemperature(Mockito.anyInt())).thenReturn( + 0.55f + ); + float forecast = mAidlWrapper.forecastSkinTemperature(10); + Mockito.verify(mAidlHalMock, Mockito.times(1)).forecastSkinTemperature(10); + assertEquals(0.55f, forecast, 0.01f); + } } diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java index cd94c0f6e245..e61571288ade 100644 --- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -70,8 +70,6 @@ import android.os.PerformanceHintManager; import android.os.Process; import android.os.RemoteException; import android.os.SessionCreationConfig; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -1388,7 +1386,6 @@ public class HintManagerServiceTest { @Test - @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) public void testCpuHeadroomCache() throws Exception { CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal(); CpuHeadroomParams halParams1 = new CpuHeadroomParams(); @@ -1476,8 +1473,7 @@ public class HintManagerServiceTest { } @Test - @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) - public void testGetCpuHeadroomDifferentAffinity_flagOn() throws Exception { + public void testGetCpuHeadroomDifferentAffinity() throws Exception { CountDownLatch latch = new CountDownLatch(2); int[] tids = createThreads(2, latch); CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); @@ -1497,28 +1493,6 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getCpuHeadroom(any()); } - @Test - @DisableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) - public void testGetCpuHeadroomDifferentAffinity_flagOff() throws Exception { - CountDownLatch latch = new CountDownLatch(2); - int[] tids = createThreads(2, latch); - CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); - params.tids = tids; - CpuHeadroomParams halParams = new CpuHeadroomParams(); - halParams.tids = tids; - float headroom = 0.1f; - CpuHeadroomResult halRet = CpuHeadroomResult.globalHeadroom(headroom); - String ret1 = runAndWaitForCommand("taskset -p 1 " + tids[0]); - String ret2 = runAndWaitForCommand("taskset -p 3 " + tids[1]); - - HintManagerService service = createService(); - clearInvocations(mIPowerMock); - when(mIPowerMock.getCpuHeadroom(eq(halParams))).thenReturn(halRet); - assertEquals("taskset cmd return: " + ret1 + "\n" + ret2, halRet, - service.getBinderServiceInstance().getCpuHeadroom(params)); - verify(mIPowerMock, times(1)).getCpuHeadroom(any()); - } - private String runAndWaitForCommand(String command) throws Exception { java.lang.Process process = Runtime.getRuntime().exec(command); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 3cb27451bd57..d9256247b835 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -89,6 +89,7 @@ import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.UserHandle; import android.os.test.TestLooper; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; @@ -119,6 +120,7 @@ import com.android.server.power.PowerManagerService.WakeLock; import com.android.server.power.batterysaver.BatterySaverController; import com.android.server.power.batterysaver.BatterySaverPolicy; import com.android.server.power.batterysaver.BatterySaverStateMachine; +import com.android.server.power.feature.flags.Flags; import com.android.server.power.feature.PowerManagerFlags; import com.android.server.testutils.OffsettableClock; @@ -3456,6 +3458,25 @@ public class PowerManagerServiceTest { } @Test + @DisableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) + public void testAcquireWakelock_screenTimeoutListenersDisabled_noCrashes() { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + + acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK, + Display.DEFAULT_DISPLAY); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void testAddWakeLockKeepingScreenOn_addsToNotifierAndReportsTimeoutPolicyChange() { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); @@ -3483,6 +3504,7 @@ public class PowerManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void test_addAndRemoveScreenTimeoutListener_propagatesToNotifier() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); @@ -3509,6 +3531,7 @@ public class PowerManagerServiceTest { } @Test + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) public void test_displayIsRemoved_clearsScreenTimeoutListeners() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); @@ -3531,7 +3554,8 @@ public class PowerManagerServiceTest { } @Test - public void testScreenWakeLockListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() + @EnableFlags(Flags.FLAG_ENABLE_SCREEN_TIMEOUT_POLICY_LISTENER_API) + public void testScreenTimeoutListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() throws Exception { IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index d6ca10a23fb9..07b18db59960 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -27,6 +27,7 @@ android_test { "servicestests-utils", "platform-test-annotations", "flag-junit", + "apct-perftests-utils", ], libs: [ @@ -64,10 +65,12 @@ android_ravenwood_test { "ravenwood-junit", "truth", "androidx.annotation_annotation", + "androidx.test.ext.junit", "androidx.test.rules", "androidx.test.uiautomator_uiautomator", "modules-utils-binary-xml", "flag-junit", + "apct-perftests-utils", ], srcs: [ "src/com/android/server/power/stats/*.java", diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java new file mode 100644 index 000000000000..cc75e9e3114f --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTraceTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2025 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.power.stats; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.BatteryStatsManager; +import android.os.BatteryUsageStats; +import android.os.BatteryUsageStatsQuery; +import android.os.ParcelFileDescriptor; +import android.perftests.utils.TraceMarkParser; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.uiautomator.UiDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@LargeTest +@android.platform.test.annotations.DisabledOnRavenwood(reason = "Atrace event test") +public class BatteryStatsHistoryTraceTest { + private static final String ATRACE_START = "atrace --async_start -b 1024 -c ss"; + private static final String ATRACE_STOP = "atrace --async_stop"; + private static final String ATRACE_DUMP = "atrace --async_dump"; + + @Before + public void before() throws Exception { + runShellCommand(ATRACE_START); + } + + @After + public void after() throws Exception { + runShellCommand(ATRACE_STOP); + } + + @Test + public void dumpsys() throws Exception { + runShellCommand("dumpsys batterystats --history"); + + Set<String> slices = readAtraceSlices(); + assertThat(slices).contains("BatteryStatsHistory.copy"); + assertThat(slices).contains("BatteryStatsHistory.iterate"); + } + + @Test + public void getBatteryUsageStats() throws Exception { + BatteryStatsManager batteryStatsManager = + getInstrumentation().getTargetContext().getSystemService(BatteryStatsManager.class); + BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .includeBatteryHistory().build(); + BatteryUsageStats batteryUsageStats = batteryStatsManager.getBatteryUsageStats(query); + assertThat(batteryUsageStats).isNotNull(); + + Set<String> slices = readAtraceSlices(); + assertThat(slices).contains("BatteryStatsHistory.copy"); + assertThat(slices).contains("BatteryStatsHistory.iterate"); + assertThat(slices).contains("BatteryStatsHistory.writeToParcel"); + } + + private String runShellCommand(String cmd) throws Exception { + return UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd); + } + + private Set<String> readAtraceSlices() throws Exception { + Set<String> keys = new HashSet<>(); + + TraceMarkParser parser = new TraceMarkParser( + line -> line.name.startsWith("BatteryStatsHistory.")); + ParcelFileDescriptor pfd = + getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(new ParcelFileDescriptor.AutoCloseInputStream(pfd)))) { + String line; + while ((line = reader.readLine()) != null) { + parser.visit(line); + } + } + parser.forAllSlices((key, slices) -> keys.add(key)); + return keys; + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java index 115cdf6cee63..e654b40af1b5 100644 --- a/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java +++ b/services/tests/powerstatstests/src/com/android/server/powerstats/PowerStatsServiceTest.java @@ -37,11 +37,13 @@ import android.os.Bundle; import android.os.IPowerStatsService; import android.os.Looper; import android.os.PowerMonitor; +import android.os.PowerMonitorReadings; import android.os.ResultReceiver; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; +import android.util.IntArray; import androidx.test.InstrumentationRegistry; @@ -129,6 +131,8 @@ public class PowerStatsServiceTest { } } + private final IntArray mFinePowerMonitorsPermissionGranted = new IntArray(); + private final PowerStatsService.Injector mInjector = new PowerStatsService.Injector() { @Override @@ -220,6 +224,11 @@ public class PowerStatsServiceTest { IntervalRandomNoiseGenerator createIntervalRandomNoiseGenerator() { return mMockNoiseGenerator; } + + @Override + boolean checkFinePowerMonitorsPermission(Context context, int callingUid) { + return mFinePowerMonitorsPermissionGranted.contains(callingUid); + } }; public static final class TestPowerStatsHALWrapper implements IPowerStatsHALWrapper { @@ -1109,6 +1118,8 @@ public class PowerStatsServiceTest { public int resultCode; public long[] energyUws; public long[] timestamps; + @PowerMonitorReadings.PowerMonitorGranularity + public int granularity; GetPowerMonitorsResult() { super(null); @@ -1120,12 +1131,23 @@ public class PowerStatsServiceTest { if (resultData != null) { energyUws = resultData.getLongArray(IPowerStatsService.KEY_ENERGY); timestamps = resultData.getLongArray(IPowerStatsService.KEY_TIMESTAMPS); + granularity = resultData.getInt(IPowerStatsService.KEY_GRANULARITY); } } } @Test public void getPowerMonitors() { + testGetPowerMonitors(PowerMonitorReadings.GRANULARITY_UNSPECIFIED); + } + + @Test + public void getPowerMonitors_finePowerMonitorPermissionGranted() { + mFinePowerMonitorsPermissionGranted.add(APP_UID); + testGetPowerMonitors(PowerMonitorReadings.GRANULARITY_FINE); + } + + private void testGetPowerMonitors(int expectedGranularity) { mMockClock.realtime = 10 * 60_000; mMockNoiseGenerator.reseed(314); @@ -1161,6 +1183,7 @@ public class PowerStatsServiceTest { assertThat(result.energyUws).isEqualTo(new long[]{42, 142, 314, 514}); assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_100, 600_000, 600_200}); + assertThat(result.granularity).isEqualTo(expectedGranularity); // Test caching/throttling mMockClock.realtime += 1; @@ -1180,6 +1203,7 @@ public class PowerStatsServiceTest { assertThat(result.energyUws).isEqualTo(new long[]{42, 314}); assertThat(result.timestamps).isEqualTo(new long[]{600_000, 600_000}); + assertThat(result.granularity).isEqualTo(expectedGranularity); mMockClock.realtime += 10 * 60000; @@ -1189,6 +1213,7 @@ public class PowerStatsServiceTest { // This time, random noise is added assertThat(result.energyUws).isEqualTo(new long[]{298, 399}); assertThat(result.timestamps).isEqualTo(new long[]{600_301, 600_401}); + assertThat(result.granularity).isEqualTo(expectedGranularity); } @Test @@ -1234,7 +1259,6 @@ public class PowerStatsServiceTest { assertThrows(NullPointerException.class, () -> iPowerStatsService.getPowerMonitorReadings( new int[] {0}, null)); } - @Test public void getEnergyConsumedAsync_halException() { mPowerStatsHALWrapper.exception = new IllegalArgumentException(); diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java index e86108d84538..ede61a5a0269 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -15,18 +15,14 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; import static com.google.common.truth.Truth.assertThat; -import android.provider.DeviceConfig; - import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,24 +41,12 @@ public class SelinuxAuditLogsBuilderTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - - mAuditLogBuilder = new SelinuxAuditLogBuilder(); + mAuditLogBuilder = new SelinuxAuditLogBuilder(TEST_DOMAIN); mScontextMatcher = mAuditLogBuilder.mScontextMatcher; mTcontextMatcher = mAuditLogBuilder.mTcontextMatcher; mPathMatcher = mAuditLogBuilder.mPathMatcher; } - @After - public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); - } - @Test public void testMatcher_scontext() { assertThat(mScontextMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isTrue(); @@ -109,13 +93,9 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testMatcher_scontextDefaultConfig() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.clearLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN)); - - Matcher scontexMatcher = new SelinuxAuditLogBuilder().mScontextMatcher; + Matcher scontexMatcher = + new SelinuxAuditLogBuilder(SelinuxAuditLogsCollector.DEFAULT_SELINUX_AUDIT_DOMAIN) + .mScontextMatcher; assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0").matches()).isFalse(); assertThat(scontexMatcher.reset("u:r:" + TEST_DOMAIN + ":s0:c123,c456").matches()) @@ -221,13 +201,7 @@ public class SelinuxAuditLogsBuilderTest { @Test public void testSelinuxAuditLogsBuilder_wrongConfig() { String notARegexDomain = "not]a[regex"; - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - notARegexDomain)); - SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(); + SelinuxAuditLogBuilder noOpBuilder = new SelinuxAuditLogBuilder(notARegexDomain); noOpBuilder.reset( "granted { p } scontext=u:r:" diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java index b6ccf5e0ad80..29f55ff53e6e 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -15,12 +15,12 @@ */ package com.android.server.selinux; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -28,7 +28,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; -import android.provider.DeviceConfig; import android.util.EventLog; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -59,6 +58,7 @@ public class SelinuxAuditLogsCollectorTest { private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = // Ignore rate limiting for tests new SelinuxAuditLogsCollector( + () -> TEST_DOMAIN, new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), new QuotaLimiter( mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); @@ -67,13 +67,6 @@ public class SelinuxAuditLogsCollectorTest { @Before public void setUp() { - runWithShellPermissionIdentity( - () -> - DeviceConfig.setLocalOverride( - DeviceConfig.NAMESPACE_ADSERVICES, - SelinuxAuditLogBuilder.CONFIG_SELINUX_AUDIT_DOMAIN, - TEST_DOMAIN)); - mSelinuxAutidLogsCollector.setStopRequested(false); // move the clock forward for the limiters. mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); @@ -85,12 +78,11 @@ public class SelinuxAuditLogsCollectorTest { @After public void tearDown() { - runWithShellPermissionIdentity(() -> DeviceConfig.clearAllLocalOverrides()); mMockitoSession.finishMocking(); } @Test - public void testWriteAuditLogs() { + public void testWriteAuditLogs() throws Exception { writeTestLog("granted", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm1", TEST_DOMAIN, "ttype1", "tclass1"); @@ -126,7 +118,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_multiplePerms() { + public void testWriteAuditLogs_multiplePerms() throws Exception { writeTestLog("denied", "perm1 perm2", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm3 perm4", TEST_DOMAIN, "ttype", "tclass"); @@ -162,7 +154,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_withPaths() { + public void testWriteAuditLogs_withPaths() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", "/good/path"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", "/very/long/path"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", "/short_path"); @@ -226,7 +218,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_withCategories() { + public void testWriteAuditLogs_withCategories() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, new int[] {123}, "ttype", null, "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, new int[] {123, 456}, "ttype", null, "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, null, "ttype", new int[] {666}, "tclass"); @@ -297,7 +289,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_withPathAndCategories() { + public void testWriteAuditLogs_withPathAndCategories() throws Exception { writeTestLog( "denied", "perm", @@ -327,7 +319,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_permissive() { + public void testWriteAuditLogs_permissive() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", true); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", false); @@ -365,7 +357,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testNotWriteAuditLogs_notTestDomain() { + public void testNotWriteAuditLogs_notTestDomain() throws Exception { writeTestLog("denied", "perm", "stype", "ttype", "tclass"); boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); @@ -388,7 +380,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_upToQuota() { + public void testWriteAuditLogs_upToQuota() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); @@ -398,9 +390,9 @@ public class SelinuxAuditLogsCollectorTest { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); - boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThrows(QuotaExceededException.class, () -> + mSelinuxAutidLogsCollector.collect(ANSWER_TAG)); - assertThat(done).isTrue(); verify( () -> FrameworkStatsLog.write( @@ -418,7 +410,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testWriteAuditLogs_resetQuota() { + public void testWriteAuditLogs_resetQuota() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); @@ -427,8 +419,8 @@ public class SelinuxAuditLogsCollectorTest { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); - boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); - assertThat(done).isTrue(); + assertThrows(QuotaExceededException.class, () -> + mSelinuxAutidLogsCollector.collect(ANSWER_TAG)); verify( () -> FrameworkStatsLog.write( @@ -451,8 +443,8 @@ public class SelinuxAuditLogsCollectorTest { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); // move the clock forward to reset the quota limiter. mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); - done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); - assertThat(done).isTrue(); + assertThrows(QuotaExceededException.class, () -> + mSelinuxAutidLogsCollector.collect(ANSWER_TAG)); verify( () -> FrameworkStatsLog.write( @@ -470,14 +462,11 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testNotWriteAuditLogs_stopRequested() { - writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); + public void testNotWriteAuditLogs_stopRequested() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); - // These are not pushed. - writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); mSelinuxAutidLogsCollector.setStopRequested(true); @@ -518,7 +507,7 @@ public class SelinuxAuditLogsCollectorTest { } @Test - public void testAuditLogs_resumeJobDoesNotExceedLimit() { + public void testAuditLogs_resumeJobDoesNotExceedLimit() throws Exception { writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass"); mSelinuxAutidLogsCollector.setStopRequested(true); diff --git a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsJobTest.java b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsJobTest.java index 2aea8a033f87..344f3295f682 100644 --- a/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsJobTest.java +++ b/services/tests/selinux/src/com/android/server/selinux/SelinuxAuditLogsJobTest.java @@ -53,7 +53,7 @@ public class SelinuxAuditLogsJobTest { } @Test - public void testFinishSuccessfully() { + public void testFinishSuccessfully() throws Exception { when(mAuditLogsCollector.collect(anyInt())).thenReturn(true); mAuditLogsJob.start(mJobService, mParams); @@ -63,7 +63,7 @@ public class SelinuxAuditLogsJobTest { } @Test - public void testInterrupt() { + public void testInterrupt() throws Exception { when(mAuditLogsCollector.collect(anyInt())).thenReturn(false); mAuditLogsJob.start(mJobService, mParams); @@ -73,7 +73,7 @@ public class SelinuxAuditLogsJobTest { } @Test - public void testInterruptAndResume() { + public void testInterruptAndResume() throws Exception { when(mAuditLogsCollector.collect(anyInt())).thenReturn(false); mAuditLogsJob.start(mJobService, mParams); verify(mJobService, never()).jobFinished(any(), anyBoolean()); @@ -85,7 +85,7 @@ public class SelinuxAuditLogsJobTest { } @Test - public void testRequestStop() throws InterruptedException { + public void testRequestStop() throws Exception { Semaphore isRunning = new Semaphore(0); Semaphore stopRequested = new Semaphore(0); AtomicReference<Throwable> uncaughtException = new AtomicReference<>(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index fa78dfce0a17..4d8aef49f080 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -220,6 +220,9 @@ public class AccessibilityManagerServiceTest { @Mock private ProxyManager mProxyManager; @Mock private StatusBarManagerInternal mStatusBarManagerInternal; @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock + private HearingDevicePhoneCallNotificationController + mMockHearingDevicePhoneCallNotificationController; @Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback; @Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor; private IAccessibilityManager mA11yManagerServiceOnDevice; @@ -289,7 +292,8 @@ public class AccessibilityManagerServiceTest { mMockMagnificationController, mInputFilter, mProxyManager, - mFakePermissionEnforcer); + mFakePermissionEnforcer, + mMockHearingDevicePhoneCallNotificationController); mA11yms.switchUser(mTestableContext.getUserId()); mTestableLooper.processAllMessages(); @@ -825,26 +829,6 @@ public class AccessibilityManagerServiceTest { @SmallTest @Test - @DisableFlags(com.android.systemui.Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) - public void testPerformAccessibilityShortcut_hearingAids_startActivityWithExpectedComponent() { - final AccessibilityUserState userState = mA11yms.mUserStates.get( - mA11yms.getCurrentUserIdLocked()); - mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY); - userState.updateShortcutTargetsLocked( - Set.of(ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()), HARDWARE); - - mA11yms.performAccessibilityShortcut( - Display.DEFAULT_DISPLAY, HARDWARE, - ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()); - mTestableLooper.processAllMessages(); - - assertStartActivityWithExpectedComponentName(mTestableContext.getMockContext(), - ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()); - } - - @SmallTest - @Test - @EnableFlags(com.android.systemui.Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG) public void testPerformAccessibilityShortcut_hearingAids_sendExpectedBroadcast() { final AccessibilityUserState userState = mA11yms.mUserStates.get( mA11yms.getCurrentUserIdLocked()); diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java index 0bf419ec242c..998c1d1a23b1 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -35,6 +36,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.UserInfo; import android.os.Binder; import android.os.Bundle; import android.os.DropBoxManager; @@ -48,6 +50,7 @@ import android.os.Parcel; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; import android.provider.Settings; @@ -68,6 +71,7 @@ import org.junit.Test; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -150,6 +154,25 @@ public class ActivityManagerTest { } @Test + public void testRemovedUserShouldNotBeRunning() throws Exception { + final UserManager userManager = mContext.getSystemService(UserManager.class); + assertNotNull("UserManager should not be null", userManager); + final UserInfo user = userManager.createUser( + "TestUser", UserManager.USER_TYPE_FULL_SECONDARY, 0); + + mService.startUserInBackground(user.id); + assertTrue("User should be running", mService.isUserRunning(user.id, 0)); + assertTrue("User should be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + + userManager.removeUser(user.id); + mService.startUserInBackground(user.id); + assertFalse("Removed user should not be running", mService.isUserRunning(user.id, 0)); + assertFalse("Removed user should not be in running users", + Arrays.stream(mService.getRunningUserIds()).anyMatch(x -> x == user.id)); + } + + @Test public void testServiceUnbindAndKilling() { for (int i = TEST_LOOPS; i > 0; i--) { runOnce(i); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 6411463fe0d9..06958b81d846 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -490,29 +490,6 @@ public class UserControllerTest { mInjector.mHandler.clearAllRecordedMessages(); // Verify that continueUserSwitch worked as expected continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(0)).dismissKeyguard(any()); - verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); - continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); - verifySystemUserVisibilityChangesNeverNotified(); - } - - @Test - public void testContinueUserSwitchDismissKeyguard() { - when(mInjector.mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(false); - mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, - /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false, - /* backgroundUserScheduledStopTimeSecs= */ -1); - // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); - Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); - assertNotNull(reportMsg); - UserState userState = (UserState) reportMsg.obj; - int oldUserId = reportMsg.arg1; - int newUserId = reportMsg.arg2; - mInjector.mHandler.clearAllRecordedMessages(); - // Verify that continueUserSwitch worked as expected - continueAndCompleteUserSwitch(userState, oldUserId, newUserId); - verify(mInjector, times(1)).dismissKeyguard(any()); verify(mInjector, times(1)).dismissUserSwitchingDialog(any()); continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false, false); verifySystemUserVisibilityChangesNeverNotified(); @@ -1923,11 +1900,6 @@ public class UserControllerTest { } @Override - protected void dismissKeyguard(Runnable runnable) { - runnable.run(); - } - - @Override void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser, String switchingFromSystemUserMessage, String switchingToSystemUserMessage, Runnable onShown) { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index 861e72d4ac79..cfdf17668229 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -2868,6 +2868,9 @@ public class HdmiCecLocalDevicePlaybackTest { assertThat(mPowerManager.isInteractive()).isTrue(); mNativeWrapper.clearResultMessages(); + mTestLooper.moveTimeForward(TIMEOUT_MS); + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(MONITORING_INTERVAL_MS); mTestLooper.dispatchAll(); diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 0d86d4c3fa28..f536cae53e3a 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -167,7 +167,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { * Test for {@link ShortcutService#getLastResetTimeLocked()} and * {@link ShortcutService#getNextResetTimeLocked()}. */ - public void testUpdateAndGetNextResetTimeLocked() { + public void disabled_testUpdateAndGetNextResetTimeLocked() { assertResetTimes(START_TIME, START_TIME + INTERVAL); // Advance clock. @@ -284,7 +284,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(START_TIME + 5 * INTERVAL, mManager.getRateLimitResetTime()); } - public void testSetDynamicShortcuts() { + public void disabled_testSetDynamicShortcuts() { setCaller(CALLING_PACKAGE_1, USER_10); final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.icon1); @@ -402,7 +402,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPushDynamicShortcut() { + public void disabled_testPushDynamicShortcut() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); @@ -544,7 +544,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_10)); } - public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() + public void disabled_testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() throws InterruptedException { mService.updateConfigurationLocked( ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500"); @@ -596,7 +596,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_2), any(), eq(USER_10)); } - public void testUnlimitedCalls() { + public void disabled_testUnlimitedCalls() { setCaller(CALLING_PACKAGE_1, USER_10); final ShortcutInfo si1 = makeShortcut("shortcut1"); @@ -627,7 +627,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals(3, mManager.getRemainingCallCount()); } - public void testPublishWithNoActivity() { + public void disbabledTestPublishWithNoActivity() { // If activity is not explicitly set, use the default one. mRunningUsers.put(USER_11, true); @@ -733,7 +733,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPublishWithNoActivity_noMainActivityInPackage() { + public void disabled_testPublishWithNoActivity_noMainActivityInPackage() { mRunningUsers.put(USER_11, true); runWithCaller(CALLING_PACKAGE_2, USER_11, () -> { @@ -1205,7 +1205,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { maxSize)); } - public void testShrinkBitmap() { + public void disabled_testShrinkBitmap() { checkShrinkBitmap(32, 32, R.drawable.black_512x512, 32); checkShrinkBitmap(511, 511, R.drawable.black_512x512, 511); checkShrinkBitmap(512, 512, R.drawable.black_512x512, 512); @@ -1302,7 +1302,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertFalse(p11_1_3.getName().contains("_")); } - public void testUpdateShortcuts() { + public void disabled_testUpdateShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s1"), @@ -1433,7 +1433,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testUpdateShortcuts_icons() { + public void disabled_testUpdateShortcuts_icons() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( makeShortcut("s1") @@ -1527,7 +1527,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testShortcutManagerGetShortcuts_shortcutTypes() { + public void disabled_testShortcutManagerGetShortcuts_shortcutTypes() { // Create 3 manifest and 3 dynamic shortcuts addManifestShortcutResource( @@ -2281,7 +2281,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertEquals("ABC", findById(list, "s1").getTitle()); } - public void testPinShortcutAndGetPinnedShortcuts() { + public void disabled_testPinShortcutAndGetPinnedShortcuts() { runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final ShortcutInfo s1_1 = makeShortcutWithTimestamp("s1", 1000); final ShortcutInfo s1_2 = makeShortcutWithTimestamp("s2", 2000); @@ -2557,7 +2557,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPinShortcutAndGetPinnedShortcuts_multi() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_multi() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -2889,7 +2889,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { + public void disabled_testPinShortcutAndGetPinnedShortcuts_crossProfile_plusLaunch() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { assertTrue(mManager.setDynamicShortcuts(list( @@ -3478,7 +3478,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testStartShortcut() { + public void disabled_testStartShortcut() { // Create some shortcuts. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final ShortcutInfo s1_1 = makeShortcut( @@ -3902,7 +3902,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { // === Test for persisting === - public void testSaveAndLoadUser_empty() { + public void disabled_testSaveAndLoadUser_empty() { assertTrue(mManager.setDynamicShortcuts(list())); Log.i(TAG, "Saved state"); @@ -3919,7 +3919,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { /** * Try save and load, also stop/start the user. */ - public void testSaveAndLoadUser() { + public void disabled_testSaveAndLoadUser() { // First, create some shortcuts and save. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16); @@ -4074,7 +4074,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { assertNull(ShortcutPackage.loadFromFile(mService, user, corruptedShortcutPackage, false)); } - public void testSaveCorruptAndLoadUser() throws Exception { + public void disabled_testSaveCorruptAndLoadUser() throws Exception { // First, create some shortcuts and save. runWithCaller(CALLING_PACKAGE_1, USER_10, () -> { final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16); @@ -6658,7 +6658,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } - public void testSaveAndLoad_crossProfile() { + public void disabled_testSaveAndLoad_crossProfile() { prepareCrossProfileDataSet(); dumpsysOnLogcat("Before save & load"); @@ -8471,7 +8471,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testShortcutsPushedOutByManifest() { + public void disabled_testShortcutsPushedOutByManifest() { // Change the max number of shortcuts. mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=3"); @@ -8579,7 +8579,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testReturnedByServer() { + public void disabled_testReturnedByServer() { // Package 1 updated, with manifest shortcuts. addManifestShortcutResource( new ComponentName(CALLING_PACKAGE_1, ShortcutActivity.class.getName()), @@ -8708,7 +8708,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { } } - public void testShareTargetInfo_saveToXml() throws IOException, XmlPullParserException { + public void disabled_testShareTargetInfo_saveToXml() throws IOException, XmlPullParserException { List<ShareTargetInfo> expectedValues = new ArrayList<>(); expectedValues.add(new ShareTargetInfo( new ShareTargetInfo.TargetData[]{new ShareTargetInfo.TargetData( @@ -8902,7 +8902,7 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { }); } - public void testUpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() { + public void disabled_testUpdateShortcuts_ExcludesHiddenFromLauncherShortcuts() { final ShortcutInfo s1 = makeShortcut("s1"); final ShortcutInfo s2 = makeShortcut("s2"); final ShortcutInfo s3 = makeShortcut("s3"); diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java index 551808243640..7f2935e0e497 100644 --- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java +++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java @@ -18,6 +18,7 @@ package com.android.server.policy; import static android.content.Context.SENSOR_SERVICE; +import static android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_CONFIGURATION_FLAG; import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED; import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED; @@ -42,6 +43,9 @@ import android.hardware.SensorEvent; import android.hardware.SensorManager; import android.hardware.devicestate.DeviceState; import android.os.PowerManager; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.annotation.NonNull; @@ -51,6 +55,7 @@ import com.android.server.input.InputManagerInternal; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -84,6 +89,10 @@ public final class DeviceStateProviderImplTest { private Context mContext; private SensorManager mSensorManager; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setup() { LocalServices.addService(InputManagerInternal.class, mock(InputManagerInternal.class)); @@ -605,6 +614,37 @@ public final class DeviceStateProviderImplTest { verify(listener, never()).onStateChanged(mIntegerCaptor.capture()); } + @RequiresFlagsEnabled(FLAG_DEVICE_STATE_CONFIGURATION_FLAG) + @Test + public void test_createConfigWithFlags() { + String configString = "<device-state-config>\n" + + " <device-state>\n" + + " <identifier>1</identifier>\n" + + " <flags>\n" + + " <flag>FLAG_CANCEL_OVERRIDE_REQUESTS</flag>\n" + + " </flags>\n" + + " </device-state>\n" + + " <device-state>\n" + + " <identifier>2</identifier>\n" + + " <name>CLOSED</name>\n" + + " </device-state>\n" + + "</device-state-config>\n"; + DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString); + DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext, + config); + + DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class); + provider.setListener(listener); + + verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(), + eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED)); + final DeviceState[] expectedStates = new DeviceState[]{ + createDeviceState(1, "", + Set.of(DeviceState.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS)), + createDeviceState(2, "CLOSED", EMPTY_PROPERTY_SET)}; + assertArrayEquals(expectedStates, mDeviceStateArrayCaptor.getValue()); + } + private DeviceState createDeviceState(int identifier, @NonNull String name, @NonNull Set<@DeviceState.DeviceStateProperties Integer> systemProperties) { DeviceState.Configuration configuration = new DeviceState.Configuration.Builder(identifier, diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java index 2ed71cecd79d..952d8fa47a34 100644 --- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java @@ -118,7 +118,6 @@ public class ThermalManagerServiceTest { private class ThermalHalFake extends ThermalHalWrapper { private static final int INIT_STATUS = Temperature.THROTTLING_NONE; private List<Temperature> mTemperatureList = new ArrayList<>(); - private List<Temperature> mOverrideTemperatures = null; private List<CoolingDevice> mCoolingDeviceList = new ArrayList<>(); private List<TemperatureThreshold> mTemperatureThresholdList = initializeThresholds(); @@ -132,6 +131,9 @@ public class ThermalManagerServiceTest { INIT_STATUS); private CoolingDevice mCpu = new CoolingDevice(40, CoolingDevice.TYPE_BATTERY, "cpu"); private CoolingDevice mGpu = new CoolingDevice(43, CoolingDevice.TYPE_BATTERY, "gpu"); + private Map<Integer, Float> mForecastSkinTemperatures = null; + private int mForecastSkinTemperaturesCalled = 0; + private boolean mForecastSkinTemperaturesError = false; private List<TemperatureThreshold> initializeThresholds() { ArrayList<TemperatureThreshold> thresholds = new ArrayList<>(); @@ -173,12 +175,17 @@ public class ThermalManagerServiceTest { mCoolingDeviceList.add(mGpu); } - void setOverrideTemperatures(List<Temperature> temperatures) { - mOverrideTemperatures = temperatures; + void enableForecastSkinTemperature() { + mForecastSkinTemperatures = Map.of(0, 22.0f, 10, 25.0f, 20, 28.0f, + 30, 31.0f, 40, 34.0f, 50, 37.0f, 60, 40.0f); } - void resetOverrideTemperatures() { - mOverrideTemperatures = null; + void disableForecastSkinTemperature() { + mForecastSkinTemperatures = null; + } + + void failForecastSkinTemperature() { + mForecastSkinTemperaturesError = true; } @Override @@ -219,6 +226,18 @@ public class ThermalManagerServiceTest { } @Override + protected float forecastSkinTemperature(int forecastSeconds) { + mForecastSkinTemperaturesCalled++; + if (mForecastSkinTemperaturesError) { + throw new RuntimeException(); + } + if (mForecastSkinTemperatures == null) { + throw new UnsupportedOperationException(); + } + return mForecastSkinTemperatures.get(forecastSeconds); + } + + @Override protected boolean connectToHal() { return true; } @@ -388,7 +407,7 @@ public class ThermalManagerServiceTest { Thread.sleep(CALLBACK_TIMEOUT_MILLI_SEC); resetListenerMock(); int status = Temperature.THROTTLING_SEVERE; - mFakeHal.setOverrideTemperatures(new ArrayList<>()); + mFakeHal.mTemperatureList = new ArrayList<>(); // Should not notify on non-skin type Temperature newBattery = new Temperature(37, Temperature.TYPE_BATTERY, "batt", status); @@ -518,6 +537,99 @@ public class ThermalManagerServiceTest { } @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + assertEquals(1.0f, mService.mService.getThermalHeadroom(60), 0.01f); + assertEquals(0.9f, mService.mService.getThermalHeadroom(50), 0.01f); + assertEquals(0.8f, mService.mService.getThermalHeadroom(40), 0.01f); + assertEquals(0.7f, mService.mService.getThermalHeadroom(30), 0.01f); + assertEquals(0.6f, mService.mService.getThermalHeadroom(20), 0.01f); + assertEquals(0.5f, mService.mService.getThermalHeadroom(10), 0.01f); + assertEquals(0.4f, mService.mService.getThermalHeadroom(0), 0.01f); + assertEquals(7, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholds() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + List<TemperatureThreshold> thresholds = mFakeHal.initializeThresholds(); + TemperatureThreshold skinThreshold = new TemperatureThreshold(); + skinThreshold.type = Temperature.TYPE_SKIN; + skinThreshold.name = "skin2"; + skinThreshold.hotThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + skinThreshold.coldThrottlingThresholds = new float[7 /*ThrottlingSeverity#len*/]; + for (int i = 0; i < skinThreshold.hotThrottlingThresholds.length; ++i) { + // Sets NONE to 45.0f, SEVERE to 60.0f, and SHUTDOWN to 75.0f + skinThreshold.hotThrottlingThresholds[i] = 45.0f + 5.0f * i; + } + thresholds.add(skinThreshold); + mFakeHal.mTemperatureThresholdList = thresholds; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertFalse("HAL skin forecast should be disabled on multiple SKIN thresholds", + mService.mIsHalSkinForecastSupported.get()); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST, + Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK}) + public void testGetThermalHeadroom_halForecast_disabledOnMultiThresholdsCallback() + throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + TemperatureThreshold newThreshold = new TemperatureThreshold(); + newThreshold.name = "skin2"; + newThreshold.type = Temperature.TYPE_SKIN; + newThreshold.hotThrottlingThresholds = new float[]{ + Float.NaN, 43.0f, 46.0f, 49.0f, Float.NaN, Float.NaN, Float.NaN + }; + mFakeHal.mCallback.onThresholdChanged(newThreshold); + mService.mService.getThermalHeadroom(10); + assertEquals(0, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test + @EnableFlags({Flags.FLAG_ALLOW_THERMAL_HAL_SKIN_FORECAST}) + public void testGetThermalHeadroom_halForecast_errorOnHal() throws RemoteException { + mFakeHal.mForecastSkinTemperaturesCalled = 0; + mFakeHal.enableForecastSkinTemperature(); + mService = new ThermalManagerService(mContext, mFakeHal); + mService.onBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY); + assertTrue(mService.mIsHalSkinForecastSupported.get()); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.mForecastSkinTemperaturesCalled = 0; + + mFakeHal.disableForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(1, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.enableForecastSkinTemperature(); + assertFalse(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(2, mFakeHal.mForecastSkinTemperaturesCalled); + mFakeHal.failForecastSkinTemperature(); + assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(10))); + assertEquals(3, mFakeHal.mForecastSkinTemperaturesCalled); + } + + @Test @EnableFlags({Flags.FLAG_ALLOW_THERMAL_THRESHOLDS_CALLBACK, Flags.FLAG_ALLOW_THERMAL_HEADROOM_THRESHOLDS}) public void testTemperatureWatcherUpdateSevereThresholds() throws Exception { diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java new file mode 100644 index 000000000000..47e3dc85f6d0 --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/ConfigInternalForTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.UserIdInt; + +public final class ConfigInternalForTests { + + static final @UserIdInt int USER_ID = 9876; + + static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(false) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(false) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(true) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(false) + .setGeoDetectionFeatureSupported(false) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .build(); + + static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED = + new ConfigurationInternal.Builder() + .setUserId(USER_ID) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(true) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(true) + .build(); +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/FakeEnvironment.java b/services/tests/timetests/src/com/android/server/timezonedetector/FakeEnvironment.java new file mode 100644 index 000000000000..5d181b81eb4c --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/FakeEnvironment.java @@ -0,0 +1,141 @@ +/* + * Copyright 2025 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.timezonedetector; + +import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; + +import com.android.server.SystemTimeZone; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * A partially implemented, fake implementation of Environment for tests. + */ +public class FakeEnvironment implements Environment { + + private final TestState<String> mTimeZoneId = new TestState<>(); + private final TestState<Integer> mTimeZoneConfidence = new TestState<>(); + private final List<Runnable> mAsyncRunnables = new ArrayList<>(); + private @ElapsedRealtimeLong long mElapsedRealtimeMillis; + private @CurrentTimeMillisLong long mInitializationTimeMillis; + + FakeEnvironment() { + // Ensure the fake environment starts with the defaults a fresh device would. + initializeTimeZoneSetting("", TIME_ZONE_CONFIDENCE_LOW); + } + + void initializeClock(@CurrentTimeMillisLong long currentTimeMillis, + @ElapsedRealtimeLong long elapsedRealtimeMillis) { + mInitializationTimeMillis = currentTimeMillis - elapsedRealtimeMillis; + mElapsedRealtimeMillis = elapsedRealtimeMillis; + } + + void initializeTimeZoneSetting(String zoneId, + @SystemTimeZone.TimeZoneConfidence int timeZoneConfidence) { + mTimeZoneId.init(zoneId); + mTimeZoneConfidence.init(timeZoneConfidence); + } + + void incrementClock() { + mElapsedRealtimeMillis++; + } + + @Override + public String getDeviceTimeZone() { + return mTimeZoneId.getLatest(); + } + + @Override + public int getDeviceTimeZoneConfidence() { + return mTimeZoneConfidence.getLatest(); + } + + @Override + public void setDeviceTimeZoneAndConfidence( + String zoneId, @SystemTimeZone.TimeZoneConfidence int confidence, String logInfo) { + mTimeZoneId.set(zoneId); + mTimeZoneConfidence.set(confidence); + } + + void assertTimeZoneNotChanged() { + mTimeZoneId.assertHasNotBeenSet(); + mTimeZoneConfidence.assertHasNotBeenSet(); + } + + void assertTimeZoneChangedTo(String timeZoneId, + @SystemTimeZone.TimeZoneConfidence int confidence) { + mTimeZoneId.assertHasBeenSet(); + mTimeZoneId.assertChangeCount(1); + mTimeZoneId.assertLatestEquals(timeZoneId); + + mTimeZoneConfidence.assertHasBeenSet(); + mTimeZoneConfidence.assertChangeCount(1); + mTimeZoneConfidence.assertLatestEquals(confidence); + } + + void commitAllChanges() { + mTimeZoneId.commitLatest(); + mTimeZoneConfidence.commitLatest(); + } + + @Override + @ElapsedRealtimeLong + public long elapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + @Override + @CurrentTimeMillisLong + public long currentTimeMillis() { + return mInitializationTimeMillis + mElapsedRealtimeMillis; + } + + @Override + public void addDebugLogEntry(String logMsg) { + // No-op for tests + } + + @Override + public void dumpDebugLog(PrintWriter printWriter) { + // No-op for tests + } + + /** + * Adds the supplied runnable to a list but does not run them. To run all the runnables that + * have been supplied, call {@code #runAsyncRunnables}. + */ + @Override + public void runAsync(Runnable runnable) { + mAsyncRunnables.add(runnable); + } + + /** + * Requests that the runnable that have been supplied to {@code #runAsync} are invoked + * asynchronously and cleared. + */ + public void runAsyncRunnables() { + for (Runnable runnable : mAsyncRunnables) { + runnable.run(); + } + mAsyncRunnables.clear(); + } +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java index fc6afe486187..aeb4d9a19ff0 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/FakeServiceConfigAccessor.java @@ -31,7 +31,7 @@ import java.util.Optional; /** * A partially implemented, fake implementation of ServiceConfigAccessor for tests. * - * <p>This class has rudamentary support for multiple users, but unlike the real thing, it doesn't + * <p>This class has rudimentary support for multiple users, but unlike the real thing, it doesn't * simulate that some settings are global and shared between users. It also delivers config updates * synchronously. */ diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java new file mode 100644 index 000000000000..45cc891bff55 --- /dev/null +++ b/services/tests/timetests/src/com/android/server/timezonedetector/NotifyingTimeZoneChangeListenerTest.java @@ -0,0 +1,535 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.AUTO_REVERT_THRESHOLD; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_NONE; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.SIGNAL_TYPE_UNKNOWN; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_REJECTED; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_SUPERSEDED; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNKNOWN; +import static com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.STATUS_UNTRACKED; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.spy; + +import android.annotation.Nullable; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.UiAutomation; +import android.content.Context; +import android.os.HandlerThread; +import android.os.Process; +import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.flags.Flags; +import com.android.server.timezonedetector.NotifyingTimeZoneChangeListener.TimeZoneChangeRecord; +import com.android.server.timezonedetector.TimeZoneChangeListener.TimeZoneChangeEvent; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.time.InstantSource; +import java.util.ArrayList; +import java.util.List; + +/** + * White-box unit tests for {@link NotifyingTimeZoneChangeListener}. + */ +@RunWith(JUnitParamsRunner.class) +@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS) +public class NotifyingTimeZoneChangeListenerTest { + + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + + @Rule(order = 0) + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + + @Rule(order = 1) + public final MockitoRule mockito = MockitoJUnit.rule(); + + public static List<@TimeZoneDetectorStrategy.Origin Integer> getDetectionOrigins() { + return List.of(ORIGIN_LOCATION, ORIGIN_TELEPHONY); + } + + private static final String INTERACT_ACROSS_USERS_FULL_PERMISSION = + "android.permission.INTERACT_ACROSS_USERS_FULL"; + + @Mock + private Context mContext; + private UiAutomation mUiAutomation; + + private FakeNotificationManager mNotificationManager; + private HandlerThread mHandlerThread; + private TestHandler mHandler; + private FakeServiceConfigAccessor mServiceConfigAccessor; + private FakeEnvironment mFakeEnvironment; + private int mUid; + + private NotifyingTimeZoneChangeListener mTimeZoneChangeTracker; + + @Before + public void setUp() { + mUid = Process.myUid(); + mFakeEnvironment = new FakeEnvironment(); + mFakeEnvironment.initializeClock(1735689600L, 1234L); + + // Create a thread + handler for processing the work that the service posts. + mHandlerThread = new HandlerThread("TimeZoneDetectorInternalTest"); + mHandlerThread.start(); + mHandler = new TestHandler(mHandlerThread.getLooper()); + + ConfigurationInternal config = new ConfigurationInternal.Builder() + .setUserId(mUid) + .setTelephonyDetectionFeatureSupported(true) + .setGeoDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(false) + .setGeoDetectionRunInBackgroundEnabled(false) + .setEnhancedMetricsCollectionEnabled(false) + .setUserConfigAllowed(true) + .setAutoDetectionEnabledSetting(false) + .setLocationEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(false) + .setManualChangeTrackingSupported(false) + .build(); + + mServiceConfigAccessor = spy(new FakeServiceConfigAccessor()); + mServiceConfigAccessor.initializeCurrentUserConfiguration(config); + + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL_PERMISSION); + + mNotificationManager = new FakeNotificationManager(mContext, InstantSource.system()); + + mTimeZoneChangeTracker = new NotifyingTimeZoneChangeListener(mHandler, mContext, + mServiceConfigAccessor, mNotificationManager, mFakeEnvironment); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + mHandlerThread.join(); + } + + @Test + public void process_autoDetectionOff_noManualTracking_shouldTrackWithoutNotifying() { + enableTimeZoneNotifications(); + + TimeZoneChangeRecord expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + + assertEquals(expectedTimeZoneChangeRecord, + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(0); + } + + @Test + public void process_autoDetectionOff_shouldTrackWithoutNotifying() { + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + + assertEquals(expectedTimeZoneChangeRecord, + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord1 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord1); + assertEquals(1, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + + expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 2, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1000L, + /* unixEpochTimeMillis= */ 1726597800000L + 1000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/London", + /* newZoneId= */ "Europe/Paris", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + // lastTrackedChangeEvent != null + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord2 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(STATUS_SUPERSEDED, timeZoneChangeRecord1.getStatus()); + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord2); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(2); + + disableTimeZoneAutoDetection(); + + // Test manual change within revert threshold + { + expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 3, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 999L + AUTO_REVERT_THRESHOLD, + /* unixEpochTimeMillis= */ + 1726597800000L + 999L + AUTO_REVERT_THRESHOLD, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord3 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + // The user manually changed the time zone within a short period of receiving the + // notification, indicating that they rejected the automatic change of time zone + assertEquals(STATUS_REJECTED, timeZoneChangeRecord2.getStatus()); + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord3); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(3); + } + + // Test manual change outside of revert threshold + { + // [START] Reset previous event + enableNotificationsWithManualChangeTracking(); + mTimeZoneChangeTracker.process(timeZoneChangeRecord2.getEvent()); + timeZoneChangeRecord2 = mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + disableTimeZoneAutoDetection(); + // [END] Reset previous event + + expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 5, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1001L + AUTO_REVERT_THRESHOLD, + /* unixEpochTimeMillis= */ + 1726597800000L + 1001L + AUTO_REVERT_THRESHOLD, + /* origin= */ ORIGIN_MANUAL, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNTRACKED, SIGNAL_TYPE_NONE); + + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord3 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + // The user manually changed the time zone outside of the period we consider as a revert + assertEquals(STATUS_SUPERSEDED, timeZoneChangeRecord2.getStatus()); + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord3); + assertEquals(3, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(5); + } + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported_missingTransition( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/London", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord1 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord1); + assertEquals(1, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + + expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 3, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 1000L, + /* unixEpochTimeMillis= */ 1726597800000L + 1000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Athens", + /* newZoneId= */ "Europe/Paris", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + // lastTrackedChangeEvent != null + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord2 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(STATUS_SUPERSEDED, timeZoneChangeRecord1.getStatus()); + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord2); + assertEquals(2, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(2); + } + + @Test + @Parameters(method = "getDetectionOrigins") + public void process_automaticDetection_trackingSupported_sameOffset( + @TimeZoneDetectorStrategy.Origin int origin) { + if (origin == ORIGIN_LOCATION) { + enableLocationTimeZoneDetection(); + } else if (origin == ORIGIN_TELEPHONY) { + enableTelephonyTimeZoneDetection(); + } else { + throw new IllegalStateException( + "The given origin has not been implemented for this test: " + origin); + } + + enableNotificationsWithManualChangeTracking(); + + TimeZoneChangeRecord expectedTimeZoneChangeRecord = new TimeZoneChangeRecord( + /* id= */ 1, + new TimeZoneChangeEvent( + /* elapsedRealtimeMillis= */ 0, + /* unixEpochTimeMillis= */ 1726597800000L, + /* origin= */ origin, + /* userId= */ mUid, + /* oldZoneId= */ "Europe/Paris", + /* newZoneId= */ "Europe/Rome", + /* newConfidence= */ 1, + /* cause= */ "NO_REASON")); + expectedTimeZoneChangeRecord.setStatus(STATUS_UNKNOWN, SIGNAL_TYPE_UNKNOWN); + + assertNull(mTimeZoneChangeTracker.getLastTimeZoneChangeRecord()); + + // lastTrackedChangeEvent == null + mTimeZoneChangeTracker.process(expectedTimeZoneChangeRecord.getEvent()); + TimeZoneChangeRecord timeZoneChangeRecord1 = + mTimeZoneChangeTracker.getLastTimeZoneChangeRecord(); + + assertEquals(expectedTimeZoneChangeRecord, timeZoneChangeRecord1); + // No notification sent for the same UTC offset + assertEquals(0, mNotificationManager.getNotifications().size()); + mHandler.assertTotalMessagesEnqueued(1); + } + + private void enableLocationTimeZoneDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(true) + .setGeoDetectionFeatureSupported(true) + .setGeoDetectionEnabledSetting(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableTelephonyTimeZoneDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(true) + .setGeoDetectionEnabledSetting(false) + .setTelephonyDetectionFeatureSupported(true) + .setTelephonyFallbackSupported(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableTimeZoneNotifications() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(true) + .setManualChangeTrackingSupported(false) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void enableNotificationsWithManualChangeTracking() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setNotificationsSupported(true) + .setNotificationsTrackingSupported(true) + .setNotificationsEnabledSetting(true) + .setManualChangeTrackingSupported(true) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private void disableTimeZoneAutoDetection() { + ConfigurationInternal oldConfiguration = + mServiceConfigAccessor.getCurrentUserConfigurationInternal(); + ConfigurationInternal newConfiguration = toBuilder(oldConfiguration) + .setAutoDetectionEnabledSetting(false) + .setGeoDetectionEnabledSetting(false) + .build(); + + mServiceConfigAccessor.simulateCurrentUserConfigurationInternalChange(newConfiguration); + } + + private ConfigurationInternal.Builder toBuilder(ConfigurationInternal config) { + return new ConfigurationInternal.Builder() + .setUserId(config.getUserId()) + .setTelephonyDetectionFeatureSupported(config.isTelephonyDetectionSupported()) + .setGeoDetectionFeatureSupported(config.isGeoDetectionSupported()) + .setTelephonyFallbackSupported(config.isTelephonyFallbackSupported()) + .setGeoDetectionRunInBackgroundEnabled( + config.getGeoDetectionRunInBackgroundEnabledSetting()) + .setEnhancedMetricsCollectionEnabled(config.isEnhancedMetricsCollectionEnabled()) + .setUserConfigAllowed(config.isUserConfigAllowed()) + .setAutoDetectionEnabledSetting(config.getAutoDetectionEnabledSetting()) + .setLocationEnabledSetting(config.getLocationEnabledSetting()) + .setGeoDetectionEnabledSetting(config.getGeoDetectionEnabledSetting()) + .setNotificationsTrackingSupported(config.isNotificationTrackingSupported()) + .setNotificationsEnabledSetting(config.getNotificationsEnabledBehavior()) + .setNotificationsSupported(config.areNotificationsSupported()) + .setManualChangeTrackingSupported(config.isManualChangeTrackingSupported()); + } + + private static class FakeNotificationManager extends NotificationManager { + + private final List<Notification> mNotifications = new ArrayList<>(); + + FakeNotificationManager(Context context, InstantSource clock) { + super(context, clock); + } + + @Override + public void notifyAsUser(@Nullable String tag, int id, Notification notification, + UserHandle user) { + mNotifications.add(notification); + } + + public List<Notification> getNotifications() { + return mNotifications; + } + } +} diff --git a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java index e52e8b60a61d..44232e7ddfa2 100644 --- a/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java +++ b/services/tests/timetests/src/com/android/server/timezonedetector/TimeZoneDetectorStrategyImplTest.java @@ -35,6 +35,15 @@ import static android.service.timezone.TimeZoneProviderStatus.OPERATION_STATUS_U import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH; import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_LOW; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DETECT_NOT_SUPPORTED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_DISABLED_GEO_DISABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_DISABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_AUTO_ENABLED_GEO_ENABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.CONFIG_USER_RESTRICTED_AUTO_ENABLED; +import static com.android.server.timezonedetector.ConfigInternalForTests.USER_ID; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_LOCATION; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_MANUAL; +import static com.android.server.timezonedetector.TimeZoneDetectorStrategy.ORIGIN_TELEPHONY; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGH; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_HIGHEST; import static com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.TELEPHONY_SCORE_LOW; @@ -54,7 +63,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.time.LocationTimeZoneAlgorithmStatus; @@ -67,21 +75,27 @@ import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.MatchType; import android.app.timezonedetector.TelephonyTimeZoneSuggestion.Quality; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.timezone.TimeZoneProviderStatus; +import android.util.IndentingPrintWriter; import com.android.server.SystemTimeZone.TimeZoneConfidence; +import com.android.server.flags.Flags; import com.android.server.timezonedetector.TimeZoneDetectorStrategyImpl.QualifiedTelephonyTimeZoneSuggestion; import junitparams.JUnitParamsRunner; import junitparams.Parameters; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; @@ -90,9 +104,15 @@ import java.util.function.Function; * White-box unit tests for {@link TimeZoneDetectorStrategyImpl}. */ @RunWith(JUnitParamsRunner.class) +@EnableFlags(Flags.FLAG_DATETIME_NOTIFICATIONS) public class TimeZoneDetectorStrategyImplTest { - private static final @UserIdInt int USER_ID = 9876; + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + + private static final long ARBITRARY_CURRENT_TIME_MILLIS = 1735689600; // 2025-01-01 00:00:00 private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 1234; /** A time zone used for initialization that does not occur elsewhere in tests. */ private static final String ARBITRARY_TIME_ZONE_ID = "Etc/UTC"; @@ -101,7 +121,7 @@ public class TimeZoneDetectorStrategyImplTest { // Telephony test cases are ordered so that each successive one is of the same or higher score // than the previous. - private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[] { + private static final TelephonyTestCase[] TELEPHONY_TEST_CASES = new TelephonyTestCase[]{ newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, QUALITY_MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, TELEPHONY_SCORE_LOW), newTelephonyTestCase(MATCH_TYPE_NETWORK_COUNTRY_ONLY, @@ -118,95 +138,12 @@ public class TimeZoneDetectorStrategyImplTest { TELEPHONY_SCORE_HIGHEST), }; - private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(false) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_USER_RESTRICTED_AUTO_ENABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(false) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(true) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_DETECT_NOT_SUPPORTED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(false) - .setGeoDetectionFeatureSupported(false) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_DISABLED_GEO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(false) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_DISABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(false) - .build(); - - private static final ConfigurationInternal CONFIG_AUTO_ENABLED_GEO_ENABLED = - new ConfigurationInternal.Builder() - .setUserId(USER_ID) - .setTelephonyDetectionFeatureSupported(true) - .setGeoDetectionFeatureSupported(true) - .setTelephonyFallbackSupported(false) - .setGeoDetectionRunInBackgroundEnabled(false) - .setEnhancedMetricsCollectionEnabled(false) - .setUserConfigAllowed(true) - .setAutoDetectionEnabledSetting(true) - .setLocationEnabledSetting(true) - .setGeoDetectionEnabledSetting(true) - .build(); - private static final TelephonyTimeZoneAlgorithmStatus TELEPHONY_ALGORITHM_RUNNING_STATUS = new TelephonyTimeZoneAlgorithmStatus(DETECTION_ALGORITHM_STATUS_RUNNING); private FakeServiceConfigAccessor mFakeServiceConfigAccessorSpy; private FakeEnvironment mFakeEnvironment; + private FakeTimeZoneChangeEventListener mFakeTimeZoneChangeEventTracker; private TimeZoneDetectorStrategyImpl mTimeZoneDetectorStrategy; @@ -216,9 +153,10 @@ public class TimeZoneDetectorStrategyImplTest { mFakeServiceConfigAccessorSpy = spy(new FakeServiceConfigAccessor()); mFakeServiceConfigAccessorSpy.initializeCurrentUserConfiguration( CONFIG_AUTO_DISABLED_GEO_DISABLED); + mFakeTimeZoneChangeEventTracker = new FakeTimeZoneChangeEventListener(); mTimeZoneDetectorStrategy = new TimeZoneDetectorStrategyImpl( - mFakeServiceConfigAccessorSpy, mFakeEnvironment); + mFakeServiceConfigAccessorSpy, mFakeEnvironment, mFakeTimeZoneChangeEventTracker); } @Test @@ -421,7 +359,7 @@ public class TimeZoneDetectorStrategyImplTest { new QualifiedTelephonyTimeZoneSuggestion(slotIndex1TimeZoneSuggestion, TELEPHONY_SCORE_NONE); script.verifyLatestQualifiedTelephonySuggestionReceived( - SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) + SLOT_INDEX1, expectedSlotIndex1ScoredSuggestion) .verifyLatestQualifiedTelephonySuggestionReceived(SLOT_INDEX2, null); assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); @@ -440,6 +378,10 @@ public class TimeZoneDetectorStrategyImplTest { // SlotIndex1 should always beat slotIndex2, all other things being equal. assertEquals(expectedSlotIndex1ScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } /** @@ -475,6 +417,10 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // A good quality suggestion will be used. @@ -492,6 +438,13 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + assertEquals(ORIGIN_TELEPHONY, + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst() + .getOrigin()); + } } // A low quality suggestion will be accepted, but not used to set the device time zone. @@ -509,6 +462,11 @@ public class TimeZoneDetectorStrategyImplTest { SLOT_INDEX1, expectedScoredSuggestion); assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); + + if (Flags.datetimeNotifications()) { + // Still 1 from last good quality suggestion but not recorded as quality is too low + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } } @@ -569,6 +527,17 @@ public class TimeZoneDetectorStrategyImplTest { assertEquals(expectedScoredSuggestion, mTimeZoneDetectorStrategy.findBestTelephonySuggestionForTests()); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * configuration is reset at every loop. + */ + assertEquals(6, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + assertEquals(ORIGIN_TELEPHONY, + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().getFirst() + .getOrigin()); + } } @Test @@ -595,6 +564,18 @@ public class TimeZoneDetectorStrategyImplTest { for (TelephonyTestCase testCase : descendingCasesByScore) { makeSlotIndex1SuggestionAndCheckState(script, testCase); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * set of tests is run twice. + */ + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(12, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin()); + } } private void makeSlotIndex1SuggestionAndCheckState(Script script, TelephonyTestCase testCase) { @@ -629,7 +610,7 @@ public class TimeZoneDetectorStrategyImplTest { */ @Test public void testTelephonySuggestionMultipleSlotIndexSuggestionScoringAndSlotIndexBias() { - String[] zoneIds = { "Europe/London", "Europe/Paris" }; + String[] zoneIds = {"Europe/London", "Europe/Paris"}; TelephonyTimeZoneSuggestion emptySlotIndex1Suggestion = createEmptySlotIndex1Suggestion(); TelephonyTimeZoneSuggestion emptySlotIndex2Suggestion = createEmptySlotIndex2Suggestion(); QualifiedTelephonyTimeZoneSuggestion expectedEmptySlotIndex1ScoredSuggestion = @@ -672,7 +653,7 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyLatestQualifiedTelephonySuggestionReceived( - SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) + SLOT_INDEX1, expectedZoneSlotIndex1ScoredSuggestion) .verifyLatestQualifiedTelephonySuggestionReceived( SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); assertEquals(expectedZoneSlotIndex1ScoredSuggestion, @@ -718,6 +699,18 @@ public class TimeZoneDetectorStrategyImplTest { .verifyLatestQualifiedTelephonySuggestionReceived( SLOT_INDEX2, expectedEmptySlotIndex2ScoredSuggestion); } + + if (Flags.datetimeNotifications()) { + /* + * Only 6 out of 7 tests have a quality good enough to trigger an event and the + * simulation runs twice per loop with a different time zone (i.e. London and Paris). + */ + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(12, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.getFirst().getOrigin()); + } } /** @@ -760,6 +753,14 @@ public class TimeZoneDetectorStrategyImplTest { // Latest suggestion should be used. script.simulateSetAutoMode(true) .verifyTimeZoneChangedAndReset(newYorkSuggestion); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(2, timeZoneChangeEvents.size()); + assertTrue(timeZoneChangeEvents.stream() + .allMatch(x -> x.getOrigin() == ORIGIN_TELEPHONY)); + } } @Test @@ -791,6 +792,10 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -805,14 +810,23 @@ public class TimeZoneDetectorStrategyImplTest { boolean bypassUserPolicyChecks = false; boolean expectedResult = true; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) .verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin() + ); + } } @Test - @Parameters({ "true,true", "true,false", "false,true", "false,false" }) + @Parameters({"true,true", "true,false", "false,true", "false,false"}) public void testManualSuggestion_autoTimeEnabled_userRestrictions( boolean userConfigAllowed, boolean bypassUserPolicyChecks) { ConfigurationInternal config = @@ -831,10 +845,14 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test - @Parameters({ "true,true", "true,false", "false,true", "false,false" }) + @Parameters({"true,true", "true,false", "false,true", "false,false"}) public void testManualSuggestion_autoTimeDisabled_userRestrictions( boolean userConfigAllowed, boolean bypassUserPolicyChecks) { ConfigurationInternal config = @@ -849,7 +867,7 @@ public class TimeZoneDetectorStrategyImplTest { ManualTimeZoneSuggestion manualSuggestion = createManualSuggestion("Europe/Paris"); boolean expectedResult = userConfigAllowed || bypassUserPolicyChecks; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult); + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult); if (expectedResult) { script.verifyTimeZoneChangedAndReset(manualSuggestion); assertEquals(manualSuggestion, mTimeZoneDetectorStrategy.getLatestManualSuggestion()); @@ -857,6 +875,16 @@ public class TimeZoneDetectorStrategyImplTest { script.verifyTimeZoneNotChanged(); assertNull(mTimeZoneDetectorStrategy.getLatestManualSuggestion()); } + + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + if (Flags.datetimeNotifications() && expectedResult) { + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()); + } else { + assertEmpty(timeZoneChangeEvents); + } } @Test @@ -907,6 +935,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } { @@ -934,6 +966,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } } @@ -970,6 +1006,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -1004,6 +1044,10 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } @Test @@ -1039,6 +1083,14 @@ public class TimeZoneDetectorStrategyImplTest { // Assert internal service state. script.verifyCachedDetectorStatus(expectedDetectorStatus) .verifyLatestLocationAlgorithmEventReceived(locationAlgorithmEvent); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.getFirst().getOrigin()); + } } /** @@ -1076,6 +1128,17 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(londonOrParisEvent) .verifyTimeZoneNotChanged() .verifyLatestLocationAlgorithmEventReceived(londonOrParisEvent); + + if (Flags.datetimeNotifications()) { + // we do not record events if the time zone does not change (i.e. 2 / 4 of the + // simulated cases) + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(2, timeZoneChangeEvents.size()); + assertTrue(timeZoneChangeEvents.stream() + .allMatch(x -> x.getOrigin() == ORIGIN_LOCATION)); + } } /** @@ -1136,6 +1199,16 @@ public class TimeZoneDetectorStrategyImplTest { // A configuration change is considered a "state change". assertStateChangeNotificationsSent(stateChangeListener, 1); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(2).getOrigin()); + } } @Test @@ -1146,7 +1219,7 @@ public class TimeZoneDetectorStrategyImplTest { .build(); Script script = new Script() - .initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .initializeClock(ARBITRARY_CURRENT_TIME_MILLIS, ARBITRARY_ELAPSED_REALTIME_MILLIS) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(config) .resetConfigurationTracking(); @@ -1165,6 +1238,14 @@ public class TimeZoneDetectorStrategyImplTest { .simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneChangedAndReset(telephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + } } // Receiving an "uncertain" geolocation suggestion should have no effect. @@ -1175,6 +1256,11 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. @@ -1186,6 +1272,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(2, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + } } // Used to record the last telephony suggestion received, which will be used when fallback @@ -1202,6 +1296,11 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); lastTelephonySuggestion = telephonySuggestion; + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Geolocation suggestions should continue to be used as normal (previous telephony @@ -1228,6 +1327,14 @@ public class TimeZoneDetectorStrategyImplTest { // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin()); + } } // Enable telephony fallback. Nothing will change, because the geolocation is still certain, @@ -1237,6 +1344,11 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(3, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Make the geolocation algorithm uncertain. @@ -1247,6 +1359,14 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(4, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin()); + } } // Make the geolocation algorithm certain, disabling telephony fallback. @@ -1259,6 +1379,13 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(5, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin()); + } } // Demonstrate what happens when geolocation is uncertain when telephony fallback is @@ -1273,6 +1400,14 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + assertEquals(6, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(5).getOrigin()); + } } } @@ -1284,7 +1419,7 @@ public class TimeZoneDetectorStrategyImplTest { .build(); Script script = new Script() - .initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .initializeClock(ARBITRARY_CURRENT_TIME_MILLIS, ARBITRARY_ELAPSED_REALTIME_MILLIS) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(config) .resetConfigurationTracking(); @@ -1303,6 +1438,13 @@ public class TimeZoneDetectorStrategyImplTest { .simulateTelephonyTimeZoneSuggestion(telephonySuggestion) .verifyTimeZoneChangedAndReset(telephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(0).getOrigin()); + } } // Receiving an "uncertain" geolocation suggestion without a status should have no effect. @@ -1313,6 +1455,11 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(1, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Receiving a "certain" geolocation suggestion should disable telephony fallback mode. @@ -1324,6 +1471,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(2, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(1).getOrigin()); + } } // Used to record the last telephony suggestion received, which will be used when fallback @@ -1340,6 +1494,11 @@ public class TimeZoneDetectorStrategyImplTest { .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); lastTelephonySuggestion = telephonySuggestion; + + if (Flags.datetimeNotifications()) { + // unchanged + assertEquals(2, mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents().size()); + } } // Geolocation suggestions should continue to be used as normal (previous telephony @@ -1369,6 +1528,13 @@ public class TimeZoneDetectorStrategyImplTest { // No change needed, device will already be set to Europe/Rome. .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(3, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(2).getOrigin()); + } } // Enable telephony fallback via a LocationAlgorithmEvent containing an "uncertain" @@ -1388,6 +1554,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(uncertainEventBlockedBySettings) .verifyTimeZoneChangedAndReset(lastTelephonySuggestion) .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(4, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_TELEPHONY, timeZoneChangeEvents.get(3).getOrigin()); + } } // Make the geolocation algorithm certain, disabling telephony fallback. @@ -1399,6 +1572,13 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneChangedAndReset(locationAlgorithmEvent) .verifyTelephonyFallbackIsEnabled(false); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(5, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(4).getOrigin()); + } } } @@ -1410,7 +1590,7 @@ public class TimeZoneDetectorStrategyImplTest { .build(); Script script = new Script() - .initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .initializeClock(ARBITRARY_CURRENT_TIME_MILLIS, ARBITRARY_ELAPSED_REALTIME_MILLIS) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(config) .resetConfigurationTracking(); @@ -1427,6 +1607,10 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // Make an uncertain geolocation suggestion, there is no telephony suggestion to fall back @@ -1438,6 +1622,10 @@ public class TimeZoneDetectorStrategyImplTest { script.simulateLocationAlgorithmEvent(locationAlgorithmEvent) .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + assertEmpty(mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents()); + } } // Similar to the case above, but force a fallback attempt after making a "certain" @@ -1464,13 +1652,20 @@ public class TimeZoneDetectorStrategyImplTest { .simulateEnableTelephonyFallback() .verifyTimeZoneNotChanged() .verifyTelephonyFallbackIsEnabled(true); + + if (Flags.datetimeNotifications()) { + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_LOCATION, timeZoneChangeEvents.get(0).getOrigin()); + } } } @Test public void testGetTimeZoneState() { Script script = new Script() - .initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .initializeClock(ARBITRARY_CURRENT_TIME_MILLIS, ARBITRARY_ELAPSED_REALTIME_MILLIS) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED) .resetConfigurationTracking(); @@ -1492,7 +1687,7 @@ public class TimeZoneDetectorStrategyImplTest { @Test public void testSetTimeZoneState() { Script script = new Script() - .initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .initializeClock(ARBITRARY_CURRENT_TIME_MILLIS, ARBITRARY_ELAPSED_REALTIME_MILLIS) .initializeTimeZoneSetting(ARBITRARY_TIME_ZONE_ID, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED_GEO_DISABLED) .resetConfigurationTracking(); @@ -1519,7 +1714,7 @@ public class TimeZoneDetectorStrategyImplTest { private void testConfirmTimeZone(ConfigurationInternal config) { String timeZoneId = "Europe/London"; Script script = new Script() - .initializeClock(ARBITRARY_ELAPSED_REALTIME_MILLIS) + .initializeClock(ARBITRARY_CURRENT_TIME_MILLIS, ARBITRARY_ELAPSED_REALTIME_MILLIS) .initializeTimeZoneSetting(timeZoneId, TIME_ZONE_CONFIDENCE_LOW) .simulateConfigurationInternalChange(config) .resetConfigurationTracking(); @@ -1569,7 +1764,7 @@ public class TimeZoneDetectorStrategyImplTest { boolean bypassUserPolicyChecks = false; boolean expectedResult = true; script.simulateManualTimeZoneSuggestion( - USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) + USER_ID, manualSuggestion, bypassUserPolicyChecks, expectedResult) .verifyTimeZoneChangedAndReset(manualSuggestion); expectedDeviceTimeZoneId = manualSuggestion.getZoneId(); assertMetricsState(expectedInternalConfig, expectedDeviceTimeZoneId, @@ -1706,97 +1901,6 @@ public class TimeZoneDetectorStrategyImplTest { mFakeEnvironment.elapsedRealtimeMillis(), Arrays.asList(zoneIds)); } - static class FakeEnvironment implements TimeZoneDetectorStrategyImpl.Environment { - - private final TestState<String> mTimeZoneId = new TestState<>(); - private final TestState<Integer> mTimeZoneConfidence = new TestState<>(); - private final List<Runnable> mAsyncRunnables = new ArrayList<>(); - private @ElapsedRealtimeLong long mElapsedRealtimeMillis; - - FakeEnvironment() { - // Ensure the fake environment starts with the defaults a fresh device would. - initializeTimeZoneSetting("", TIME_ZONE_CONFIDENCE_LOW); - } - - void initializeClock(@ElapsedRealtimeLong long elapsedRealtimeMillis) { - mElapsedRealtimeMillis = elapsedRealtimeMillis; - } - - void initializeTimeZoneSetting(String zoneId, @TimeZoneConfidence int timeZoneConfidence) { - mTimeZoneId.init(zoneId); - mTimeZoneConfidence.init(timeZoneConfidence); - } - - void incrementClock() { - mElapsedRealtimeMillis++; - } - - @Override - public String getDeviceTimeZone() { - return mTimeZoneId.getLatest(); - } - - @Override - public int getDeviceTimeZoneConfidence() { - return mTimeZoneConfidence.getLatest(); - } - - @Override - public void setDeviceTimeZoneAndConfidence( - String zoneId, @TimeZoneConfidence int confidence, String logInfo) { - mTimeZoneId.set(zoneId); - mTimeZoneConfidence.set(confidence); - } - - void assertTimeZoneNotChanged() { - mTimeZoneId.assertHasNotBeenSet(); - mTimeZoneConfidence.assertHasNotBeenSet(); - } - - void assertTimeZoneChangedTo(String timeZoneId, @TimeZoneConfidence int confidence) { - mTimeZoneId.assertHasBeenSet(); - mTimeZoneId.assertChangeCount(1); - mTimeZoneId.assertLatestEquals(timeZoneId); - - mTimeZoneConfidence.assertHasBeenSet(); - mTimeZoneConfidence.assertChangeCount(1); - mTimeZoneConfidence.assertLatestEquals(confidence); - } - - void commitAllChanges() { - mTimeZoneId.commitLatest(); - mTimeZoneConfidence.commitLatest(); - } - - @Override - @ElapsedRealtimeLong - public long elapsedRealtimeMillis() { - return mElapsedRealtimeMillis; - } - - @Override - public void addDebugLogEntry(String logMsg) { - // No-op for tests - } - - @Override - public void dumpDebugLog(PrintWriter printWriter) { - // No-op for tests - } - - @Override - public void runAsync(Runnable runnable) { - mAsyncRunnables.add(runnable); - } - - public void runAsyncRunnables() { - for (Runnable runnable : mAsyncRunnables) { - runnable.run(); - } - mAsyncRunnables.clear(); - } - } - private void assertStateChangeNotificationsSent( TestStateChangeListener stateChangeListener, int expectedCount) { // The fake environment needs to be told to run posted work. @@ -1817,8 +1921,8 @@ public class TimeZoneDetectorStrategyImplTest { return this; } - Script initializeClock(long elapsedRealtimeMillis) { - mFakeEnvironment.initializeClock(elapsedRealtimeMillis); + Script initializeClock(long currentTimeMillis, long elapsedRealtimeMillis) { + mFakeEnvironment.initializeClock(currentTimeMillis, elapsedRealtimeMillis); return this; } @@ -1880,6 +1984,17 @@ public class TimeZoneDetectorStrategyImplTest { boolean actualResult = mTimeZoneDetectorStrategy.suggestManualTimeZone( userId, manualTimeZoneSuggestion, bypassUserPolicyChecks); assertEquals(expectedResult, actualResult); + + List<TimeZoneChangeListener.TimeZoneChangeEvent> timeZoneChangeEvents = + mFakeTimeZoneChangeEventTracker.getTimeZoneChangeEvents(); + + if (actualResult && Flags.datetimeNotifications()) { + assertEquals(1, timeZoneChangeEvents.size()); + assertEquals(ORIGIN_MANUAL, timeZoneChangeEvents.getFirst().getOrigin()); + } else { + assertEmpty(timeZoneChangeEvents); + } + return this; } @@ -2001,4 +2116,34 @@ public class TimeZoneDetectorStrategyImplTest { return new TelephonyTestCase(matchType, quality, expectedScore); } + static class FakeTimeZoneChangeEventListener implements TimeZoneChangeListener { + private final List<TimeZoneChangeEvent> mEvents = new ArrayList<>(); + + FakeTimeZoneChangeEventListener() { + } + + @Override + public void process(TimeZoneChangeEvent event) { + mEvents.add(event); + } + + public List<TimeZoneChangeEvent> getTimeZoneChangeEvents() { + return mEvents; + } + + @Override + public void dump(IndentingPrintWriter ipw) { + // No-op for tests + } + } + + private static void assertEmpty(Collection<?> collection) { + assertTrue( + "Expected empty, but contains (" + collection.size() + ") elements: " + collection, + collection.isEmpty()); + } + + private static void assertNotEmpty(Collection<?> collection) { + assertFalse("Expected not empty: " + collection, collection.isEmpty()); + } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 6cb24293a7d5..fa733e85c89c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -2509,6 +2509,134 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRepostWithNewChannel_afterAutogrouping_isRegrouped() { + final String pkg = "package"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + // Post ungrouped notifications => will be autogrouped + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, null, false); + notificationList.add(notification); + mGroupHelper.onNotificationPosted(notification, false); + } + + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + + // Post ungrouped notifications to a different section, below autogroup limit + Mockito.reset(mCallback); + // Post ungrouped notifications => will be autogrouped + final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_LOW); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 4242, + String.valueOf(i + 4242), UserHandle.SYSTEM, null, false, silentChannel); + notificationList.add(notification); + mGroupHelper.onNotificationPosted(notification, false); + } + + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + + // Update a notification to a different channel that moves it to a different section + Mockito.reset(mCallback); + final NotificationRecord notifToInvalidate = notificationList.get(0); + final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate); + final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + notifToInvalidate.updateNotificationChannel(updatedChannel); + assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection); + boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false); + assertThat(needsAutogrouping).isTrue(); + + // Check that the silent section was autogrouped + final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(silentSectionGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), + eq(silentSectionGroupKey), eq(true)); + verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey())); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), + eq(expectedGroupKey), any()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRepostWithNewChannel_afterForceGrouping_isRegrouped() { + final String pkg = "package"; + final String groupName = "testGroup"; + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + // Post valid section summary notifications without children => force group + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 42, + String.valueOf(i + 42), UserHandle.SYSTEM, groupName, false); + notificationList.add(notification); + mGroupHelper.onNotificationPostedWithDelay(notification, notificationList, + summaryByGroup); + } + + final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey), eq(true)); + + // Update a notification to a different channel that moves it to a different section + Mockito.reset(mCallback); + final NotificationRecord notifToInvalidate = notificationList.get(0); + final NotificationSectioner initialSection = GroupHelper.getSection(notifToInvalidate); + final NotificationChannel updatedChannel = new NotificationChannel("TEST_CHANNEL_ID2", + "TEST_CHANNEL_ID2", IMPORTANCE_LOW); + notifToInvalidate.updateNotificationChannel(updatedChannel); + assertThat(GroupHelper.getSection(notifToInvalidate)).isNotEqualTo(initialSection); + boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, false); + + mGroupHelper.onNotificationPostedWithDelay(notifToInvalidate, notificationList, + summaryByGroup); + + // Check that the updated notification is removed from the autogroup + assertThat(needsAutogrouping).isFalse(); + verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey())); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), + eq(expectedGroupKey), any()); + + // Post child notifications for the silent sectin => will be autogrouped + Mockito.reset(mCallback); + final NotificationChannel silentChannel = new NotificationChannel("TEST_CHANNEL_ID1", + "TEST_CHANNEL_ID1", IMPORTANCE_LOW); + for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) { + NotificationRecord notification = getNotificationRecord(pkg, i + 4242, + String.valueOf(i + 4242), UserHandle.SYSTEM, "aGroup", false, silentChannel); + notificationList.add(notification); + needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false); + assertThat(needsAutogrouping).isFalse(); + mGroupHelper.onNotificationPostedWithDelay(notification, notificationList, + summaryByGroup); + } + + // Check that the silent section was autogrouped + final String silentSectionGroupKey = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(silentSectionGroupKey), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(silentSectionGroupKey), eq(true)); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testMoveAggregateGroups_updateChannel() { final String pkg = "package"; 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 4f5cdb73edd2..1df8e3deb84b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java @@ -64,6 +64,8 @@ import android.testing.TestableContext; import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; +import android.util.StatsEvent; +import android.util.StatsEventTestUtils; import android.util.Xml; import androidx.test.runner.AndroidJUnit4; @@ -71,9 +73,17 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.CollectionUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import com.android.os.AtomsProto; +import com.android.os.notification.NotificationBundlePreferences; +import com.android.os.notification.NotificationExtensionAtoms; +import com.android.os.notification.NotificationProtoEnums; import com.android.server.UiServiceTestCase; import com.android.server.notification.NotificationManagerService.NotificationAssistants; +import com.google.protobuf.CodedInputStream; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.ExtensionRegistryLite; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -120,6 +130,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { ComponentName mCn = new ComponentName("a", "b"); + private ExtensionRegistryLite mRegistry; + // Helper function to hold mApproved lock, avoid GuardedBy lint errors private boolean isUserSetServicesEmpty(NotificationAssistants assistant, int userId) { @@ -204,6 +216,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase { when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds); when(mNm.isNASMigrationDone(anyInt())).thenReturn(true); when(mNm.canUseManagedServices(any(), anyInt(), any())).thenReturn(true); + mRegistry = ExtensionRegistryLite.newInstance(); + NotificationExtensionAtoms.registerAllExtensions(mRegistry); } @Test @@ -749,6 +763,28 @@ public class NotificationAssistantsTest extends UiServiceTestCase { } @Test + @EnableFlags({android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION, + android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testGetPackagesWithKeyTypeAdjustmentSettings() throws Exception { + String pkg = "my.package"; + String pkg2 = "my.package.2"; + setDefaultAllowedAdjustmentKeyTypes(mAssistants); + assertThat(mAssistants.isTypeAdjustmentAllowedForPackage(pkg, TYPE_PROMOTION)).isTrue(); + assertThat(mAssistants.getPackagesWithKeyTypeAdjustmentSettings()).isEmpty(); + + mAssistants.setAssistantAdjustmentKeyTypeStateForPackage(pkg, TYPE_PROMOTION, true); + assertThat(mAssistants.getPackagesWithKeyTypeAdjustmentSettings()) + .containsExactly(pkg); + mAssistants.setAssistantAdjustmentKeyTypeStateForPackage(pkg, TYPE_PROMOTION, false); + assertThat(mAssistants.getPackagesWithKeyTypeAdjustmentSettings()) + .containsExactly(pkg); + mAssistants.setAssistantAdjustmentKeyTypeStateForPackage(pkg2, TYPE_NEWS, true); + mAssistants.setAssistantAdjustmentKeyTypeStateForPackage(pkg2, TYPE_PROMOTION, false); + assertThat(mAssistants.getPackagesWithKeyTypeAdjustmentSettings()) + .containsExactly(pkg, pkg2); + } + + @Test @EnableFlags(android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI) public void testSetAssistantAdjustmentKeyTypeStateForPackage_usesGlobalDefault() { String pkg = "my.package"; @@ -892,4 +928,88 @@ public class NotificationAssistantsTest extends UiServiceTestCase { assertThat(mAssistants.getAllowedAdjustmentKeyTypesForPackage(pkg)).asList() .containsExactly(TYPE_NEWS, TYPE_PROMOTION, TYPE_SOCIAL_MEDIA); } + + @Test + @SuppressWarnings("GuardedBy") + @EnableFlags({android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION, + android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testPullBundlePreferencesStats_fillsOutStatsEvent() + throws Exception { + // Create the current user and enable the package + int userId = ActivityManager.getCurrentUser(); + mAssistants.loadDefaultsFromConfig(true); + mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true, + true, true); + ManagedServices.ManagedServiceInfo info = + mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256); + + // Ensure bundling is enabled + mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_TYPE, true); + // Enable these specific bundle types + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false); + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, true); + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true); + + // When pullBundlePreferencesStats is run with the given preferences + ArrayList<StatsEvent> events = new ArrayList<>(); + mAssistants.pullBundlePreferencesStats(events); + + // The StatsEvent is filled out with the expected NotificationBundlePreferences values. + assertThat(events.size()).isEqualTo(1); + AtomsProto.Atom atom = StatsEventTestUtils.convertToAtom(events.get(0)); + + // The returned atom does not have external extensions registered. + // So we serialize and then deserialize with extensions registered. + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + CodedOutputStream codedos = CodedOutputStream.newInstance(outputStream); + atom.writeTo(codedos); + codedos.flush(); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + CodedInputStream codedis = CodedInputStream.newInstance(inputStream); + atom = AtomsProto.Atom.parseFrom(codedis, mRegistry); + assertTrue(atom.hasExtension(NotificationExtensionAtoms.notificationBundlePreferences)); + NotificationBundlePreferences p = + atom.getExtension(NotificationExtensionAtoms.notificationBundlePreferences); + assertThat(p.getBundlesAllowed()).isTrue(); + assertThat(p.getAllowedBundleTypes(0).getNumber()) + .isEqualTo(NotificationProtoEnums.TYPE_NEWS); + assertThat(p.getAllowedBundleTypes(1).getNumber()) + .isEqualTo(NotificationProtoEnums.TYPE_CONTENT_RECOMMENDATION); + + // Disable the top-level bundling setting + mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_TYPE, false); + // Enable these specific bundle types + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, true); + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, false); + mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true); + + ArrayList<StatsEvent> eventsDisabled = new ArrayList<>(); + mAssistants.pullBundlePreferencesStats(eventsDisabled); + + // The StatsEvent is filled out with the expected NotificationBundlePreferences values. + assertThat(eventsDisabled.size()).isEqualTo(1); + AtomsProto.Atom atomDisabled = StatsEventTestUtils.convertToAtom(eventsDisabled.get(0)); + + // The returned atom does not have external extensions registered. + // So we serialize and then deserialize with extensions registered. + outputStream = new ByteArrayOutputStream(); + codedos = CodedOutputStream.newInstance(outputStream); + atomDisabled.writeTo(codedos); + codedos.flush(); + + inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + codedis = CodedInputStream.newInstance(inputStream); + atomDisabled = AtomsProto.Atom.parseFrom(codedis, mRegistry); + assertTrue(atomDisabled.hasExtension(NotificationExtensionAtoms + .notificationBundlePreferences)); + + NotificationBundlePreferences p2 = + atomDisabled.getExtension(NotificationExtensionAtoms.notificationBundlePreferences); + assertThat(p2.getBundlesAllowed()).isFalse(); + assertThat(p2.getAllowedBundleTypes(0).getNumber()) + .isEqualTo(NotificationProtoEnums.TYPE_PROMOTION); + assertThat(p2.getAllowedBundleTypes(1).getNumber()) + .isEqualTo(NotificationProtoEnums.TYPE_CONTENT_RECOMMENDATION); + } }
\ No newline at end of file diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 7885c9b902e2..e43b28bb9404 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -6341,6 +6341,26 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testOnlyAutogroupIfNeeded_channelChanged_ghUpdate() { + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, + "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false); + mService.addNotification(r); + + NotificationRecord update = generateNotificationRecord(mSilentChannel, 0, + "testOnlyAutogroupIfNeeded_channelChanged_ghUpdate", null, false); + mService.addEnqueuedNotification(update); + + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(update.getKey(), + update.getSbn().getPackageName(), update.getUid(), + mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean()); + } + + @Test public void testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate() { NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, "testOnlyAutogroupIfGroupChanged_noValidChange_noGhUpdate", null, false); @@ -17901,4 +17921,63 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary)); } + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testRebundleNotification_restoresBundleChannel() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString(), anyInt())).thenReturn(true); + + // Post a single notification + final boolean hasOriginalSummary = false; + final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); + final String keyToUnbundle = r.getKey(); + mService.addNotification(r); + + // Classify notification into the NEWS bundle + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS); + Adjustment adjustment = new Adjustment( + r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r.applyAdjustments(); + // Check that the NotificationRecord channel is updated + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS); + + // Unbundle the notification + mService.mNotificationDelegate.unbundleNotification(keyToUnbundle); + + // Check that the original channel was restored + assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID); + assertThat(r.getBundleType()).isEqualTo(Adjustment.TYPE_NEWS); + verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary)); + + Mockito.reset(mRankingHandler); + Mockito.reset(mGroupHelper); + + // Rebundle the notification + mService.mNotificationDelegate.rebundleNotification(keyToUnbundle); + + // Actually apply the adjustments + doAnswer(invocationOnMock -> { + ((NotificationRecord) invocationOnMock.getArguments()[0]).applyAdjustments(); + ((NotificationRecord) invocationOnMock.getArguments()[0]).calculateImportance(); + return null; + }).when(mRankingHelper).extractSignals(any(NotificationRecord.class)); + mService.handleRankingSort(); + verify(handler, times(1)).scheduleSendRankingUpdate(); + + // Check that the bundle channel was restored + verify(mRankingHandler, times(1)).requestSort(); + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + } + } 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 8e79514c875e..4bc286117ac6 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -19,6 +19,7 @@ import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW; import static android.app.Flags.FLAG_MODES_UI; +import static android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI; import static android.app.Notification.VISIBILITY_PRIVATE; import static android.app.Notification.VISIBILITY_SECRET; import static android.app.NotificationChannel.ALLOW_BUBBLE_ON; @@ -66,7 +67,6 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; -import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE; @@ -162,8 +162,10 @@ import com.android.modules.utils.TypedXmlSerializer; import com.android.os.AtomsProto; import com.android.os.AtomsProto.PackageNotificationChannelPreferences; import com.android.os.AtomsProto.PackageNotificationPreferences; +import com.android.os.notification.NotificationProtoEnums; import com.android.server.UiServiceTestCase; import com.android.server.notification.PermissionHelper.PackagePermission; +import com.android.server.uri.UriGrantsManagerInternal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -179,6 +181,9 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -199,9 +204,6 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA) @@ -239,9 +241,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { private NotificationManager.Policy mTestNotificationPolicy; - private PreferencesHelper mHelper; - // fresh object for testing xml reading - private PreferencesHelper mXmlHelper; + private TestPreferencesHelper mHelper; + // fresh object for testing xml reading; also TestPreferenceHelper in order to avoid interacting + // with real IpcDataCaches + private TestPreferencesHelper mXmlHelper; private AudioAttributes mAudioAttributes; private NotificationChannelLoggerFake mLogger = new NotificationChannelLoggerFake(); @@ -255,7 +258,8 @@ public class PreferencesHelperTest extends UiServiceTestCase { public static List<FlagsParameterization> getParams() { return FlagsParameterization.allCombinationsOf( android.app.Flags.FLAG_API_RICH_ONGOING, - FLAG_NOTIFICATION_CLASSIFICATION, FLAG_MODES_UI); + FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_CLASSIFICATION_UI, + FLAG_MODES_UI); } public PreferencesHelperTest(FlagsParameterization flags) { @@ -378,10 +382,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(mUserProfiles.getCurrentProfileIds()).thenReturn(currentProfileIds); when(mClock.millis()).thenReturn(System.currentTimeMillis()); - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); resetZenModeHelper(); @@ -793,7 +797,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_oldXml_migrates() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -929,7 +933,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_showPermissionNotification() throws Exception { - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -988,7 +992,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_permissionNotificationOff() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ false, mClock); @@ -1047,7 +1051,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testReadXml_newXml_noMigration_noPermissionNotification() throws Exception { - mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, /* showReviewPermissionsNotification= */ true, mClock); @@ -1641,7 +1645,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { serializer.flush(); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(baos.toByteArray(), false, USER_ALL); @@ -1696,7 +1700,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { Duration.ofDays(2).toMillis() + System.currentTimeMillis()); // simulate load after reboot - mXmlHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); loadByteArrayXml(xml.getBytes(), false, USER_ALL); @@ -1774,10 +1778,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { when(contentResolver.getResourceId(ANDROID_RES_SOUND_URI)).thenReturn(resId).thenThrow( new FileNotFoundException("")).thenReturn(resId); - mHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); - mXmlHelper = new PreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, + mXmlHelper = new TestPreferencesHelper(mContext, mPm, mHandler, mMockZenModeHelper, mPermissionHelper, mPermissionManager, mLogger, mAppOpsManager, mUserProfiles, mUgmInternal, false, mClock); @@ -3190,7 +3194,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_contentSchemeVerified() { final Uri sound = Uri.parse(SCHEME_CONTENT + "://media/test/sound/uri"); @@ -3210,7 +3213,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_fileSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_FILE + "://path/sound"); @@ -3229,7 +3231,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI) public void testCreateChannel_noSoundUriPermission_resourceSchemaIgnored() { final Uri sound = Uri.parse(SCHEME_ANDROID_RESOURCE + "://resId/sound"); @@ -6137,6 +6138,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + @DisableFlags({FLAG_NOTIFICATION_CLASSIFICATION_UI}) public void testPullPackagePreferencesStats_postPermissionMigration() throws InvalidProtocolBufferException { // make sure there's at least one channel for each package we want to test @@ -6157,6 +6159,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.canShowBadge(PKG_O, UID_O); mHelper.canShowBadge(PKG_P, UID_P); + ArrayList<StatsEvent> events = new ArrayList<>(); + + mHelper.pullPackagePreferencesStats(events, appPermissions, + new ArrayMap<String, Set<Integer>>()); + // expected output. format: uid -> importance, as only uid (and not package name) // is in PackageNotificationPreferences ArrayMap<Integer, Pair<Integer, Boolean>> expected = new ArrayMap<>(); @@ -6164,9 +6171,6 @@ public class PreferencesHelperTest extends UiServiceTestCase { expected.put(UID_O, new Pair<>(IMPORTANCE_NONE, true)); // banned by permissions expected.put(UID_P, new Pair<>(IMPORTANCE_UNSPECIFIED, false)); // default: unspecified - ArrayList<StatsEvent> events = new ArrayList<>(); - mHelper.pullPackagePreferencesStats(events, appPermissions); - assertEquals("total number of packages", 3, events.size()); for (StatsEvent ev : events) { AtomsProto.Atom atom = StatsEventTestUtils.convertToAtom(ev); @@ -6182,6 +6186,74 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_CLASSIFICATION_UI}) + public void testPullPackagePreferencesStats_createsExpectedStatsEvents() + throws InvalidProtocolBufferException { + // make sure there's at least one channel for each package we want to test + NotificationChannel channelA = new NotificationChannel("a", "a", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channelA, true, false, + UID_N_MR1, false); + NotificationChannel channelB = new NotificationChannel("b", "b", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_O, UID_O, channelB, true, false, UID_O, false); + NotificationChannel channelC = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_P, UID_P, channelC, true, false, UID_P, false); + + // build a collection of app permissions that should be passed in and used + ArrayMap<Pair<Integer, String>, Pair<Boolean, Boolean>> pkgPermissions = new ArrayMap<>(); + pkgPermissions.put(new Pair<>(UID_N_MR1, PKG_N_MR1), new Pair<>(true, false)); + pkgPermissions.put(new Pair<>(UID_O, PKG_O), new Pair<>(false, true)); // in local prefs + + // local preferences + mHelper.canShowBadge(PKG_O, UID_O); + mHelper.canShowBadge(PKG_P, UID_P); + + // Sets bundles_allowed to true for these packages. + ArrayMap<String, Set<Integer>> packageSpecificAdjustmentKeyTypes = new ArrayMap<>(); + Set<Integer> nMr1BundlesSet = new ArraySet<Integer>(); + nMr1BundlesSet.add(TYPE_NEWS); + nMr1BundlesSet.add(TYPE_SOCIAL_MEDIA); + packageSpecificAdjustmentKeyTypes.put(PKG_N_MR1, nMr1BundlesSet); + Set<Integer> pBundlesSet = new ArraySet<Integer>(); + packageSpecificAdjustmentKeyTypes.put(PKG_P, pBundlesSet); + + ArrayList<StatsEvent> events = new ArrayList<>(); + + mHelper.pullPackagePreferencesStats(events, pkgPermissions, + packageSpecificAdjustmentKeyTypes); + + assertEquals("total number of packages", 3, events.size()); + + AtomsProto.Atom atom0 = StatsEventTestUtils.convertToAtom(events.get(0)); + assertTrue(atom0.hasPackageNotificationPreferences()); + PackageNotificationPreferences p0 = atom0.getPackageNotificationPreferences(); + assertThat(p0.getUid()).isEqualTo(UID_O); + assertThat(p0.getImportance()).isEqualTo(IMPORTANCE_NONE); // banned by permissions + assertThat(p0.getUserSetImportance()).isTrue(); + assertThat(p0.getAllowedBundleTypesList()).hasSize(0); + + AtomsProto.Atom atom1 = StatsEventTestUtils.convertToAtom(events.get(1)); + assertTrue(atom1.hasPackageNotificationPreferences()); + PackageNotificationPreferences p1 = atom1.getPackageNotificationPreferences(); + assertThat(p1.getUid()).isEqualTo(UID_N_MR1); + assertThat(p1.getImportance()).isEqualTo(IMPORTANCE_DEFAULT); + assertThat(p1.getUserSetImportance()).isFalse(); + assertThat(p1.getAllowedBundleTypesList()).hasSize(2); + + assertThat(p1.getAllowedBundleTypes(0).getNumber()) + .isEqualTo(NotificationProtoEnums.TYPE_SOCIAL_MEDIA); + assertThat(p1.getAllowedBundleTypes(1).getNumber()) + .isEqualTo(NotificationProtoEnums.TYPE_NEWS); + + AtomsProto.Atom atom2 = StatsEventTestUtils.convertToAtom(events.get(2)); + assertTrue(atom2.hasPackageNotificationPreferences()); + PackageNotificationPreferences p2 = atom2.getPackageNotificationPreferences(); + assertThat(p2.getUid()).isEqualTo(UID_P); + assertThat(p2.getImportance()).isEqualTo(IMPORTANCE_UNSPECIFIED); // default: unspecified + assertThat(p2.getUserSetImportance()).isFalse(); + assertThat(p2.getAllowedBundleTypesList()).hasSize(0); + } + + @Test public void testUnlockNotificationChannelImportance() { NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); mHelper.createNotificationChannel(PKG_O, UID_O, channel, true, false, UID_O, false); @@ -6573,4 +6645,223 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.setCanBePromoted(PKG_P, UID_P, false, false); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnCreationAndChange() { + mHelper.resetCacheInvalidation(); + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // new channel should invalidate the cache. + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // when the channel data is updated, should invalidate the cache again after that. + mHelper.resetCacheInvalidation(); + NotificationChannel newChannel = channel.copy(); + newChannel.setName("new name"); + newChannel.setImportance(IMPORTANCE_HIGH); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // also for conversations + mHelper.resetCacheInvalidation(); + String parentId = "id"; + String convId = "conversation"; + NotificationChannel conv = new NotificationChannel( + String.format(CONVERSATION_CHANNEL_ID_FORMAT, parentId, convId), "conversation", + IMPORTANCE_DEFAULT); + conv.setConversationId(parentId, convId); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, conv, true, false, UID_N_MR1, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + mHelper.resetCacheInvalidation(); + NotificationChannel newConv = conv.copy(); + newConv.setName("changed"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newConv, true, UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_invalidateOnDelete() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // recreate channel and now permanently delete + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + mHelper.resetCacheInvalidation(); + mHelper.permanentlyDeleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id"); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateChannelCache_noInvalidationWhenNoChange() { + NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, UID_N_MR1, + false); + + // ignore any invalidations up until now + mHelper.resetCacheInvalidation(); + + // newChannel, same as the old channel + NotificationChannel newChannel = channel.copy(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, false, UID_N_MR1, + false); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, newChannel, true, UID_N_MR1, false); + + // because there were no effective changes, we should not see any cache invalidations + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // deletions of a nonexistent channel also don't change anything + mHelper.resetCacheInvalidation(); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "nonexistent", UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_multipleUsersAndPackages() { + // Setup: create channels for: + // pkg O, user + // pkg O, work (same channel ID, different user) + // pkg N_MR1, user + // pkg N_MR1, user, conversation child of above + String p2u1ConvId = String.format(CONVERSATION_CHANNEL_ID_FORMAT, "p2", "conv"); + NotificationChannel p1u1 = new NotificationChannel("p1", "p1u1", IMPORTANCE_DEFAULT); + NotificationChannel p1u2 = new NotificationChannel("p1", "p1u2", IMPORTANCE_DEFAULT); + NotificationChannel p2u1 = new NotificationChannel("p2", "p2u1", IMPORTANCE_DEFAULT); + NotificationChannel p2u1Conv = new NotificationChannel(p2u1ConvId, "p2u1 conv", + IMPORTANCE_DEFAULT); + p2u1Conv.setConversationId("p2", "conv"); + + mHelper.createNotificationChannel(PKG_O, UID_O, p1u1, true, + false, UID_O, false); + mHelper.createNotificationChannel(PKG_O, UID_O + UserHandle.PER_USER_RANGE, p1u2, true, + false, UID_O + UserHandle.PER_USER_RANGE, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1, true, + false, UID_N_MR1, false); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, p2u1Conv, true, + false, UID_N_MR1, false); + mHelper.resetCacheInvalidation(); + + // Update to an existent channel, with a change: should invalidate + NotificationChannel p1u1New = p1u1.copy(); + p1u1New.setName("p1u1 new"); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New, true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // Do it again, but no change for this user + mHelper.resetCacheInvalidation(); + mHelper.updateNotificationChannel(PKG_O, UID_O, p1u1New.copy(), true, UID_O, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Delete conversations, but for a package without those conversations + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_O, UID_O, Set.of(p2u1Conv.getConversationId()), UID_O, + false); + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + + // Now delete conversations for the right package + mHelper.resetCacheInvalidation(); + mHelper.deleteConversations(PKG_N_MR1, UID_N_MR1, Set.of(p2u1Conv.getConversationId()), + UID_N_MR1, false); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_userRemoved() throws Exception { + NotificationChannel c1 = new NotificationChannel("id1", "name1", IMPORTANCE_DEFAULT); + int uid1 = UserHandle.getUid(1, 1); + setUpPackageWithUid("pkg1", uid1); + mHelper.createNotificationChannel("pkg1", uid1, c1, true, false, uid1, false); + mHelper.resetCacheInvalidation(); + + // delete user 1; should invalidate cache + mHelper.onUserRemoved(1); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_packagesChanged() { + NotificationChannel channel1 = + new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + + // package deleted: expect cache invalidation + mHelper.resetCacheInvalidation(); + mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + + // re-created: expect cache invalidation again + mHelper.resetCacheInvalidation(); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, + UID_N_MR1, false); + mHelper.onPackagesChanged(false, USER_SYSTEM, new String[]{PKG_N_MR1}, + new int[]{UID_N_MR1}); + assertThat(mHelper.hasCacheBeenInvalidated()).isTrue(); + } + + @Test + @DisableFlags(android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS) + public void testInvalidateCache_flagOff_neverTouchesCache() { + // Do a bunch of channel-changing operations. + NotificationChannel channel = + new NotificationChannel("id", "name1", NotificationManager.IMPORTANCE_HIGH); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel, true, false, + UID_N_MR1, false); + + NotificationChannel copy = channel.copy(); + copy.setName("name2"); + mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, copy, true, UID_N_MR1, false); + mHelper.deleteNotificationChannel(PKG_N_MR1, UID_N_MR1, "id", UID_N_MR1, false); + + assertThat(mHelper.hasCacheBeenInvalidated()).isFalse(); + } + + // Test version of PreferencesHelper whose only functional difference is that it does not + // interact with the real IpcDataCache, and instead tracks whether or not the cache has been + // invalidated since creation or the last reset. + private static class TestPreferencesHelper extends PreferencesHelper { + private boolean mCacheInvalidated = false; + + TestPreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler, + ZenModeHelper zenHelper, PermissionHelper permHelper, PermissionManager permManager, + NotificationChannelLogger notificationChannelLogger, + AppOpsManager appOpsManager, ManagedServices.UserProfiles userProfiles, + UriGrantsManagerInternal ugmInternal, + boolean showReviewPermissionsNotification, Clock clock) { + super(context, pm, rankingHandler, zenHelper, permHelper, permManager, + notificationChannelLogger, appOpsManager, userProfiles, ugmInternal, + showReviewPermissionsNotification, clock); + } + + @Override + protected void invalidateNotificationChannelCache() { + mCacheInvalidated = true; + } + + boolean hasCacheBeenInvalidated() { + return mCacheInvalidated; + } + + void resetCacheInvalidation() { + mCacheInvalidated = false; + } + } } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java new file mode 100644 index 000000000000..09f573cd1ee0 --- /dev/null +++ b/services/tests/vibrator/src/com/android/server/vibrator/BasicToPwleSegmentAdapterTest.java @@ -0,0 +1,158 @@ +/* + * 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.vibrator; + +import static com.google.common.truth.Truth.assertThat; + +import android.hardware.vibrator.IVibrator; +import android.os.VibratorInfo; +import android.os.vibrator.BasicPwleSegment; +import android.os.vibrator.Flags; +import android.os.vibrator.PwleSegment; +import android.os.vibrator.StepSegment; +import android.os.vibrator.VibrationEffectSegment; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +public class BasicToPwleSegmentAdapterTest { + + private static final float TEST_RESONANT_FREQUENCY = 150; + private static final float[] TEST_FREQUENCIES = + new float[]{90f, 120f, 150f, 60f, 30f, 210f, 270f, 300f, 240f, 180f}; + private static final float[] TEST_OUTPUT_ACCELERATIONS = + new float[]{1.2f, 1.8f, 2.4f, 0.6f, 0.1f, 2.2f, 1.0f, 0.5f, 1.9f, 3.0f}; + + private static final VibratorInfo.FrequencyProfile TEST_FREQUENCY_PROFILE = + new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, TEST_FREQUENCIES, + TEST_OUTPUT_ACCELERATIONS); + + private static final VibratorInfo.FrequencyProfile EMPTY_FREQUENCY_PROFILE = + new VibratorInfo.FrequencyProfile(TEST_RESONANT_FREQUENCY, null, null); + + private BasicToPwleSegmentAdapter mAdapter; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + mAdapter = new BasicToPwleSegmentAdapter(); + } + + @Test + @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_withFeatureFlagDisabled_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + + VibratorInfo vibratorInfo = createVibratorInfo( + TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_noPwleCapability_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + + VibratorInfo vibratorInfo = createVibratorInfo(TEST_FREQUENCY_PROFILE); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_invalidFrequencyProfile_returnsOriginalSegments() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.2f, 0.8f, 0.2f, 0.4f, 20), + new BasicPwleSegment(0.8f, 0.2f, 0.4f, 0.5f, 100), + new BasicPwleSegment(0.2f, 0.65f, 0.5f, 0.5f, 50))); + List<VibrationEffectSegment> originalSegments = new ArrayList<>(segments); + VibratorInfo vibratorInfo = createVibratorInfo( + EMPTY_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ -1)) + .isEqualTo(-1); + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(originalSegments); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testBasicPwleSegments_withPwleCapability_adaptSegmentsCorrectly() { + List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList( + new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100), + // startIntensity, endIntensity, startSharpness, endSharpness, duration + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100), + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100), + new BasicPwleSegment(0.0f, 1.0f, 0.0f, 1.0f, 100))); + List<VibrationEffectSegment> expectedSegments = Arrays.asList( + new StepSegment(/* amplitude= */ 1, /* frequencyHz= */ 40f, /* duration= */ 100), + // startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, duration + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100), + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100), + new PwleSegment(0.0f, 1.0f, 30.0f, 300.0f, 100)); + VibratorInfo vibratorInfo = createVibratorInfo( + TEST_FREQUENCY_PROFILE, IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + + assertThat(mAdapter.adaptToVibrator(vibratorInfo, segments, /*repeatIndex= */ 1)) + .isEqualTo(1); + + assertThat(segments).isEqualTo(expectedSegments); + } + + private static VibratorInfo createVibratorInfo(VibratorInfo.FrequencyProfile frequencyProfile, + int... capabilities) { + return new VibratorInfo.Builder(0) + .setCapabilities(IntStream.of(capabilities).reduce((a, b) -> a | b).orElse(0)) + .setFrequencyProfile(frequencyProfile) + .build(); + } +} diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp index 132f95bea360..b328fc2d5868 100644 --- a/services/tests/wmtests/Android.bp +++ b/services/tests/wmtests/Android.bp @@ -11,19 +11,46 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -// Include all test java files. filegroup { - name: "wmtests-sources", + name: "wmtests-support-sources", srcs: [ - "src/**/*.java", + "src/com/android/server/wm/WindowManagerServiceTestSupport.kt", ], + path: "src", + visibility: ["//visibility:private"], +} + +java_library { + name: "wmtests-support", + srcs: [":wmtests-support-sources"], + static_libs: [ + "com.android.window.flags.window-aconfig-java", + "kotlin-stdlib", + "services.core", + ], + lint: { + test: true, + }, + visibility: [ + "//frameworks/base/services/tests/wmtests", + "//frameworks/opt/car/services/updatableServices/tests", + ], +} + +// Include all test files, but exclude test support files. +filegroup { + name: "wmtests-sources", + srcs: ["src/**/*.java"], + exclude_srcs: [":wmtests-support-sources"], + path: "src", + visibility: ["//visibility:private"], } java_genrule { name: "wmtests.protologsrc", srcs: [ - ":protolog-impl", ":protolog-groups", + ":protolog-impl", ":wmtests-sources", ], tools: ["protologtool"], @@ -52,33 +79,34 @@ android_test { ], static_libs: [ - "frameworks-base-testutils", - "services.core", - "service-permission.stubs.system_server", - "androidx.test.runner", + "CtsSurfaceValidatorLib", + "android.view.inputmethod.flags-aconfig-java", "androidx.test.rules", + "androidx.test.runner", + "com.android.window.flags.window-aconfig-java", + "flag-junit", "flickerlib", + "frameworks-base-testutils", + "hamcrest-library", "junit-params", + "mockito-kotlin2", "mockito-target-extended-minus-junit4", + "platform-compat-test-rules", "platform-test-annotations", + "service-permission.stubs.system_server", + "service-sdksandbox.impl", + "services.core", "servicestests-utils", + "testables", "testng", "truth", - "testables", - "hamcrest-library", - "flag-junit", - "platform-compat-test-rules", - "CtsSurfaceValidatorLib", - "service-sdksandbox.impl", - "com.android.window.flags.window-aconfig-java", - "android.view.inputmethod.flags-aconfig-java", - "flag-junit", + "wmtests-support", ], libs: [ "android.hardware.power-V1-java", - "android.test.mock.stubs.system", "android.test.base.stubs.system", + "android.test.mock.stubs.system", "android.test.runner.stubs.system", ], @@ -94,8 +122,8 @@ android_test { platform_apis: true, test_suites: [ - "device-tests", "automotive-tests", + "device-tests", ], certificate: "platform", diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 6fad82b26808..c88d5153ed66 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -692,7 +692,7 @@ public class ActivityRecordTests extends WindowTestsBase { // Asserts fixed orientation request is not ignored, and the orientation is changed. assertNotEquals(activityCurOrientation, activity.getConfiguration().orientation); - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -721,13 +721,13 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation); // Clear size compat. - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); activity.ensureActivityConfiguration(); mDisplayContent.sendNewConfiguration(); // Relaunching the app should still respect the orientation request. assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation); - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -742,7 +742,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation); // The app should be letterboxed. assertEquals(ORIENTATION_PORTRAIT, activity.getConfiguration().orientation); - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -755,7 +755,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation); // Activity is not letterboxed. assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation); - assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -770,7 +770,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation); // Activity is not letterboxed. assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation); - assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -785,7 +785,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(ORIENTATION_LANDSCAPE, activity.getTask().getConfiguration().orientation); // Activity is not letterboxed. assertEquals(ORIENTATION_LANDSCAPE, activity.getConfiguration().orientation); - assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -3800,7 +3800,7 @@ public class ActivityRecordTests extends WindowTestsBase { .setResizeMode(RESIZE_MODE_RESIZEABLE) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); return activity; } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index 00c9691835db..271364445c6a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -180,7 +180,7 @@ class AppCompatActivityRobot { void setLetterboxedForFixedOrientationAndAspectRatio(boolean enabled) { doReturn(enabled).when(mActivityStack.top().mAppCompatController - .getAppCompatAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio(); + .getAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio(); } void enableFullscreenCameraCompatTreatmentForTopActivity(boolean enabled) { @@ -525,7 +525,7 @@ class AppCompatActivityRobot { activity.setRequestedOrientation(screenOrientation); } // Make sure to use the provided configuration to construct the size compat fields. - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); activity.ensureActivityConfiguration(); // Make sure the display configuration reflects the change of activity. if (activity.mDisplayContent.updateOrientation()) { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java index e046f7cc5845..b5ba111e4e72 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatLetterboxPolicyTest.java @@ -364,7 +364,7 @@ public class AppCompatLetterboxPolicyTest extends WindowTestsBase { @NonNull private AppCompatAspectRatioPolicy getAspectRatioPolicy() { - return activity().top().mAppCompatController.getAppCompatAspectRatioPolicy(); + return activity().top().mAppCompatController.getAspectRatioPolicy(); } @NonNull diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java index 9d191cea8acb..9e46c09f3ff6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java @@ -275,7 +275,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); + spyOn(activity.mAppCompatController.getAspectRatioPolicy()); } // Useful to reduce timeout during tests @@ -335,7 +335,7 @@ public class AppCompatOrientationOverridesTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index a21ab5de5de2..93fb73edb644 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -556,7 +556,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides()); - spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); + spyOn(activity.mAppCompatController.getAspectRatioPolicy()); } @Override @@ -601,7 +601,7 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { } private AppCompatOrientationOverrides getTopOrientationOverrides() { - return activity().top().mAppCompatController.getAppCompatOrientationOverrides(); + return activity().top().mAppCompatController.getOrientationOverrides(); } private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java index 463254caa845..50419d46f48f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java @@ -159,8 +159,8 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); - activity.mAppCompatController.getAppCompatReachabilityPolicy() + spyOn(activity.mAppCompatController.getReachabilityOverrides()); + activity.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); } @@ -196,7 +196,7 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { @NonNull private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + return activity().top().mAppCompatController.getReachabilityOverrides(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java index ddc4de9cfd8a..09b8bce2c930 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java @@ -246,8 +246,8 @@ public class AppCompatReachabilityPolicyTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides()); - activity.mAppCompatController.getAppCompatReachabilityPolicy() + spyOn(activity.mAppCompatController.getReachabilityOverrides()); + activity.mAppCompatController.getReachabilityPolicy() .setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier); } @@ -281,12 +281,12 @@ public class AppCompatReachabilityPolicyTest extends WindowTestsBase { @NonNull private AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() { - return activity().top().mAppCompatController.getAppCompatReachabilityOverrides(); + return activity().top().mAppCompatController.getReachabilityOverrides(); } @NonNull private AppCompatReachabilityPolicy getAppCompatReachabilityPolicy() { - return activity().top().mAppCompatController.getAppCompatReachabilityPolicy(); + return activity().top().mAppCompatController.getReachabilityPolicy(); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java index a5b2cb39cfff..bfd533aa8f79 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java @@ -252,7 +252,7 @@ public class AppCompatUtilsTest extends WindowTestsBase { @Override void onPostActivityCreation(@NonNull ActivityRecord activity) { super.onPostActivityCreation(activity); - spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy()); + spyOn(activity.mAppCompatController.getAspectRatioPolicy()); } @Override @@ -272,13 +272,13 @@ public class AppCompatUtilsTest extends WindowTestsBase { void setIsLetterboxedForFixedOrientationAndAspectRatio( boolean forFixedOrientationAndAspectRatio) { - when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy() + when(activity().top().mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()) .thenReturn(forFixedOrientationAndAspectRatio); } void setIsLetterboxedForAspectRatioOnly(boolean forAspectRatio) { - when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy() + when(activity().top().mAppCompatController.getAspectRatioPolicy() .isLetterboxedForAspectRatioOnly()).thenReturn(forAspectRatio); } diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index b6f442460ec8..576d17af2e79 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -606,7 +606,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { .setOnTop(true) .setTask(task) .build(); - mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); spyOn(mActivity.mAppCompatController.getAppCompatCameraOverrides()); spyOn(mActivity.info); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 5486aa34b5fa..dfd10ec86a20 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1183,6 +1183,18 @@ public class DisplayContentTests extends WindowTestsBase { assertEquals(prev, mDisplayContent.getLastOrientationSource()); // The top will use the rotation from "prev" with fixed rotation. assertTrue(top.hasFixedRotationTransform()); + + mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp(); + assertFalse(top.hasFixedRotationTransform()); + + // Assume that the requested orientation of "prev" is landscape. And the display is also + // rotated to landscape. The activities from bottom to top are TaskB{"prev, "behindTop"}, + // TaskB{"top"}. Then "behindTop" should also get landscape according to ORIENTATION_BEHIND + // instead of resolving as undefined which causes to unexpected fixed portrait rotation. + final ActivityRecord behindTop = new ActivityBuilder(mAtm).setTask(prev.getTask()) + .setOnTop(false).setScreenOrientation(SCREEN_ORIENTATION_BEHIND).build(); + mDisplayContent.applyFixedRotationForNonTopVisibleActivityIfNeeded(behindTop); + assertFalse(behindTop.hasFixedRotationTransform()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index de4b6fac7abf..dc16de1aab5e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -34,6 +34,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -42,16 +43,19 @@ import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.annotation.Nullable; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; import android.content.pm.ShortcutServiceInternal; import android.graphics.PixelFormat; +import android.graphics.Rect; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -60,6 +64,7 @@ import android.os.Message; import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.view.DragEvent; import android.view.InputChannel; @@ -74,6 +79,7 @@ import androidx.test.filters.SmallTest; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; +import com.android.window.flags.Flags; import org.junit.After; import org.junit.AfterClass; @@ -87,6 +93,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; /** * Tests for the {@link DragDropController} class. @@ -141,17 +148,28 @@ public class DragDropControllerTests extends WindowTestsBase { } } + private WindowState createDropTargetWindow(String name) { + return createDropTargetWindow(name, null /* targetDisplay */); + } + /** * Creates a window state which can be used as a drop target. */ - private WindowState createDropTargetWindow(String name, int ownerId) { - final Task task = new TaskBuilder(mSupervisor).setUserId(ownerId).build(); - final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess( - mProcess).build(); + private WindowState createDropTargetWindow(String name, + @Nullable DisplayContent targetDisplay) { + final WindowState window; + if (targetDisplay == null) { + final Task task = new TaskBuilder(mSupervisor).build(); + final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).setUseProcess( + mProcess).build(); + window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( + activity).setClientWindow(new TestIWindow()).build(); + } else { + window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setDisplay( + targetDisplay).setClientWindow(new TestIWindow()).build(); + } // Use a new TestIWindow so we don't collect events for other windows - final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( - activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build(); InputChannel channel = new InputChannel(); window.openInputChannel(channel); window.mHasSurface = true; @@ -174,7 +192,7 @@ public class DragDropControllerTests extends WindowTestsBase { public void setUp() throws Exception { mTarget = new TestDragDropController(mWm, mWm.mH.getLooper()); mProcess = mSystemServicesTestRule.addProcess(TEST_PACKAGE, "testProc", TEST_PID, TEST_UID); - mWindow = createDropTargetWindow("Drag test window", 0); + mWindow = createDropTargetWindow("Drag test window"); doReturn(mWindow).when(mDisplayContent).getTouchableWinAtPointLocked(0, 0); when(mWm.mInputManager.startDragAndDrop(any(IBinder.class), any(IBinder.class))).thenReturn( true); @@ -239,7 +257,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow.setDragEventJournal(dragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event is sent for invisible windows final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); @@ -263,8 +281,8 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testPrivateInterceptGlobalDragDropIgnoresNonLocalWindows() { - WindowState nonLocalWindow = createDropTargetWindow("App drag test window", 0); - WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window", 0); + WindowState nonLocalWindow = createDropTargetWindow("App drag test window"); + WindowState globalInterceptWindow = createDropTargetWindow("Global drag test window"); globalInterceptWindow.mAttrs.privateFlags |= PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events @@ -281,7 +299,7 @@ public class DragDropControllerTests extends WindowTestsBase { globalInterceptIWindow.setDragEventJournal(globalInterceptWindowDragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, - createClipDataForActivity(null, mock(UserHandle.class)), () -> { + createClipDataForActivity(null, mock(UserHandle.class)), (unused) -> { // Verify the start-drag event is sent for the local and global intercept window // but not the other window assertTrue(nonLocalWindowDragEvents.isEmpty()); @@ -324,7 +342,7 @@ public class DragDropControllerTests extends WindowTestsBase { iwindow.setDragEventJournal(dragEvents); startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { // Verify the start-drag event has the drag flags final DragEvent dragEvent = dragEvents.get(0); assertTrue(dragEvent.getAction() == ACTION_DRAG_STARTED); @@ -347,6 +365,184 @@ public class DragDropControllerTests extends WindowTestsBase { }); } + @Test + public void testDragEventCoordinates() { + int dragStartX = mWindow.getBounds().centerX(); + int dragStartY = mWindow.getBounds().centerY(); + int startOffsetPx = 10; + int dropCoordsPx = 15; + WindowState window2 = createDropTargetWindow("App drag test window"); + Rect bounds = new Rect(dragStartX + startOffsetPx, dragStartY + startOffsetPx, + mWindow.getBounds().right, mWindow.getBounds().bottom); + window2.setBounds(bounds); + window2.getFrame().set(bounds); + + // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events + // immediately after dispatching, which is a problem when using mockito arguments captor + // because it returns and modifies the same drag event. + TestIWindow iwindow = (TestIWindow) mWindow.mClient; + final ArrayList<DragEvent> dragEvents = new ArrayList<>(); + iwindow.setDragEventJournal(dragEvents); + TestIWindow iwindow2 = (TestIWindow) window2.mClient; + final ArrayList<DragEvent> dragEvents2 = new ArrayList<>(); + iwindow2.setDragEventJournal(dragEvents2); + + startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (unused) -> { + // Verify the start-drag event is sent as-is for the drag origin window. + final DragEvent dragEvent = dragEvents.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); + assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */); + assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */); + // Verify the start-drag event is sent relative to the window top-left. + final DragEvent dragEvent2 = dragEvents2.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction()); + assertEquals(-startOffsetPx, dragEvent2.getX(), 0.0 /* delta */); + assertEquals(-startOffsetPx, dragEvent2.getY(), 0.0 /* delta */); + + try { + mTarget.mDeferDragStateClosed = true; + // x, y is window-local coordinate. + mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx, + dropCoordsPx); + mTarget.handleMotionEvent(false, window2.getDisplayId(), dropCoordsPx, + dropCoordsPx); + mToken = window2.mClient.asBinder(); + // Verify only window2 received the DROP event and coords are sent as-is. + assertEquals(1, dragEvents.size()); + assertEquals(2, dragEvents2.size()); + final DragEvent dropEvent = last(dragEvents2); + assertEquals(ACTION_DROP, dropEvent.getAction()); + assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); + assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + assertEquals(window2.getDisplayId(), dropEvent.getDisplayId()); + + mTarget.reportDropResult(iwindow2, true); + // Verify both windows received ACTION_DRAG_ENDED event. + assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction()); + assertEquals(window2.getDisplayId(), last(dragEvents).getDisplayId()); + assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction()); + assertEquals(window2.getDisplayId(), last(dragEvents2).getDisplayId()); + } finally { + mTarget.mDeferDragStateClosed = false; + } + }); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND) + public void testDragEventConnectedDisplaysCoordinates() { + final DisplayContent testDisplay = createMockSimulatedDisplay(); + int dragStartX = mWindow.getBounds().centerX(); + int dragStartY = mWindow.getBounds().centerY(); + int dropCoordsPx = 15; + WindowState window2 = createDropTargetWindow("App drag test window", testDisplay); + + // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events + // immediately after dispatching, which is a problem when using mockito arguments captor + // because it returns and modifies the same drag event. + TestIWindow iwindow = (TestIWindow) mWindow.mClient; + final ArrayList<DragEvent> dragEvents = new ArrayList<>(); + iwindow.setDragEventJournal(dragEvents); + TestIWindow iwindow2 = (TestIWindow) window2.mClient; + final ArrayList<DragEvent> dragEvents2 = new ArrayList<>(); + iwindow2.setDragEventJournal(dragEvents2); + + startDrag(dragStartX, dragStartY, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (unused) -> { + // Verify the start-drag event is sent as-is for the drag origin window. + final DragEvent dragEvent = dragEvents.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent.getAction()); + assertEquals(dragStartX, dragEvent.getX(), 0.0 /* delta */); + assertEquals(dragStartY, dragEvent.getY(), 0.0 /* delta */); + // Verify the start-drag event from different display is sent out of display + // bounds. + final DragEvent dragEvent2 = dragEvents2.get(0); + assertEquals(ACTION_DRAG_STARTED, dragEvent2.getAction()); + assertEquals(-window2.getBounds().left - 1, dragEvent2.getX(), 0.0 /* delta */); + assertEquals(-window2.getBounds().top - 1, dragEvent2.getY(), 0.0 /* delta */); + + try { + mTarget.mDeferDragStateClosed = true; + mTarget.handleMotionEvent(true, testDisplay.getDisplayId(), dropCoordsPx, + dropCoordsPx); + // x, y is window-local coordinate. + mTarget.reportDropWindow(window2.mInputChannelToken, dropCoordsPx, + dropCoordsPx); + mTarget.handleMotionEvent(false, testDisplay.getDisplayId(), dropCoordsPx, + dropCoordsPx); + mToken = window2.mClient.asBinder(); + // Verify only window2 received the DROP event and coords are sent as-is + assertEquals(1, dragEvents.size()); + assertEquals(2, dragEvents2.size()); + final DragEvent dropEvent = last(dragEvents2); + assertEquals(ACTION_DROP, dropEvent.getAction()); + assertEquals(dropCoordsPx, dropEvent.getX(), 0.0 /* delta */); + assertEquals(dropCoordsPx, dropEvent.getY(), 0.0 /* delta */); + assertEquals(testDisplay.getDisplayId(), dropEvent.getDisplayId()); + + mTarget.reportDropResult(iwindow2, true); + // Verify both windows received ACTION_DRAG_ENDED event. + assertEquals(ACTION_DRAG_ENDED, last(dragEvents).getAction()); + assertEquals(testDisplay.getDisplayId(), last(dragEvents).getDisplayId()); + assertEquals(ACTION_DRAG_ENDED, last(dragEvents2).getAction()); + assertEquals(testDisplay.getDisplayId(), last(dragEvents2).getDisplayId()); + } finally { + mTarget.mDeferDragStateClosed = false; + } + }); + } + + @Test + public void testDragMove() { + startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (surface) -> { + int dragMoveX = mWindow.getBounds().centerX(); + int dragMoveY = mWindow.getBounds().centerY(); + final SurfaceControl.Transaction transaction = + mSystemServicesTestRule.mTransaction; + clearInvocations(transaction); + + mTarget.handleMotionEvent(true, mWindow.getDisplayId(), dragMoveX, dragMoveY); + verify(transaction).setPosition(surface, dragMoveX, dragMoveY); + + // Clean-up. + mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0, + 0); + mToken = mWindow.mClient.asBinder(); + }); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_DND) + public void testConnectedDisplaysDragMoveToOtherDisplay() { + final float testDensityMultiplier = 1.5f; + final DisplayContent testDisplay = createMockSimulatedDisplay(); + testDisplay.mBaseDisplayDensity = + (int) (mDisplayContent.mBaseDisplayDensity * testDensityMultiplier); + WindowState testWindow = createDropTargetWindow("App drag test window", testDisplay); + + // Test starts from mWindow which is on default display. + startDrag(0, 0, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ, + ClipData.newPlainText("label", "text"), (surface) -> { + final SurfaceControl.Transaction transaction = + mSystemServicesTestRule.mTransaction; + clearInvocations(transaction); + mTarget.handleMotionEvent(true, testWindow.getDisplayId(), 0, 0); + + verify(transaction).reparent(surface, testDisplay.getSurfaceControl()); + verify(transaction).setScale(surface, testDensityMultiplier, + testDensityMultiplier); + + // Clean-up. + mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), 0, + 0); + mToken = mWindow.mClient.asBinder(); + }); + } + private DragEvent last(ArrayList<DragEvent> list) { return list.get(list.size() - 1); } @@ -503,7 +699,7 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testRequestSurfaceForReturnAnimationFlag_dropSuccessful() { - WindowState otherWindow = createDropTargetWindow("App drag test window", 0); + WindowState otherWindow = createDropTargetWindow("App drag test window"); TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient; // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events @@ -515,7 +711,7 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify after consuming that the drag surface is relinquished @@ -534,7 +730,7 @@ public class DragDropControllerTests extends WindowTestsBase { @Test public void testRequestSurfaceForReturnAnimationFlag_dropUnsuccessful() { - WindowState otherWindow = createDropTargetWindow("App drag test window", 0); + WindowState otherWindow = createDropTargetWindow("App drag test window"); TestIWindow otherIWindow = (TestIWindow) otherWindow.mClient; // Necessary for now since DragState.sendDragStartedLocked() will recycle drag events @@ -546,7 +742,7 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION, - ClipData.newPlainText("label", "text"), () -> { + ClipData.newPlainText("label", "text"), (unused) -> { assertTrue(dragEvents.get(0).getAction() == ACTION_DRAG_STARTED); // Verify after consuming that the drag surface is relinquished @@ -583,7 +779,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), @@ -608,7 +804,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), @@ -631,7 +827,7 @@ public class DragDropControllerTests extends WindowTestsBase { doReturn(mock(Binder.class)).when(listener).asBinder(); mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; - startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { + startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was not called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(), @@ -654,7 +850,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.setGlobalDragListener(listener); final int invalidXY = 100_000; startDrag(View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG, - ClipData.newPlainText("label", "Test"), () -> { + ClipData.newPlainText("label", "Test"), (unused) -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); mTarget.handleMotionEvent(false /* keepHandling */, @@ -675,7 +871,7 @@ public class DragDropControllerTests extends WindowTestsBase { } private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { - startDrag(flags, data, () -> { + startDrag(flags, data, (unused) -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY); mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX, dropY); @@ -686,19 +882,26 @@ public class DragDropControllerTests extends WindowTestsBase { /** * Starts a drag with the given parameters, calls Runnable `r` after drag is started. */ - private void startDrag(int flag, ClipData data, Runnable r) { + private void startDrag(int flag, ClipData data, Consumer<SurfaceControl> c) { + startDrag(0, 0, flag, data, c); + } + + /** + * Starts a drag with the given parameters, calls Runnable `r` after drag is started. + */ + private void startDrag(float startInWindowX, float startInWindowY, int flag, ClipData data, + Consumer<SurfaceControl> c) { final SurfaceSession appSession = new SurfaceSession(); try { final SurfaceControl surface = new SurfaceControl.Builder(appSession).setName( "drag surface").setBufferSize(100, 100).setFormat( PixelFormat.TRANSLUCENT).build(); - assertTrue(mWm.mInputManager.startDragAndDrop(new Binder(), new Binder())); - mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, 0, 0, - 0, 0, data); + mToken = mTarget.performDrag(TEST_PID, 0, mWindow.mClient, flag, surface, 0, 0, 0, + startInWindowX, startInWindowY, 0, 0, data); assertNotNull(mToken); - r.run(); + c.accept(surface); } finally { appSession.kill(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java index 347d1bc1becc..a7e8ce915f07 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java @@ -216,7 +216,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { final Rect activityBounds = new Rect(mFirstActivity.getBounds()); // DAG is portrait (860x1200), so Task and Activity fill DAG. - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); assertThat(taskBounds).isEqualTo(dagBounds); @@ -241,7 +241,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { new Rect(mFirstActivity.getConfiguration().windowConfiguration.getBounds()); // DAG is landscape (1200x860), no fixed orientation letterbox - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isTrue(); assertThat(newDagBounds.width()).isEqualTo(dagBounds.height()); @@ -266,12 +266,12 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { mDisplay.onLastFocusedTaskDisplayAreaChanged(mFirstTda); prepareUnresizable(mFirstActivity, SCREEN_ORIENTATION_NOSENSOR); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); rotateDisplay(mDisplay, ROTATION_90); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); } @@ -289,7 +289,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { // DAG is portrait (860x1200), and activity is letterboxed for fixed orientation // (860x[860x860/1200=616]). Task fills DAG. - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isTrue(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); assertThat(taskBounds).isEqualTo(dagBounds); @@ -307,7 +307,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { mDisplay.onLastFocusedTaskDisplayAreaChanged(mFirstTda); prepareUnresizable(mFirstActivity, SCREEN_ORIENTATION_NOSENSOR); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); } @@ -318,7 +318,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { mDisplay.onLastFocusedTaskDisplayAreaChanged(mFirstTda); prepareUnresizable(mFirstActivity, SCREEN_ORIENTATION_LOCKED); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); } @@ -338,7 +338,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { final Rect newActivityBounds = new Rect(mFirstActivity.getBounds()); // DAG is landscape (1200x860), no fixed orientation letterbox - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isTrue(); assertThat(newDagBounds.width()).isEqualTo(dagBounds.height()); @@ -364,7 +364,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { rotateDisplay(mDisplay, ROTATION_90); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); } @@ -527,7 +527,7 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(mDisplay.getLastOrientation()).isEqualTo(SCREEN_ORIENTATION_LANDSCAPE); assertThat(mFirstRoot.getConfiguration().orientation).isEqualTo(ORIENTATION_PORTRAIT); assertThat(mFirstActivity.getConfiguration().orientation).isEqualTo(ORIENTATION_PORTRAIT); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); @@ -540,14 +540,14 @@ public class DualDisplayAreaGroupPolicyTest extends WindowTestsBase { assertThat(mDisplay.getLastOrientation()).isEqualTo(SCREEN_ORIENTATION_PORTRAIT); assertThat(mSecondRoot.getConfiguration().orientation).isEqualTo(ORIENTATION_LANDSCAPE); assertThat(mSecondActivity.getConfiguration().orientation).isEqualTo(ORIENTATION_LANDSCAPE); - assertThat(mSecondActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mSecondActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isFalse(); assertThat(mSecondActivity.inSizeCompatMode()).isFalse(); // First activity is letterboxed in portrait as requested. assertThat(mFirstRoot.getConfiguration().orientation).isEqualTo(ORIENTATION_LANDSCAPE); assertThat(mFirstActivity.getConfiguration().orientation).isEqualTo(ORIENTATION_PORTRAIT); - assertThat(mFirstActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertThat(mFirstActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()).isTrue(); assertThat(mFirstActivity.inSizeCompatMode()).isFalse(); diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java index 3e87f1f96fcd..ee9673f5ee77 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java @@ -177,15 +177,16 @@ public class PersisterQueueTests { assertTrue("Target didn't call callback enough times.", mListener.waitForAllExpectedCallbackDone(TIMEOUT_ALLOWANCE)); + // Wait until writing thread is waiting, which indicates the thread is waiting for new tasks + // to appear. + assertTrue("Failed to wait until the writing thread is waiting.", + mTarget.waitUntilWritingThreadIsWaiting(TIMEOUT_ALLOWANCE)); + // Second item mFactory.setExpectedProcessedItemNumber(1); mListener.setExpectedOnPreProcessItemCallbackTimes(1); dispatchTime = SystemClock.uptimeMillis(); - // Synchronize on the instance to make sure we schedule the item after it starts to wait for - // task indefinitely. - synchronized (mTarget) { - mTarget.addItem(mFactory.createItem(), false); - } + mTarget.addItem(mFactory.createItem(), false); assertTrue("Target didn't process item enough times.", mFactory.waitForAllExpectedItemsProcessed(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE)); assertEquals("Target didn't process all items.", 2, mFactory.getTotalProcessedItemCount()); diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java index 7e62b89d35bb..fc4f54a431d6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java @@ -535,7 +535,7 @@ public class RootWindowContainerTests extends WindowTestsBase { final Rect bounds = new Rect(task.getBounds()); bounds.scale(0.5f); task.setBounds(bounds); - assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(task.autoRemoveRecents).isFalse(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 9d9f24cb50f2..b19af0d0809e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -185,31 +185,46 @@ public class SizeCompatTests extends WindowTestsBase { } private ActivityRecord setUpApp(DisplayContent display) { - return setUpApp(display, null /* appBuilder */); + return setUpApp(display, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder appBuilder) { + return setUpApp(display, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpApp(DisplayContent display, ActivityBuilder aBuilder, + TaskBuilder tBuilder) { // Use the real package name (com.android.frameworks.wmtests) so that // EnableCompatChanges/DisableCompatChanges can take effect. // Otherwise the fake WindowTestsBase.DEFAULT_COMPONENT_PACKAGE_NAME will make // PlatformCompat#isChangeEnabledByPackageName always return default value. final ComponentName componentName = ComponentName.createRelative( mContext, SizeCompatTests.class.getName()); - mTask = new TaskBuilder(mSupervisor).setDisplay(display).setComponent(componentName) + final TaskBuilder taskBuilder = tBuilder != null ? tBuilder : new TaskBuilder(mSupervisor); + mTask = taskBuilder.setDisplay(display).setComponent(componentName) .build(); - final ActivityBuilder builder = appBuilder != null ? appBuilder : new ActivityBuilder(mAtm); - mActivity = builder.setTask(mTask).setComponent(componentName).build(); + final ActivityBuilder appBuilder = aBuilder != null ? aBuilder : new ActivityBuilder(mAtm); + mActivity = appBuilder.setTask(mTask).setComponent(componentName).build(); doReturn(false).when(mActivity).isImmersiveMode(any()); return mActivity; } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh) { - return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */); + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, null /* taskBuilder */); } private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder) { + return setUpDisplaySizeWithApp(dw, dh, appBuilder, null /* taskBuilder */); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, TaskBuilder taskBuilder) { + return setUpDisplaySizeWithApp(dw, dh, null /* appBuilder */, taskBuilder); + } + + private ActivityRecord setUpDisplaySizeWithApp(int dw, int dh, ActivityBuilder appBuilder, + TaskBuilder taskBuilder) { final TestDisplayContent.Builder builder = new TestDisplayContent.Builder(mAtm, dw, dh); - return setUpApp(builder.build(), appBuilder); + return setUpApp(builder.build(), appBuilder, taskBuilder); } private void setUpLargeScreenDisplayWithApp(int dw, int dh) { @@ -330,7 +345,7 @@ public class SizeCompatTests extends WindowTestsBase { if (horizontalReachability) { final Consumer<Integer> doubleClick = (Integer x) -> { - mActivity.mAppCompatController.getAppCompatReachabilityPolicy() + mActivity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(x, displayHeight / 2); mActivity.mRootWindowContainer.performSurfacePlacement(); }; @@ -360,7 +375,7 @@ public class SizeCompatTests extends WindowTestsBase { } else { final Consumer<Integer> doubleClick = (Integer y) -> { - mActivity.mAppCompatController.getAppCompatReachabilityPolicy() + mActivity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(displayWidth / 2, y); mActivity.mRootWindowContainer.performSurfacePlacement(); }; @@ -421,7 +436,7 @@ public class SizeCompatTests extends WindowTestsBase { final Consumer<Integer> doubleClick = (Integer y) -> { - activity.mAppCompatController.getAppCompatReachabilityPolicy() + activity.mAppCompatController.getReachabilityPolicy() .handleDoubleTap(dw / 2, y); activity.mRootWindowContainer.performSurfacePlacement(); }; @@ -658,7 +673,7 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.mDisplayContent.shouldImeAttachedToApp()); // Recompute the natural configuration without resolving size compat configuration. - mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); mActivity.onConfigurationChanged(mTask.getConfiguration()); // It should keep non-attachable because the resolved bounds will be computed according to // the aspect ratio that won't match its parent bounds. @@ -750,7 +765,7 @@ public class SizeCompatTests extends WindowTestsBase { / originalBounds.width())); // Recompute the natural configuration in the new display. - mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); mActivity.ensureActivityConfiguration(); // Because the display cannot rotate, the portrait activity will fit the short side of // display with keeping portrait bounds [200, 0 - 700, 1000] in center. @@ -834,7 +849,7 @@ public class SizeCompatTests extends WindowTestsBase { // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); assertTrue(mActivity.isRelaunching()); - assertTrue(mActivity.mAppCompatController.getAppCompatOrientationOverrides() + assertTrue(mActivity.mAppCompatController.getOrientationOverrides() .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); @@ -1172,7 +1187,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(activity, /* maxAspect=*/ 1.5f, SCREEN_ORIENTATION_LANDSCAPE); // Activity max bounds should not be sandboxed, even though it is letterboxed. - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(activity.getConfiguration().windowConfiguration.getMaxBounds()) .isEqualTo(activity.getDisplayArea().getBounds()); @@ -1214,7 +1229,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(activity, /* maxAspect=*/ 1.5f, SCREEN_ORIENTATION_LANDSCAPE); // Activity max bounds should not be sandboxed, even though it is letterboxed. - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(activity.getConfiguration().windowConfiguration.getMaxBounds()) .isEqualTo(activity.getDisplayArea().getBounds()); @@ -1238,7 +1253,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(activity, /* maxAspect=*/ 1.5f, SCREEN_ORIENTATION_LANDSCAPE); // Activity max bounds should not be sandboxed, even though it is letterboxed. - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(activity.getConfiguration().windowConfiguration.getMaxBounds()) .isEqualTo(activity.getDisplayArea().getBounds()); @@ -1492,7 +1507,7 @@ public class SizeCompatTests extends WindowTestsBase { // After changing the orientation to portrait the override should be applied. activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); // The per-package override forces the activity into a 3:2 aspect ratio assertEquals(1200, activity.getBounds().height()); @@ -1516,7 +1531,7 @@ public class SizeCompatTests extends WindowTestsBase { // After changing the orientation to portrait the override should be applied. activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); // The per-package override forces the activity into a 3:2 aspect ratio assertEquals(1200, activity.getBounds().height()); @@ -1539,7 +1554,7 @@ public class SizeCompatTests extends WindowTestsBase { // After changing the orientation to landscape the override shouldn't be applied. activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); // The per-package override should have no effect assertEquals(1200, activity.getBounds().height()); @@ -1739,7 +1754,7 @@ public class SizeCompatTests extends WindowTestsBase { addWindowToActivity(mActivity); // App should launch in fullscreen. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -1754,7 +1769,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(rotatedDisplayBounds.width() < rotatedDisplayBounds.height()); // App should be in size compat. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertDownScaled(); assertThat(mActivity.inSizeCompatMode()).isTrue(); @@ -1867,7 +1882,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -1898,7 +1913,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -1928,7 +1943,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -1958,7 +1973,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -2055,7 +2070,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() > displayBounds.height()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -2079,7 +2094,7 @@ public class SizeCompatTests extends WindowTestsBase { // App should launch in fixed orientation letterbox. // Activity bounds should be 700x1400 with the ratio as the display. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFitted(); assertEquals(originalScreenWidthDp, mActivity.getConfiguration().smallestScreenWidthDp); @@ -2119,7 +2134,7 @@ public class SizeCompatTests extends WindowTestsBase { // App should launch in fixed orientation letterbox. // Activity bounds should be 700x1400 with the ratio as the display. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFitted(); assertEquals(originalScreenWidthDp, mActivity.getConfiguration().smallestScreenWidthDp); @@ -2154,7 +2169,7 @@ public class SizeCompatTests extends WindowTestsBase { final Rect activityBounds = new Rect(mActivity.getBounds()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2688,7 +2703,7 @@ public class SizeCompatTests extends WindowTestsBase { final Rect activityBounds = new Rect(mActivity.getBounds()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2732,7 +2747,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2767,7 +2782,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2793,7 +2808,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2819,7 +2834,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); // App should launch in fixed orientation letterbox. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); // Checking that there is no size compat mode. assertFitted(); @@ -2848,7 +2863,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(displayBounds.width() < displayBounds.height()); // App should be in size compat. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(mActivity.inSizeCompatMode()).isTrue(); assertEquals(activityBounds.width(), newActivityBounds.width()); @@ -2865,7 +2880,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); // App should launch in fullscreen. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); // Activity inherits max bounds from TaskDisplayArea. @@ -2879,7 +2894,7 @@ public class SizeCompatTests extends WindowTestsBase { assertTrue(rotatedDisplayBounds.width() > rotatedDisplayBounds.height()); // App should be in size compat. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertDownScaled(); assertThat(mActivity.inSizeCompatMode()).isTrue(); @@ -2900,7 +2915,7 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -2923,7 +2938,7 @@ public class SizeCompatTests extends WindowTestsBase { // Task and display bounds should be equal while activity should be letterboxed and // has 700x1400 bounds with the ratio as the display. - assertTrue(newActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(newActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(newActivity.inSizeCompatMode()); // Activity max bounds are sandboxed due to size compat mode. @@ -2944,7 +2959,7 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -2965,7 +2980,7 @@ public class SizeCompatTests extends WindowTestsBase { // Portrait fixed app without max aspect. prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -2996,7 +3011,7 @@ public class SizeCompatTests extends WindowTestsBase { assertActivityMaxBoundsSandboxed(); // Activity bounds should be (1400 / 1.3 = 1076)x1400 with the app requested ratio. - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(newActivity.inSizeCompatMode()); assertEquals(displayBounds.height(), newActivityBounds.height()); @@ -3015,7 +3030,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); clearInvocations(mActivity); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); @@ -3023,7 +3038,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(mActivity.mDisplayContent, ROTATION_90); // App should be in size compat. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(mActivity.inSizeCompatMode()).isTrue(); // Activity max bounds are sandboxed due to size compat mode. @@ -3035,10 +3050,10 @@ public class SizeCompatTests extends WindowTestsBase { // App still in size compat, and the bounds don't change. final AppCompatSizeCompatModePolicy scmPolicy = mActivity.mAppCompatController - .getAppCompatSizeCompatModePolicy(); + .getSizeCompatModePolicy(); spyOn(scmPolicy); verify(scmPolicy, never()).clearSizeCompatMode(); - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(mActivity.inSizeCompatMode()).isTrue(); assertEquals(activityBounds, mActivity.getBounds()); @@ -3056,7 +3071,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_PORTRAIT); // In fixed orientation letterbox - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -3065,7 +3080,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(display, ROTATION_90); // App should be in size compat. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertThat(mActivity.inSizeCompatMode()).isTrue(); assertActivityMaxBoundsSandboxed(); @@ -3074,7 +3089,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(display, ROTATION_180); // In activity letterbox - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -3093,7 +3108,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, 0, SCREEN_ORIENTATION_LANDSCAPE); // In fixed orientation letterbox - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -3102,7 +3117,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(display, ROTATION_90); // App should be in size compat. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertDownScaled(); assertActivityMaxBoundsSandboxed(); @@ -3111,7 +3126,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(display, ROTATION_180); // In fixed orientation letterbox - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertActivityMaxBoundsSandboxed(); @@ -3310,7 +3325,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(ORIENTATION_PORTRAIT, mTask.getConfiguration().orientation); assertEquals(ORIENTATION_LANDSCAPE, mActivity.getConfiguration().orientation); assertFitted(); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertActivityMaxBoundsSandboxed(); @@ -3337,7 +3352,7 @@ public class SizeCompatTests extends WindowTestsBase { // Resizable activity is not in size compat mode but in the letterbox for fixed orientation. assertFitted(); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -3374,7 +3389,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(ORIENTATION_PORTRAIT, mTask.getConfiguration().orientation); assertEquals(ORIENTATION_PORTRAIT, mActivity.getConfiguration().orientation); assertFitted(); - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertActivityMaxBoundsSandboxed(); @@ -3427,7 +3442,7 @@ public class SizeCompatTests extends WindowTestsBase { setUpAllowThinLetterboxed(/* thinLetterboxAllowed */ false); final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivity.mAppCompatController.getReachabilityOverrides(); assertFalse(reachabilityOverrides.isVerticalReachabilityEnabled()); assertFalse(reachabilityOverrides.isHorizontalReachabilityEnabled()); } @@ -3451,7 +3466,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Horizontal reachability is disabled because the app is in split screen. - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3475,7 +3490,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode()); // Vertical reachability is disabled because the app is in split screen. - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3498,7 +3513,7 @@ public class SizeCompatTests extends WindowTestsBase { // Vertical reachability is disabled because the app does not match parent width assertNotEquals(mActivity.getScreenResolvedBounds().width(), mActivity.mDisplayContent.getBounds().width()); - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3516,7 +3531,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds()); // Vertical reachability is still enabled as resolved bounds is not empty - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3533,7 +3548,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 0, 0), mActivity.getBounds()); // Horizontal reachability is still enabled as resolved bounds is not empty - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3548,7 +3563,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3563,7 +3578,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE, SCREEN_ORIENTATION_LANDSCAPE); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3585,7 +3600,7 @@ public class SizeCompatTests extends WindowTestsBase { // Horizontal reachability is disabled because the app does not match parent height assertNotEquals(mActivity.getScreenResolvedBounds().height(), mActivity.mDisplayContent.getBounds().height()); - assertFalse(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertFalse(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3608,7 +3623,7 @@ public class SizeCompatTests extends WindowTestsBase { // Horizontal reachability is enabled because the app matches parent height assertEquals(mActivity.getScreenResolvedBounds().height(), mActivity.mDisplayContent.getBounds().height()); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isHorizontalReachabilityEnabled()); } @@ -3631,7 +3646,7 @@ public class SizeCompatTests extends WindowTestsBase { // Vertical reachability is enabled because the app matches parent width assertEquals(mActivity.getScreenResolvedBounds().width(), mActivity.mDisplayContent.getBounds().width()); - assertTrue(mActivity.mAppCompatController.getAppCompatReachabilityOverrides() + assertTrue(mActivity.mAppCompatController.getReachabilityOverrides() .isVerticalReachabilityEnabled()); } @@ -3874,7 +3889,7 @@ public class SizeCompatTests extends WindowTestsBase { private void recomputeNaturalConfigurationOfUnresizableActivity() { // Recompute the natural configuration of the non-resizable activity and the split screen. - mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); // Draw letterbox. mActivity.setVisible(false); @@ -4016,7 +4031,7 @@ public class SizeCompatTests extends WindowTestsBase { // orientation is not respected with insets as insets have been decoupled. final Rect appBounds = activity.getWindowConfiguration().getAppBounds(); final Rect displayBounds = display.getBounds(); - assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertNotNull(appBounds); assertEquals(displayBounds.width(), appBounds.width()); @@ -4048,7 +4063,7 @@ public class SizeCompatTests extends WindowTestsBase { final Rect bounds = activity.getBounds(); // Activity should be letterboxed and should have portrait app bounds - assertTrue(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertTrue(bounds.height() > bounds.width()); } @@ -4083,7 +4098,7 @@ public class SizeCompatTests extends WindowTestsBase { assertNotNull(activity.getAppCompatDisplayInsets()); // Activity is not letterboxed for fixed orientation because orientation is respected // with insets, and should not be in size compat mode - assertFalse(activity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(activity.inSizeCompatMode()); } @@ -4315,7 +4330,7 @@ public class SizeCompatTests extends WindowTestsBase { // Make sure app doesn't jump to top (default tabletop position) when unfolding. assertEquals(1.0f, mActivity.mAppCompatController - .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity + .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity .getParent().getConfiguration()), 0); // Simulate display fully open after unfolding. @@ -4323,7 +4338,7 @@ public class SizeCompatTests extends WindowTestsBase { doReturn(false).when(mActivity.mDisplayContent).inTransition(); assertEquals(1.0f, mActivity.mAppCompatController - .getAppCompatReachabilityOverrides().getVerticalPositionMultiplier(mActivity + .getReachabilityOverrides().getVerticalPositionMultiplier(mActivity .getParent().getConfiguration()), 0); } @@ -4469,6 +4484,78 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(0, 0, 1000, 2800), bounds); } + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + assertFalse(appCompatController.getAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testUserAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + spyOn(mActivity.mWmService.mAppCompatConfiguration); + doReturn(true).when(mActivity.mWmService.mAppCompatConfiguration) + .isUserAppAspectRatioSettingsEnabled(); + final AppCompatController appCompatController = mActivity.mAppCompatController; + final AppCompatAspectRatioOverrides aspectRatioOverrides = + appCompatController.getAppCompatAspectRatioOverrides(); + spyOn(aspectRatioOverrides); + // Set user aspect ratio override. + doReturn(USER_MIN_ASPECT_RATIO_16_9).when(aspectRatioOverrides) + .getUserMinAspectRatioOverrideCode(); + + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + assertTrue(appCompatController.getAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ false); + + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy().isAspectRatioApplied()); + } + + @Test + @EnableCompatChanges({ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO, + ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE}) + @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) + public void testSystemAspectRatioOverridesAppliedToNonResizeableFreeformActivity() { + final TaskBuilder taskBuilder = + new TaskBuilder(mSupervisor).setWindowingMode(WINDOWING_MODE_FREEFORM); + setUpDisplaySizeWithApp(2500, 1600, taskBuilder); + prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_PORTRAIT, /* isUnresizable= */ true); + + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy().isAspectRatioApplied()); + } + private void assertVerticalPositionForDifferentDisplayConfigsForLandscapeActivity( float letterboxVerticalPositionMultiplier, Rect fixedOrientationLetterbox, Rect sizeCompatUnscaled, Rect sizeCompatScaled) { @@ -4515,7 +4602,7 @@ public class SizeCompatTests extends WindowTestsBase { verifyLogAppCompatState(mActivity, APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED); prepareUnresizable(mActivity, /* maxAspect= */ 2, SCREEN_ORIENTATION_PORTRAIT); - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); @@ -4532,7 +4619,7 @@ public class SizeCompatTests extends WindowTestsBase { // ActivityRecord#resolveSizeCompatModeConfiguration because mCompatDisplayInsets aren't // null but activity doesn't enter size compat mode. Checking that areBoundsLetterboxed() // still returns true because of the aspect ratio restrictions. - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); @@ -4560,7 +4647,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertFalse(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); @@ -4579,7 +4666,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(mActivity.mDisplayContent, ROTATION_90); - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertTrue(mActivity.inSizeCompatMode()); assertTrue(mActivity.areBoundsLetterboxed()); @@ -4641,7 +4728,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE); assertFalse(mActivity.isEligibleForLetterboxEducation()); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -4700,7 +4787,7 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); assertTrue(mActivity.isEligibleForLetterboxEducation()); - assertTrue(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertTrue(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); } @@ -4715,7 +4802,7 @@ public class SizeCompatTests extends WindowTestsBase { rotateDisplay(mActivity.mDisplayContent, ROTATION_90); assertTrue(mActivity.isEligibleForLetterboxEducation()); - assertFalse(mActivity.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(mActivity.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertTrue(mActivity.inSizeCompatMode()); } @@ -4918,7 +5005,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mActivity.getOverrideOrientation()); assertEquals(mActivity.getTask().getBounds(), mActivity.getBounds()); final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivity.mAppCompatController - .getAppCompatAspectRatioPolicy(); + .getAspectRatioPolicy(); assertEquals(0, aspectRatioPolicy.getMaxAspectRatio(), 0 /* delta */); assertEquals(0, aspectRatioPolicy.getMinAspectRatio(), 0 /* delta */); @@ -5001,7 +5088,7 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(origDensity, mActivity.getConfiguration().densityDpi); // Activity should exit size compat with new density. - mActivity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); assertFitted(); assertEquals(newDensity, mActivity.getConfiguration().densityDpi); @@ -5028,7 +5115,7 @@ public class SizeCompatTests extends WindowTestsBase { private void setUpAllowThinLetterboxed(boolean thinLetterboxAllowed) { final AppCompatReachabilityOverrides reachabilityOverrides = - mActivity.mAppCompatController.getAppCompatReachabilityOverrides(); + mActivity.mAppCompatController.getReachabilityOverrides(); spyOn(reachabilityOverrides); doReturn(thinLetterboxAllowed).when(reachabilityOverrides) .allowVerticalReachabilityForThinLetterbox(); @@ -5185,7 +5272,7 @@ public class SizeCompatTests extends WindowTestsBase { activity.setRequestedOrientation(screenOrientation); } // Make sure to use the provided configuration to construct the size compat fields. - activity.mAppCompatController.getAppCompatSizeCompatModePolicy().clearSizeCompatMode(); + activity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode(); activity.ensureActivityConfiguration(); // Make sure the display configuration reflects the change of activity. if (activity.mDisplayContent.updateOrientation()) { 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 a12831e1ccf1..a95093d7e113 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.app.ActivityThread; import android.app.AppOpsManager; @@ -67,7 +68,6 @@ import android.os.Looper; import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; -import android.os.StrictMode; import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; @@ -75,7 +75,6 @@ import android.util.Log; import android.view.DisplayInfo; import android.view.InputChannel; import android.view.SurfaceControl; -import android.window.ConfigurationChangeSetting; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.BackgroundThread; @@ -96,11 +95,9 @@ import com.android.server.input.InputManagerService; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.policy.PermissionPolicyInternal; -import com.android.server.policy.WindowManagerPolicy; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.testutils.StubTransaction; import com.android.server.uri.UriGrantsManagerInternal; -import com.android.window.flags.Flags; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -144,13 +141,15 @@ public class SystemServicesTestRule implements TestRule { private ActivityTaskManagerService mAtmService; private WindowManagerService mWmService; private InputManagerService mImService; - private Runnable mOnBeforeServicesCreated; + @Nullable + private final Runnable mOnBeforeServicesCreated; + /** * Spied {@link SurfaceControl.Transaction} class than can be used to verify calls. */ SurfaceControl.Transaction mTransaction; - public SystemServicesTestRule(Runnable onBeforeServicesCreated) { + public SystemServicesTestRule(@Nullable Runnable onBeforeServicesCreated) { mOnBeforeServicesCreated = onBeforeServicesCreated; } @@ -398,15 +397,13 @@ public class SystemServicesTestRule implements TestRule { } private void setUpWindowManagerService() { - TestWindowManagerPolicy wmPolicy = new TestWindowManagerPolicy(); - TestDisplayWindowSettingsProvider testDisplayWindowSettingsProvider = - new TestDisplayWindowSettingsProvider(); - // Suppress StrictMode violation (DisplayWindowSettings) to avoid log flood. - DisplayThread.getHandler().post(StrictMode::allowThreadDiskWritesMask); - mWmService = WindowManagerService.main( - mContext, mImService, false, wmPolicy, mAtmService, - testDisplayWindowSettingsProvider, StubTransaction::new, - MockSurfaceControlBuilder::new, mAppCompat); + // Use a spied Transaction class to prevent native code calls and verify interactions. + mTransaction = spy(StubTransaction.class); + + mWmService = WindowManagerServiceTestSupport.setUpService(mContext, mImService, + new TestWindowManagerPolicy(), mAtmService, new TestDisplayWindowSettingsProvider(), + mTransaction, new MockSurfaceControlBuilder(), mAppCompat); + spyOn(mWmService); spyOn(mWmService.mRoot); // Invoked during {@link ActivityStack} creation. @@ -418,10 +415,6 @@ public class SystemServicesTestRule implements TestRule { spyOn(mWmService.mDisplayWindowSettings); spyOn(mWmService.mDisplayWindowSettingsProvider); - // Setup factory classes to prevent calls to native code. - mTransaction = spy(StubTransaction.class); - // Return a spied Transaction class than can be used to verify calls. - mWmService.mTransactionFactory = () -> mTransaction; mWmService.mSurfaceAnimationRunner = new SurfaceAnimationRunner( null, null, mTransaction, mWmService.mPowerManagerInternal); @@ -488,12 +481,12 @@ public class SystemServicesTestRule implements TestRule { } private static void tearDownLocalServices() { + WindowManagerServiceTestSupport.tearDownService(); + LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.removeServiceForTest(PowerManagerInternal.class); LocalServices.removeServiceForTest(ActivityManagerInternal.class); LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); - LocalServices.removeServiceForTest(WindowManagerInternal.class); - LocalServices.removeServiceForTest(WindowManagerPolicy.class); LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.removeServiceForTest(UriGrantsManagerInternal.class); LocalServices.removeServiceForTest(PermissionPolicyInternal.class); @@ -501,12 +494,7 @@ public class SystemServicesTestRule implements TestRule { LocalServices.removeServiceForTest(UsageStatsManagerInternal.class); LocalServices.removeServiceForTest(StatusBarManagerInternal.class); LocalServices.removeServiceForTest(UserManagerInternal.class); - LocalServices.removeServiceForTest(ImeTargetVisibilityPolicy.class); LocalServices.removeServiceForTest(GrammaticalInflectionManagerInternal.class); - if (Flags.condenseConfigurationChangeForSimpleMode()) { - LocalServices.removeServiceForTest( - ConfigurationChangeSetting.ConfigurationChangeSettingInternal.class); - } } Description getDescription() { 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 299717393028..0d9772492e59 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -759,13 +759,13 @@ public class TaskFragmentTest extends WindowTestsBase { // Assert fixed orientation request is ignored for activity in ActivityEmbedding split. activity0.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); - assertFalse(activity0.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity0.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); activity1.setRequestedOrientation(SCREEN_ORIENTATION_PORTRAIT); - assertFalse(activity1.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity1.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); @@ -773,9 +773,9 @@ public class TaskFragmentTest extends WindowTestsBase { mDisplayContent.setIgnoreOrientationRequest(true); task.onConfigurationChanged(task.getParent().getConfiguration()); - assertFalse(activity0.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity0.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); - assertFalse(activity1.mAppCompatController.getAppCompatAspectRatioPolicy() + assertFalse(activity1.mAppCompatController.getAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio()); assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java index f1180ff93edb..9cd302e71d3b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java @@ -354,7 +354,7 @@ public class TransparentPolicyTest extends WindowTestsBase { ta.launchTransparentActivityInTask(); a.assertNotNullOnTopActivity(ActivityRecord::getAppCompatDisplayInsets); a.applyToTopActivity((top) -> { - top.mAppCompatController.getAppCompatSizeCompatModePolicy() + top.mAppCompatController.getSizeCompatModePolicy() .clearSizeCompatMode(); }); a.assertNullOnTopActivity(ActivityRecord::getAppCompatDisplayInsets); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java index 5ed2df30518b..cc447a18758c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java @@ -1269,6 +1269,7 @@ public class WindowContainerTests extends WindowTestsBase { final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); spyOn(container); spyOn(surfaceAnimator); + doReturn(t).when(container).getSyncTransaction(); // Trigger for first relative layer call. container.assignRelativeLayer(t, relativeParent, 1 /* layer */); @@ -1295,6 +1296,7 @@ public class WindowContainerTests extends WindowTestsBase { spyOn(container); spyOn(surfaceAnimator); spyOn(surfaceFreezer); + doReturn(t).when(container).getSyncTransaction(); container.setLayer(t, 1); container.setRelativeLayer(t, relativeParent, 2); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTestSupport.kt b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTestSupport.kt new file mode 100644 index 000000000000..a165d20eb5c1 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTestSupport.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm + +import android.content.Context +import android.os.StrictMode +import android.view.SurfaceControl +import android.window.ConfigurationChangeSetting +import com.android.server.DisplayThread +import com.android.server.LocalServices +import com.android.server.input.InputManagerService +import com.android.server.policy.WindowManagerPolicy +import com.android.window.flags.Flags + +/** + * Provides support for tests that require a [WindowManagerService]. + * + * It provides functionalities for setting up and tearing down the service with proper dependencies, + * which can be used across different test modules. + */ +object WindowManagerServiceTestSupport { + + /** + * Sets up and initializes a [WindowManagerService] instance with the provided dependencies. + * + * This method constructs a [WindowManagerService] using the provided dependencies for testing. + * It's marked as `internal` due to the package-private classes [DisplayWindowSettingsProvider] + * and [AppCompatConfiguration]. The `@JvmName` annotation is used to bypass name mangling and + * allow access from Java via `WindowManagerServiceTestSupport.setUpService`. + * + * **Important:** Before calling this method, ensure that any previous [WindowManagerService] + * instance and its related services are properly torn down. In your test's setup, it is + * recommended to call [tearDownService] before calling [setUpService] to handle cases where a + * previous test might have crashed and left services in an inconsistent state. This is crucial + * for test reliability. + * + * Example usage in a test's `setUp()` method: + * ``` + * @Before + * fun setUp() { + * WindowManagerServiceTestSupport.tearDownService() // Clean up before setup. + * mWindowManagerService = WindowManagerServiceTestSupport.setUpService(...) + * // ... rest of your setup logic ... + * } + * ``` + * + * @param context the [Context] for the service. + * @param im the [InputManagerService] to use. + * @param policy the [WindowManagerPolicy] to use. + * @param atm the [ActivityTaskManagerService] to use. + * @param displayWindowSettingsProvider the [DisplayWindowSettingsProvider] to use. + * @param surfaceControlTransaction the [SurfaceControl.Transaction] instance to use. + * @param surfaceControlBuilder the [SurfaceControl.Builder] instance to use. + * @param appCompat the [AppCompatConfiguration] to use. + * @return the created [WindowManagerService] instance. + */ + @JvmStatic + @JvmName("setUpService") + internal fun setUpService( + context: Context, + im: InputManagerService, + policy: WindowManagerPolicy, + atm: ActivityTaskManagerService, + displayWindowSettingsProvider: DisplayWindowSettingsProvider, + surfaceControlTransaction: SurfaceControl.Transaction, + surfaceControlBuilder: SurfaceControl.Builder, + appCompat: AppCompatConfiguration, + ): WindowManagerService { + // Suppress StrictMode violation (DisplayWindowSettings) to avoid log flood. + DisplayThread.getHandler().post { StrictMode.allowThreadDiskWritesMask() } + + return WindowManagerService.main( + context, + im, + false, /* showBootMsgs */ + policy, + atm, + displayWindowSettingsProvider, + { surfaceControlTransaction }, + { surfaceControlBuilder }, + appCompat, + ) + } + + /** Tears down the [WindowManagerService] and removes related local services. */ + @JvmStatic + fun tearDownService() { + LocalServices.removeServiceForTest(WindowManagerPolicy::class.java) + LocalServices.removeServiceForTest(WindowManagerInternal::class.java) + LocalServices.removeServiceForTest(ImeTargetVisibilityPolicy::class.java) + + if (Flags.condenseConfigurationChangeForSimpleMode()) { + LocalServices.removeServiceForTest( + ConfigurationChangeSetting.ConfigurationChangeSettingInternal::class.java, + ) + } + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index d1f5d157560b..be79160c3a09 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -76,6 +76,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.ActivityThread; import android.app.IApplicationThread; import android.content.pm.ActivityInfo; @@ -1522,7 +1523,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { @EnableFlags(Flags.FLAG_CONDENSE_CONFIGURATION_CHANGE_FOR_SIMPLE_MODE) public void setConfigurationChangeSettingsForUser_createsFromParcel_callsSettingImpl() throws Settings.SettingNotFoundException { - final int userId = 0; + final int currentUserId = ActivityManager.getCurrentUser(); final int forcedDensity = 400; final float forcedFontScaleFactor = 1.15f; final Parcelable.Creator<ConfigurationChangeSetting> creator = @@ -1536,10 +1537,10 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.setConfigurationChangeSettingsForUser(settings, UserHandle.USER_CURRENT); - verify(mDisplayContent).setForcedDensity(forcedDensity, userId); + verify(mDisplayContent).setForcedDensity(forcedDensity, currentUserId); assertEquals(forcedFontScaleFactor, Settings.System.getFloat( mContext.getContentResolver(), Settings.System.FONT_SCALE), 0.1f /* delta */); - verify(mAtm).updateFontScaleIfNeeded(userId); + verify(mAtm).updateFontScaleIfNeeded(currentUserId); } @Test diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 7082f0028a5e..e65e4b05ef98 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -29,6 +29,7 @@ import android.annotation.SuppressAutoDoc; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.annotation.TestApi; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -1886,6 +1887,34 @@ public class TelecomManager { } /** + * This test API determines the foreground service delegation state for a VoIP app that adds + * calls via {@link TelecomManager#addCall(CallAttributes, Executor, OutcomeReceiver, + * CallControlCallback, CallEventCallback)}. Foreground Service Delegation allows applications + * to operate in the background starting in Android 14 and is granted by Telecom via a request + * to the ActivityManager. + * + * @param handle of the voip app that is being checked + * @return true if the app has foreground service delegation. Otherwise, false. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_VOIP_CALL_MONITOR_REFACTOR) + @TestApi + public boolean hasForegroundServiceDelegation(@Nullable PhoneAccountHandle handle) { + ITelecomService service = getTelecomService(); + if (service != null) { + try { + return service.hasForegroundServiceDelegation(handle, mContext.getOpPackageName()); + } catch (RemoteException e) { + Log.e(TAG, + "RemoteException calling ITelecomService#hasForegroundServiceDelegation.", + e); + } + } + return false; + } + + /** * Return the line 1 phone number for given phone account. * * <p>Requires Permission: diff --git a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl index c85374e0b660..b32379ae4b1e 100644 --- a/telecomm/java/com/android/internal/telecom/ITelecomService.aidl +++ b/telecomm/java/com/android/internal/telecom/ITelecomService.aidl @@ -409,4 +409,10 @@ interface ITelecomService { */ void addCall(in CallAttributes callAttributes, in ICallEventCallback callback, String callId, String callingPackage); + + /** + * @see TelecomServiceImpl#hasForegroundServiceDelegation + */ + boolean hasForegroundServiceDelegation(in PhoneAccountHandle phoneAccountHandle, + String callingPackage); } diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 63a12816f783..b7b209b78300 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -3690,8 +3690,8 @@ public final class SatelliteManager { * @param list The list of provisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. - * If the request is successful, {@link OutcomeReceiver#onResult(Object)} - * will return {@code true}. + * If the request is successful, {@link OutcomeReceiver#onResult} + * will be called. * If the request is not successful, * {@link OutcomeReceiver#onError(Throwable)} will return an error with * a SatelliteException. @@ -3704,7 +3704,7 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + @NonNull OutcomeReceiver<Void, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -3718,8 +3718,8 @@ public final class SatelliteManager { if (resultData.containsKey(KEY_PROVISION_SATELLITE_TOKENS)) { boolean isUpdated = resultData.getBoolean(KEY_PROVISION_SATELLITE_TOKENS); - executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(isUpdated))); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> callback.onResult(null))); } else { loge("KEY_REQUEST_PROVISION_TOKENS does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> @@ -3751,8 +3751,8 @@ public final class SatelliteManager { * @param list The list of deprovisioned satellite subscriber infos. * @param executor The executor on which the callback will be called. * @param callback The callback object to which the result will be delivered. - * If the request is successful, {@link OutcomeReceiver#onResult(Object)} - * will return {@code true}. + * If the request is successful, {@link OutcomeReceiver#onResult} + * will be called. * If the request is not successful, * {@link OutcomeReceiver#onError(Throwable)} will return an error with * a SatelliteException. @@ -3765,7 +3765,7 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_SATELLITE_SYSTEM_APIS) public void deprovisionSatellite(@NonNull List<SatelliteSubscriberInfo> list, @NonNull @CallbackExecutor Executor executor, - @NonNull OutcomeReceiver<Boolean, SatelliteException> callback) { + @NonNull OutcomeReceiver<Void, SatelliteException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -3780,7 +3780,7 @@ public final class SatelliteManager { boolean isUpdated = resultData.getBoolean(KEY_DEPROVISION_SATELLITE_TOKENS); executor.execute(() -> Binder.withCleanCallingIdentity(() -> - callback.onResult(isUpdated))); + callback.onResult(null))); } else { loge("KEY_DEPROVISION_SATELLITE_TOKENS does not exist."); executor.execute(() -> Binder.withCleanCallingIdentity(() -> diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt index 08b5f38a4655..75bd5d157bb2 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -17,7 +17,6 @@ package com.android.server.wm.flicker.activityembedding.open import android.graphics.Rect -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest @@ -68,13 +67,21 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest } } - @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun navBarWindowIsVisibleAtStartAndEnd() {} - @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {} + @FlakyTest(bugId = 291575593) + @Test + override fun entireScreenCovered() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarWindowIsAlwaysVisible() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarLayerPositionAtStartAndEnd() {} /** Transition begins with a split. */ @FlakyTest(bugId = 286952194) @@ -122,7 +129,6 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest /** Always expand activity is on top of the split. */ @FlakyTest(bugId = 286952194) - @Presubmit @Test fun endsWithAlwaysExpandActivityOnTop() { flicker.assertWmEnd { diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt index 0ca8f37b239b..e41364595648 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -176,12 +176,15 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa } @Ignore("Not applicable to this CUJ.") + @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} @FlakyTest(bugId = 342596801) + @Test override fun entireScreenCovered() = super.entireScreenCovered() @FlakyTest(bugId = 342596801) + @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index b8f11dcf8970..ad083fa428a9 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -81,7 +80,6 @@ class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyF } @FlakyTest(bugId = 290767483) - @Postsubmit @Test fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace") diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 914adc40194d..8d380f0d72a6 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -32,7 +32,7 @@ android:process=":externalProcess"> </activity> - <activity android:name="com.android.test.input.CaptureEventActivity" + <activity android:name="com.android.cts.input.CaptureEventActivity" android:label="Capture events" android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize" android:enableOnBackInvokedCallback="false" diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index d35c9008e8cb..8c04f647fb2f 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -59,6 +59,7 @@ import junitparams.Parameters import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -1466,20 +1467,32 @@ class KeyGestureControllerTests { @Parameters(method = "customInputGesturesTestArguments") fun testCustomKeyGestures(test: TestData) { setupKeyGestureController() + val trigger = InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) val builder = InputGestureData.Builder() .setKeyGestureType(test.expectedKeyGestureType) - .setTrigger( - InputGestureData.createKeyTrigger( - test.expectedKeys[0], - test.expectedModifierState - ) - ) + .setTrigger(trigger) if (test.expectedAppLaunchData != null) { builder.setAppLaunchData(test.expectedAppLaunchData) } val inputGestureData = builder.build() - keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + assertNull( + test.toString(), + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) + assertEquals( + test.toString(), + InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, + keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + ) + assertEquals( + test.toString(), + inputGestureData.aidlData, + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) testKeyGestureInternal(test) } diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt deleted file mode 100644 index d54e3470d9c4..000000000000 --- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.test.input - -import android.app.Activity -import android.os.Bundle -import android.view.InputEvent -import android.view.KeyEvent -import android.view.MotionEvent -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import org.junit.Assert.assertNull - -class CaptureEventActivity : Activity() { - private val events = LinkedBlockingQueue<InputEvent>() - var shouldHandleKeyEvents = true - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - // Set the fixed orientation if requested - if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) { - val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0) - setRequestedOrientation(orientation) - } - - // Set the flag if requested - if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) { - val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0) - window.addFlags(flags) - } - } - - override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - events.add(KeyEvent(event)) - return shouldHandleKeyEvents - } - - override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { - events.add(MotionEvent.obtain(ev)) - return true - } - - fun getInputEvent(): InputEvent? { - return events.poll(5, TimeUnit.SECONDS) - } - - fun hasReceivedEvents(): Boolean { - return !events.isEmpty() - } - - fun assertNoEvents() { - val event = events.poll(100, TimeUnit.MILLISECONDS) - assertNull("Expected no events, but received $event", event) - } - - companion object { - const val EXTRA_FIXED_ORIENTATION = "fixed_orientation" - const val EXTRA_WINDOW_FLAGS = "window_flags" - } -} diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index 0b281d8d39e2..9e0f7347943d 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -28,6 +28,7 @@ import android.view.InputEvent import android.view.MotionEvent import androidx.test.platform.app.InstrumentationRegistry import com.android.cts.input.BatchedEventSplitter +import com.android.cts.input.CaptureEventActivity import com.android.cts.input.InputJsonParser import com.android.cts.input.VirtualDisplayActivityScenario import com.android.cts.input.inputeventmatchers.isResampled diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java index 060133df0a40..e7e3d10c958b 100644 --- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java +++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java @@ -81,7 +81,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -90,7 +91,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -99,7 +101,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -108,7 +110,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -133,7 +135,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -143,7 +146,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -152,7 +156,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -161,7 +166,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -171,7 +176,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -180,7 +185,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 20315561cceb..f00a6cad6b46 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -54,7 +54,9 @@ std::string GetSafePath(StringPiece arg) { void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::string* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -65,7 +67,9 @@ void Command::AddRequiredFlag(StringPiece name, StringPiece description, std::st void Command::AddRequiredFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -76,7 +80,9 @@ void Command::AddRequiredFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlag(StringPiece name, StringPiece description, std::optional<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + if (value) { + *value = (flags & Command::kPath) ? GetSafePath(arg) : std::string(arg); + } return true; }; @@ -87,7 +93,9 @@ void Command::AddOptionalFlag(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::vector<std::string>* value, uint32_t flags) { auto func = [value, flags](StringPiece arg, std::ostream*) -> bool { - value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + if (value) { + value->push_back((flags & Command::kPath) ? GetSafePath(arg) : std::string(arg)); + } return true; }; @@ -98,7 +106,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalFlagList(StringPiece name, StringPiece description, std::unordered_set<std::string>* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - value->emplace(arg); + if (value) { + value->emplace(arg); + } return true; }; @@ -108,7 +118,9 @@ void Command::AddOptionalFlagList(StringPiece name, StringPiece description, void Command::AddOptionalSwitch(StringPiece name, StringPiece description, bool* value) { auto func = [value](StringPiece arg, std::ostream* out_error) -> bool { - *value = true; + if (value) { + *value = true; + } return true; }; diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 2a3cb2a0c65d..ad167c979662 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -159,4 +159,22 @@ TEST(CommandTest, ShortOptions) { ASSERT_NE(0, command.Execute({"-w"s, "2"s}, &std::cerr)); } +TEST(CommandTest, OptionsWithNullptrToAcceptValues) { + TestCommand command; + command.AddRequiredFlag("--rflag", "", nullptr); + command.AddRequiredFlagList("--rlflag", "", nullptr); + command.AddOptionalFlag("--oflag", "", nullptr); + command.AddOptionalFlagList("--olflag", "", (std::vector<std::string>*)nullptr); + command.AddOptionalFlagList("--olflag2", "", (std::unordered_set<std::string>*)nullptr); + command.AddOptionalSwitch("--switch", "", nullptr); + + ASSERT_EQ(0, command.Execute({ + "--rflag"s, "1"s, + "--rlflag"s, "1"s, + "--oflag"s, "1"s, + "--olflag"s, "1"s, + "--olflag2"s, "1"s, + "--switch"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 6c3eae11eab9..060bc5fa2242 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,9 +425,6 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { << output_format_.value()); return 1; } - if (enable_sparse_encoding_) { - table_flattener_options_.sparse_entries = SparseEntriesMode::Enabled; - } if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 9452e588953e..98c8f5ff89c0 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -36,11 +36,9 @@ class ConvertCommand : public Command { kOutputFormatProto, kOutputFormatBinary, kOutputFormatBinary), &output_format_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &enable_sparse_encoding_); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" @@ -87,7 +85,6 @@ class ConvertCommand : public Command { std::string output_path_; std::optional<std::string> output_format_; bool verbose_ = false; - bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 232b4024abd2..eb71189ffc46 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -2504,9 +2504,6 @@ int LinkCommand::Action(const std::vector<std::string>& args) { << "the --merge-only flag can be only used when building a static library"); return 1; } - if (options_.use_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } // The default build type. context.SetPackageType(PackageType::kApp); diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 2f17853718ec..b5bd905c02be 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -75,7 +75,6 @@ struct LinkOptions { bool no_resource_removal = false; bool no_xml_namespaces = false; bool do_not_compress_anything = false; - bool use_sparse_encoding = false; std::unordered_set<std::string> extensions_to_not_compress; std::optional<std::regex> regex_to_not_compress; FeatureFlagValues feature_flag_values; @@ -163,9 +162,11 @@ class LinkCommand : public Command { AddOptionalSwitch("--no-resource-removal", "Disables automatic removal of resources without\n" "defaults. Use this only when building runtime resource overlay packages.", &options_.no_resource_removal); - AddOptionalSwitch("--enable-sparse-encoding", - "This decreases APK size at the cost of resource retrieval performance.", - &options_.use_sparse_encoding); + AddOptionalSwitch( + "--enable-sparse-encoding", + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--enable-compact-entries", "This decreases APK size by using compact resource entries for simple data types.", &options_.table_flattener_options.use_compact_entries); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 762441ee1872..f218307af578 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -406,9 +406,6 @@ int OptimizeCommand::Action(const std::vector<std::string>& args) { return 1; } - if (options_.enable_sparse_encoding) { - options_.table_flattener_options.sparse_entries = SparseEntriesMode::Enabled; - } if (options_.force_sparse_encoding) { options_.table_flattener_options.sparse_entries = SparseEntriesMode::Forced; } diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 012b0f230ca2..e3af584cbbd9 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -61,9 +61,6 @@ struct OptimizeOptions { // TODO(b/246489170): keep the old option and format until transform to the new one std::optional<std::string> shortened_paths_map_path; - // Whether sparse encoding should be used for O+ resources. - bool enable_sparse_encoding = false; - // Whether sparse encoding should be used for all resources. bool force_sparse_encoding = false; @@ -106,11 +103,9 @@ class OptimizeCommand : public Command { &kept_artifacts_); AddOptionalSwitch( "--enable-sparse-encoding", - "Enables encoding sparse entries using a binary search tree.\n" - "This decreases APK size at the cost of resource retrieval performance.\n" - "Only applies sparse encoding to Android O+ resources or all resources if minSdk of " - "the APK is O+", - &options_.enable_sparse_encoding); + "[DEPRECATED] This flag is a no-op as of aapt2 v2.20. Sparse encoding is always\n" + "enabled if minSdk of the APK is >= 32.", + nullptr); AddOptionalSwitch("--force-sparse-encoding", "Enables encoding sparse entries using a binary search tree.\n" "This decreases APK size at the cost of resource retrieval performance.\n" diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 1a82021bce71..b8ac7925d44e 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -201,7 +201,7 @@ class PackageFlattener { (context_->GetMinSdkVersion() == 0 && config.sdkVersion == 0)) { // Sparse encode if forced or sdk version is not set in context and config. } else { - // Otherwise, only sparse encode if the entries will be read on platforms S_V2+. + // Otherwise, only sparse encode if the entries will be read on platforms S_V2+ (32). sparse_encode = sparse_encode && (context_->GetMinSdkVersion() >= SDK_S_V2); } diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 0633bc81cb25..f1c4c3512ed3 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -37,8 +37,7 @@ constexpr const size_t kSparseEncodingThreshold = 60; enum class SparseEntriesMode { // Disables sparse encoding for entries. Disabled, - // Enables sparse encoding for all entries for APKs with O+ minSdk. For APKs with minSdk less - // than O only applies sparse encoding for resource configuration available on O+. + // Enables sparse encoding for all entries for APKs with minSdk >= 32 (S_V2). Enabled, // Enables sparse encoding for all entries regardless of minSdk. Forced, @@ -47,7 +46,7 @@ enum class SparseEntriesMode { struct TableFlattenerOptions { // When enabled, types for configurations with a sparse set of entries are encoded // as a sparse map of entry ID and offset to actual data. - SparseEntriesMode sparse_entries = SparseEntriesMode::Disabled; + SparseEntriesMode sparse_entries = SparseEntriesMode::Enabled; // When true, use compact entries for simple data bool use_compact_entries = false; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index 0f1168514c4a..e3d589eb078b 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -337,13 +337,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithMinSdkSV2) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); @@ -421,13 +421,13 @@ TEST_F(TableFlattenerTest, FlattenSparseEntryWithSdkVersionNotSet) { auto table_in = BuildTableWithSparseEntries(context.get(), sparse_config, 0.25f); TableFlattenerOptions options; - options.sparse_entries = SparseEntriesMode::Enabled; + options.sparse_entries = SparseEntriesMode::Disabled; std::string no_sparse_contents; - ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &no_sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &no_sparse_contents)); std::string sparse_contents; - ASSERT_TRUE(Flatten(context.get(), options, table_in.get(), &sparse_contents)); + ASSERT_TRUE(Flatten(context.get(), {}, table_in.get(), &sparse_contents)); EXPECT_GT(no_sparse_contents.size(), sparse_contents.size()); diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt index 6da6fc6f12c3..d0807f2ecd34 100644 --- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt +++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt @@ -877,7 +877,7 @@ resource_table { } tool_fingerprint { tool: "Android Asset Packaging Tool (aapt)" - version: "2.19-SOONG BUILD NUMBER PLACEHOLDER" + version: "2.20-SOONG BUILD NUMBER PLACEHOLDER" } } xml_files { diff --git a/tools/aapt2/readme.md b/tools/aapt2/readme.md index 8368f9d16af8..664d8412a3be 100644 --- a/tools/aapt2/readme.md +++ b/tools/aapt2/readme.md @@ -1,5 +1,11 @@ # Android Asset Packaging Tool 2.0 (AAPT2) release notes +## Version 2.20 +- Too many features, bug fixes, and improvements to list since the last minor version update in + 2017. This README will be updated more frequently in the future. +- Sparse encoding is now always enabled by default if the minSdkVersion is >= 32 (S_V2). The + `--enable-sparse-encoding` flag still exists, but is a no-op. + ## Version 2.19 - Added navigation resource type. - Fixed issue with resource deduplication. (bug 64397629) diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index 3d83caf29bba..6a4dfa629394 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -227,7 +227,7 @@ std::string GetToolFingerprint() { static const char* const sMajorVersion = "2"; // Update minor version whenever a feature or flag is added. - static const char* const sMinorVersion = "19"; + static const char* const sMinorVersion = "20"; // The build id of aapt2 binary. static const std::string sBuildId = [] { diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index ea660b013893..22d364ec3212 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -263,7 +263,7 @@ object SystemFeaturesGenerator { .returns(Boolean::class.java) .addParameter(CONTEXT_CLASS, "context") .addParameter(String::class.java, "featureName") - .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)") + .addStatement("return context.getPackageManager().hasSystemFeature(featureName)") .build() ) } diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen index ee97b26159de..730dacbbf995 100644 --- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen @@ -70,7 +70,7 @@ public final class RoFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen index 40c7db7ff1df..fe268c70708e 100644 --- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen @@ -25,7 +25,7 @@ public final class RoNoFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen index 7bf89614b92d..bcf978de3c1f 100644 --- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen @@ -55,7 +55,7 @@ public final class RwFeatures { } private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen index eb7ec63f1d7d..7bad5a2bae2a 100644 --- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen @@ -14,7 +14,7 @@ import android.util.ArrayMap; */ public final class RwNoFeatures { private static boolean hasFeatureFallback(Context context, String featureName) { - return context.getPackageManager().hasSystemFeature(featureName, 0); + return context.getPackageManager().hasSystemFeature(featureName); } /** diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java index ed3f5c94ba79..491b55e7992c 100644 --- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java @@ -76,28 +76,28 @@ public class SystemFeaturesGeneratorTest { // Also ensure we fall back to the PackageManager for feature APIs without an accompanying // versioned feature definition. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); } @Test public void testReadonlyDisabledWithDefinedFeatures() { // Always fall back to the PackageManager for defined, explicit features queries. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(true); assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)).thenReturn(false); assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true); assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN)).thenReturn(false); assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false); assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse(); // For defined and undefined features, conditional queries should report null (unknown). @@ -139,9 +139,9 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); // VERSION= - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(false); assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO)).thenReturn(true); assertThat(RoFeatures.hasFeatureAuto(mContext)).isTrue(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isNull(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); @@ -149,9 +149,9 @@ public class SystemFeaturesGeneratorTest { // For feature APIs without an associated feature definition, conditional queries should // report null, and explicit queries should report runtime-defined versions. - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(true); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(true); assertThat(RoFeatures.hasFeaturePc(mContext)).isTrue(); - when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC, 0)).thenReturn(false); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(false); assertThat(RoFeatures.hasFeaturePc(mContext)).isFalse(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, -1)).isNull(); assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_PC, 0)).isNull(); |