diff options
382 files changed, 11266 insertions, 3088 deletions
diff --git a/apex/jobscheduler/service/aconfig/alarm.aconfig b/apex/jobscheduler/service/aconfig/alarm.aconfig index d3068d7d37e8..a6e980726a9a 100644 --- a/apex/jobscheduler/service/aconfig/alarm.aconfig +++ b/apex/jobscheduler/service/aconfig/alarm.aconfig @@ -2,16 +2,6 @@ package: "com.android.server.alarm" container: "system" flag { - name: "use_frozen_state_to_drop_listener_alarms" - namespace: "backstage_power" - description: "Use frozen state callback to drop listener alarms for cached apps" - bug: "324470945" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "start_user_before_scheduled_alarms" namespace: "multiuser" description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due" diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 033da2df9bf6..60ba3b896a28 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -282,7 +282,6 @@ public class AlarmManagerService extends SystemService { private final Injector mInjector; int mBroadcastRefCount = 0; - boolean mUseFrozenStateToDropListenerAlarms; MetricsHelper mMetricsHelper; PowerManager.WakeLock mWakeLock; SparseIntArray mAlarmsPerUid = new SparseIntArray(); @@ -1784,40 +1783,37 @@ public class AlarmManagerService extends SystemService { mMetricsHelper = new MetricsHelper(getContext(), mLock); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); - mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms(); mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms() && UserManager.supportsMultipleUsers(); if (mStartUserBeforeScheduledAlarms) { mUserWakeupStore = new UserWakeupStore(); mUserWakeupStore.init(); } - if (mUseFrozenStateToDropListenerAlarms) { - final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> { - final int size = frozenStates.length; - if (uids.length != size) { - Slog.wtf(TAG, "Got different length arrays in frozen state callback!" - + " uids.length: " + uids.length + " frozenStates.length: " + size); - // Cannot process received data in any meaningful way. - return; - } - final IntArray affectedUids = new IntArray(); - for (int i = 0; i < size; i++) { - if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) { - continue; - } - if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, - uids[i])) { - continue; - } - affectedUids.add(uids[i]); + final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> { + final int size = frozenStates.length; + if (uids.length != size) { + Slog.wtf(TAG, "Got different length arrays in frozen state callback!" + + " uids.length: " + uids.length + " frozenStates.length: " + size); + // Cannot process received data in any meaningful way. + return; + } + final IntArray affectedUids = new IntArray(); + for (int i = 0; i < size; i++) { + if (frozenStates[i] != UID_FROZEN_STATE_FROZEN) { + continue; } - if (affectedUids.size() > 0) { - removeExactListenerAlarms(affectedUids.toArray()); + if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, + uids[i])) { + continue; } - }; - final ActivityManager am = getContext().getSystemService(ActivityManager.class); - am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback); - } + affectedUids.add(uids[i]); + } + if (affectedUids.size() > 0) { + removeExactListenerAlarms(affectedUids.toArray()); + } + }; + final ActivityManager am = getContext().getSystemService(ActivityManager.class); + am.registerUidFrozenStateChangedCallback(new HandlerExecutor(mHandler), callback); mListenerDeathRecipient = new IBinder.DeathRecipient() { @Override @@ -2994,13 +2990,10 @@ public class AlarmManagerService extends SystemService { pw.println("Feature Flags:"); pw.increaseIndent(); - pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS, - mUseFrozenStateToDropListenerAlarms); - pw.println(); pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS, Flags.startUserBeforeScheduledAlarms()); - pw.decreaseIndent(); pw.println(); + pw.decreaseIndent(); pw.println(); pw.println("App Standby Parole: " + mAppStandbyParole); @@ -5146,38 +5139,6 @@ public class AlarmManagerService extends SystemService { removeForStoppedLocked(uid); } } - - @Override - public void handleUidCachedChanged(int uid, boolean cached) { - if (mUseFrozenStateToDropListenerAlarms) { - // Use ActivityManager#UidFrozenStateChangedCallback instead. - return; - } - if (!CompatChanges.isChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, uid)) { - return; - } - // Apps can quickly get frozen after being cached, breaking the exactness guarantee on - // listener alarms. So going forward, the contract of exact listener alarms explicitly - // states that they will be removed as soon as the app goes out of lifecycle. We still - // allow a short grace period for quick shuffling of proc-states that may happen - // unexpectedly when switching between different lifecycles and is generally hard for - // apps to avoid. - - final long delay; - synchronized (mLock) { - delay = mConstants.CACHED_LISTENER_REMOVAL_DELAY; - } - final Integer uidObj = uid; - - if (cached && !mHandler.hasEqualMessages(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, - uidObj)) { - mHandler.sendMessageDelayed( - mHandler.obtainMessage(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, uidObj), - delay); - } else { - mHandler.removeEqualMessages(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED, uidObj); - } - } }; private final BroadcastStats getStatsLocked(PendingIntent pi) { diff --git a/core/api/current.txt b/core/api/current.txt index e3e042ce2a2e..83463eed2eb4 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -336,7 +336,7 @@ package android { field public static final String WRITE_SECURE_SETTINGS = "android.permission.WRITE_SECURE_SETTINGS"; field public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS"; field public static final String WRITE_SYNC_SETTINGS = "android.permission.WRITE_SYNC_SETTINGS"; - field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES"; + field @FlaggedApi("com.android.settingslib.flags.write_system_preference_permission_enabled") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES"; field public static final String WRITE_VOICEMAIL = "com.android.voicemail.permission.WRITE_VOICEMAIL"; } @@ -1276,6 +1276,7 @@ package android { field public static final int paddingStart = 16843699; // 0x10103b3 field public static final int paddingTop = 16842967; // 0x10100d7 field public static final int paddingVertical = 16844094; // 0x101053e + field @FlaggedApi("android.content.pm.app_compat_option_16kb") public static final int pageSizeCompat; field public static final int panelBackground = 16842846; // 0x101005e field public static final int panelColorBackground = 16842849; // 0x1010061 field public static final int panelColorForeground = 16842848; // 0x1010060 @@ -17496,6 +17497,25 @@ package android.graphics { method public void setIntUniform(@NonNull String, @NonNull int[]); } + @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode { + ctor public RuntimeXfermode(@NonNull String); + method public void setColorUniform(@NonNull String, @ColorInt int); + method public void setColorUniform(@NonNull String, @ColorLong long); + method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color); + method public void setFloatUniform(@NonNull String, float); + method public void setFloatUniform(@NonNull String, float, float); + method public void setFloatUniform(@NonNull String, float, float, float); + method public void setFloatUniform(@NonNull String, float, float, float, float); + method public void setFloatUniform(@NonNull String, @NonNull float[]); + method public void setInputColorFilter(@NonNull String, @NonNull android.graphics.ColorFilter); + method public void setInputShader(@NonNull String, @NonNull android.graphics.Shader); + method public void setIntUniform(@NonNull String, int); + method public void setIntUniform(@NonNull String, int, int); + method public void setIntUniform(@NonNull String, int, int, int); + method public void setIntUniform(@NonNull String, int, int, int, int); + method public void setIntUniform(@NonNull String, @NonNull int[]); + } + public class Shader { ctor @Deprecated public Shader(); method public boolean getLocalMatrix(@NonNull android.graphics.Matrix); @@ -18826,6 +18846,19 @@ package android.hardware { field public static final int TRANSFER_UNSPECIFIED = 0; // 0x0 } + @FlaggedApi("android.hardware.flags.luts_api") public final class DisplayLuts { + ctor @FlaggedApi("android.hardware.flags.luts_api") public DisplayLuts(); + method @FlaggedApi("android.hardware.flags.luts_api") public void set(@NonNull android.hardware.DisplayLuts.Entry); + method @FlaggedApi("android.hardware.flags.luts_api") public void set(@NonNull android.hardware.DisplayLuts.Entry, @NonNull android.hardware.DisplayLuts.Entry); + } + + @FlaggedApi("android.hardware.flags.luts_api") public static class DisplayLuts.Entry { + ctor @FlaggedApi("android.hardware.flags.luts_api") public DisplayLuts.Entry(@NonNull float[], int, int); + method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public float[] getBuffer(); + method @FlaggedApi("android.hardware.flags.luts_api") public int getDimension(); + method @FlaggedApi("android.hardware.flags.luts_api") public int getSamplingKey(); + } + public class GeomagneticField { ctor public GeomagneticField(float, float, float, long); method public float getDeclination(); @@ -18887,8 +18920,19 @@ package android.hardware { field @FlaggedApi("android.media.codec.p210_format_support") public static final int YCBCR_P210 = 60; // 0x3c } + @FlaggedApi("android.hardware.flags.luts_api") public final class LutProperties { + method @FlaggedApi("android.hardware.flags.luts_api") public int getDimension(); + method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public int[] getSamplingKeys(); + method @FlaggedApi("android.hardware.flags.luts_api") public int getSize(); + field @FlaggedApi("android.hardware.flags.luts_api") public static final int ONE_DIMENSION = 1; // 0x1 + field @FlaggedApi("android.hardware.flags.luts_api") public static final int SAMPLING_KEY_MAX_RGB = 1; // 0x1 + field @FlaggedApi("android.hardware.flags.luts_api") public static final int SAMPLING_KEY_RGB = 0; // 0x0 + field @FlaggedApi("android.hardware.flags.luts_api") public static final int THREE_DIMENSION = 3; // 0x3 + } + @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public final class OverlayProperties implements android.os.Parcelable { method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public int describeContents(); + method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public android.hardware.LutProperties[] getLutProperties(); method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public boolean isCombinationSupported(int, int); method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public boolean isMixedColorSpacesSupported(); method @FlaggedApi("android.hardware.flags.overlayproperties_class_api") public void writeToParcel(@NonNull android.os.Parcel, int); @@ -20628,8 +20672,14 @@ package android.hardware.display { method @NonNull public android.hardware.display.HdrConversionMode getHdrConversionMode(); method public int getMatchContentFrameRateUserPreference(); method public void registerDisplayListener(android.hardware.display.DisplayManager.DisplayListener, android.os.Handler); + method @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public void registerDisplayListener(@NonNull java.util.concurrent.Executor, long, @NonNull android.hardware.display.DisplayManager.DisplayListener); method public void unregisterDisplayListener(android.hardware.display.DisplayManager.DisplayListener); field public static final String DISPLAY_CATEGORY_PRESENTATION = "android.hardware.display.category.PRESENTATION"; + field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_FLAG_DISPLAY_ADDED = 1L; // 0x1L + field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_FLAG_DISPLAY_CHANGED = 4L; // 0x4L + field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_FLAG_DISPLAY_REFRESH_RATE = 8L; // 0x8L + field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_FLAG_DISPLAY_REMOVED = 2L; // 0x2L + field @FlaggedApi("com.android.server.display.feature.flags.display_listener_performance_improvements") public static final long EVENT_FLAG_DISPLAY_STATE = 16L; // 0x10L field public static final int MATCH_CONTENT_FRAMERATE_ALWAYS = 2; // 0x2 field public static final int MATCH_CONTENT_FRAMERATE_NEVER = 0; // 0x0 field public static final int MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY = 1; // 0x1 @@ -21019,6 +21069,7 @@ package android.inputmethodservice { method public abstract android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodImpl onCreateInputMethodInterface(); method public abstract android.inputmethodservice.AbstractInputMethodService.AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface(); method public boolean onGenericMotionEvent(android.view.MotionEvent); + method @FlaggedApi("android.view.inputmethod.verify_key_event") public boolean onShouldVerifyKeyEvent(@NonNull android.view.KeyEvent); method public boolean onTrackballEvent(android.view.MotionEvent); } @@ -21036,6 +21087,7 @@ package android.inputmethodservice { method public void dispatchTrackballEvent(int, android.view.MotionEvent, android.view.inputmethod.InputMethodSession.EventCallback); method public boolean isEnabled(); method public boolean isRevoked(); + method @FlaggedApi("android.view.inputmethod.verify_key_event") public boolean onShouldVerifyKeyEvent(@NonNull android.view.KeyEvent); method public void revokeSelf(); method public void setEnabled(boolean); } @@ -21976,7 +22028,7 @@ package android.media { public final class AudioPlaybackConfiguration implements android.os.Parcelable { method public int describeContents(); method public android.media.AudioAttributes getAudioAttributes(); - method @Nullable public android.media.AudioDeviceInfo getAudioDeviceInfo(); + method @Deprecated @FlaggedApi("android.media.audio.routed_device_ids") @Nullable public android.media.AudioDeviceInfo getAudioDeviceInfo(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioPlaybackConfiguration> CREATOR; } @@ -22059,6 +22111,7 @@ package android.media { method public android.media.AudioDeviceInfo getPreferredDevice(); method public int getRecordingState(); method public android.media.AudioDeviceInfo getRoutedDevice(); + method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices(); method public int getSampleRate(); method public int getState(); method public int getTimestamp(@NonNull android.media.AudioTimestamp, int); @@ -22153,6 +22206,7 @@ package android.media { method public void addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, android.os.Handler); method public android.media.AudioDeviceInfo getPreferredDevice(); method public android.media.AudioDeviceInfo getRoutedDevice(); + method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public default java.util.List<android.media.AudioDeviceInfo> getRoutedDevices(); method public void removeOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener); method public boolean setPreferredDevice(android.media.AudioDeviceInfo); } @@ -22211,6 +22265,7 @@ package android.media { method public int getPositionNotificationPeriod(); method public android.media.AudioDeviceInfo getPreferredDevice(); method public android.media.AudioDeviceInfo getRoutedDevice(); + method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices(); method public int getSampleRate(); method @IntRange(from=1) public int getStartThresholdInFrames(); method public int getState(); @@ -24379,6 +24434,7 @@ package android.media { method @NonNull public android.media.PlaybackParams getPlaybackParams(); method public android.media.AudioDeviceInfo getPreferredDevice(); method public android.media.AudioDeviceInfo getRoutedDevice(); + method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices(); method public int getSelectedTrack(int) throws java.lang.IllegalStateException; method @NonNull public android.media.SyncParams getSyncParams(); method @Nullable public android.media.MediaTimestamp getTimestamp(); @@ -24592,6 +24648,7 @@ package android.media { method public android.os.PersistableBundle getMetrics(); method public android.media.AudioDeviceInfo getPreferredDevice(); method public android.media.AudioDeviceInfo getRoutedDevice(); + method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull public java.util.List<android.media.AudioDeviceInfo> getRoutedDevices(); method public android.view.Surface getSurface(); method public boolean isPrivacySensitive(); method public void pause() throws java.lang.IllegalStateException; @@ -28523,6 +28580,8 @@ package android.media.tv.ad { method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback); method public void setOnUnhandledInputEventListener(@NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener); method public boolean setTvView(@Nullable android.media.tv.TvView); + method @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public void setZOrderMediaOverlay(boolean); + method @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public void setZOrderOnTop(boolean); method public void startAdService(); method public void stopAdService(); field public static final String ERROR_KEY_ERROR_CODE = "error_code"; @@ -28795,6 +28854,8 @@ package android.media.tv.interactive { method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.interactive.TvInteractiveAppView.OnUnhandledInputEventListener); method public void setTeletextAppEnabled(boolean); method public int setTvView(@Nullable android.media.tv.TvView); + method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void setZOrderMediaOverlay(boolean); + method @FlaggedApi("android.media.tv.flags.tiaf_v_apis") public void setZOrderOnTop(boolean); method public void startInteractiveApp(); method public void stopInteractiveApp(); field public static final String BI_INTERACTIVE_APP_KEY_ALIAS = "alias"; @@ -29834,6 +29895,7 @@ package android.net.vcn { method @NonNull public java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate> getVcnUnderlyingNetworkPriorities(); method public boolean hasGatewayOption(int); method @FlaggedApi("android.net.vcn.safe_mode_config") public boolean isSafeModeEnabled(); + field @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET = -1; // 0xffffffff field public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; // 0x0 } @@ -33307,6 +33369,14 @@ package android.os { method public final android.os.CountDownTimer start(); } + @FlaggedApi("android.os.cpu_gpu_headrooms") public final class CpuHeadroomParams { + ctor public CpuHeadroomParams(); + method public int getCalculationType(); + method public void setCalculationType(int); + field public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1 + field public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0 + } + public final class CpuUsageInfo implements android.os.Parcelable { method public int describeContents(); method public long getActive(); @@ -33554,6 +33624,14 @@ package android.os { method public void onProgress(long); } + @FlaggedApi("android.os.cpu_gpu_headrooms") public final class GpuHeadroomParams { + ctor public GpuHeadroomParams(); + method public int getCalculationType(); + method public void setCalculationType(int); + field public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; // 0x1 + field public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0; // 0x0 + } + public class Handler { ctor @Deprecated public Handler(); ctor @Deprecated public Handler(@Nullable android.os.Handler.Callback); @@ -34804,6 +34882,10 @@ package android.os.health { } public class SystemHealthManager { + method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getCpuHeadroom(@Nullable android.os.CpuHeadroomParams); + method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getCpuHeadroomMinIntervalMillis(); + method @FlaggedApi("android.os.cpu_gpu_headrooms") @FloatRange(from=0.0f, to=100.0f) public float getGpuHeadroom(@Nullable android.os.GpuHeadroomParams); + method @FlaggedApi("android.os.cpu_gpu_headrooms") public long getGpuHeadroomMinIntervalMillis(); method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getPowerMonitorReadings(@NonNull java.util.List<android.os.PowerMonitor>, @Nullable java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.PowerMonitorReadings,java.lang.RuntimeException>); method @FlaggedApi("com.android.server.power.optimization.power_monitor_api") public void getSupportedPowerMonitors(@Nullable java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.os.PowerMonitor>>); method public android.os.health.HealthStats takeMyUidSnapshot(); @@ -46666,6 +46748,8 @@ package android.telephony { method public long getDataUsageBytes(); method public long getDataUsageTime(); method @NonNull public int[] getNetworkTypes(); + method @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") @Nullable public java.time.ZonedDateTime getPlanEndDate(); + method @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public int getSubscriptionStatus(); method @Nullable public CharSequence getSummary(); method @Nullable public CharSequence getTitle(); method public void writeToParcel(android.os.Parcel, int); @@ -46676,6 +46760,11 @@ package android.telephony { field public static final int LIMIT_BEHAVIOR_DISABLED = 0; // 0x0 field public static final int LIMIT_BEHAVIOR_THROTTLED = 2; // 0x2 field public static final int LIMIT_BEHAVIOR_UNKNOWN = -1; // 0xffffffff + field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_ACTIVE = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_INACTIVE = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_SUSPENDED = 4; // 0x4 + field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_TRIAL = 3; // 0x3 + field @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") public static final int SUBSCRIPTION_STATUS_UNKNOWN = 0; // 0x0 field public static final long TIME_UNKNOWN = -1L; // 0xffffffffffffffffL } @@ -46687,6 +46776,7 @@ package android.telephony { method public android.telephony.SubscriptionPlan.Builder setDataLimit(long, int); method public android.telephony.SubscriptionPlan.Builder setDataUsage(long, long); method @NonNull public android.telephony.SubscriptionPlan.Builder setNetworkTypes(@NonNull int[]); + method @FlaggedApi("com.android.internal.telephony.flags.subscription_plan_allow_status_and_end_date") @NonNull public android.telephony.SubscriptionPlan.Builder setSubscriptionStatus(int); method public android.telephony.SubscriptionPlan.Builder setSummary(@Nullable CharSequence); method public android.telephony.SubscriptionPlan.Builder setTitle(@Nullable CharSequence); } @@ -53162,6 +53252,7 @@ package android.view { method @FlaggedApi("com.android.window.flags.sdk_desired_present_time") @NonNull public android.view.SurfaceControl.Transaction setFrameTimeline(long); method @Deprecated @NonNull public android.view.SurfaceControl.Transaction setGeometry(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect, @Nullable android.graphics.Rect, int); method @NonNull public android.view.SurfaceControl.Transaction setLayer(@NonNull android.view.SurfaceControl, @IntRange(from=java.lang.Integer.MIN_VALUE, to=java.lang.Integer.MAX_VALUE) int); + method @FlaggedApi("android.hardware.flags.luts_api") @NonNull public android.view.SurfaceControl.Transaction setLuts(@NonNull android.view.SurfaceControl, @Nullable android.hardware.DisplayLuts); method @NonNull public android.view.SurfaceControl.Transaction setOpaque(@NonNull android.view.SurfaceControl, boolean); method @NonNull public android.view.SurfaceControl.Transaction setPosition(@NonNull android.view.SurfaceControl, float, float); method @NonNull public android.view.SurfaceControl.Transaction setScale(@NonNull android.view.SurfaceControl, float, float); @@ -56954,6 +57045,7 @@ package android.view.inputmethod { method @NonNull public java.util.Set<java.lang.Class<? extends android.view.inputmethod.PreviewableHandwritingGesture>> getSupportedHandwritingGesturePreviews(); method @NonNull public java.util.List<java.lang.Class<? extends android.view.inputmethod.HandwritingGesture>> getSupportedHandwritingGestures(); method @FlaggedApi("android.view.inputmethod.editorinfo_handwriting_enabled") public boolean isStylusHandwritingEnabled(); + method @FlaggedApi("android.view.inputmethod.writing_tools") public boolean isWritingToolsEnabled(); method public final void makeCompatible(int); method @FlaggedApi("android.view.inputmethod.public_autofill_id_in_editorinfo") public void setAutofillId(@Nullable android.view.autofill.AutofillId); method public void setInitialSurroundingSubText(@NonNull CharSequence, int); @@ -56962,6 +57054,7 @@ package android.view.inputmethod { method @FlaggedApi("android.view.inputmethod.editorinfo_handwriting_enabled") public void setStylusHandwritingEnabled(boolean); method public void setSupportedHandwritingGesturePreviews(@NonNull java.util.Set<java.lang.Class<? extends android.view.inputmethod.PreviewableHandwritingGesture>>); method public void setSupportedHandwritingGestures(@NonNull java.util.List<java.lang.Class<? extends android.view.inputmethod.HandwritingGesture>>); + method @FlaggedApi("android.view.inputmethod.writing_tools") public void setWritingToolsEnabled(boolean); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.EditorInfo> CREATOR; field public static final int IME_ACTION_DONE = 6; // 0x6 diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index bc73220cb4c1..1a949d84c052 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -251,6 +251,10 @@ package android.media.session { package android.net { + @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public final class ConnectivityFrameworkInitializerBaklava { + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static void registerServiceWrappers(); + } + public class LocalSocket implements java.io.Closeable { ctor public LocalSocket(@NonNull java.io.FileDescriptor); } @@ -310,6 +314,25 @@ package android.net.netstats { } +package android.net.vcn { + + @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public final class VcnTransportInfo implements android.os.Parcelable android.net.TransportInfo { + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public int describeContents(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public long getApplicableRedactions(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public int getMinUdpPort4500NatTimeoutSeconds(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.TransportInfo makeCopy(long); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public void writeToParcel(@NonNull android.os.Parcel, int); + field @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public static final android.os.Parcelable.Creator<android.net.vcn.VcnTransportInfo> CREATOR; + } + + @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static final class VcnTransportInfo.Builder { + ctor @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public VcnTransportInfo.Builder(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.vcn.VcnTransportInfo build(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.vcn.VcnTransportInfo.Builder setMinUdpPort4500NatTimeoutSeconds(@IntRange(from=0x78) int); + } + +} + package android.net.wifi { public final class WifiMigration { @@ -379,6 +402,13 @@ package android.os { field public static final int DEVICE_INITIAL_SDK_INT; } + public final class Bundle extends android.os.BaseBundle implements java.lang.Cloneable android.os.Parcelable { + method @FlaggedApi("android.os.enable_has_binders") public int hasBinders(); + field @FlaggedApi("android.os.enable_has_binders") public static final int STATUS_BINDERS_NOT_PRESENT = 0; // 0x0 + field @FlaggedApi("android.os.enable_has_binders") public static final int STATUS_BINDERS_PRESENT = 1; // 0x1 + field @FlaggedApi("android.os.enable_has_binders") public static final int STATUS_BINDERS_UNKNOWN = 2; // 0x2 + } + public class Handler { method @FlaggedApi("android.os.mainline_vcn_platform_api") public final boolean hasMessagesOrCallbacks(); method @FlaggedApi("android.os.mainline_vcn_platform_api") public final void removeCallbacksAndEqualMessages(@Nullable Object); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ed95fdd52f45..a46f872bc1d4 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -65,6 +65,7 @@ package android { field @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public static final String BIND_EXPLICIT_HEALTH_CHECK_SERVICE = "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"; field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE"; field public static final String BIND_FIELD_CLASSIFICATION_SERVICE = "android.permission.BIND_FIELD_CLASSIFICATION_SERVICE"; + field @FlaggedApi("android.security.afl_api") public static final String BIND_FORENSIC_EVENT_TRANSPORT_SERVICE = "android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE"; field public static final String BIND_GBA_SERVICE = "android.permission.BIND_GBA_SERVICE"; field public static final String BIND_HOTWORD_DETECTION_SERVICE = "android.permission.BIND_HOTWORD_DETECTION_SERVICE"; field public static final String BIND_IMS_SERVICE = "android.permission.BIND_IMS_SERVICE"; @@ -209,6 +210,7 @@ package android { field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String MANAGE_ENHANCED_CONFIRMATION_STATES = "android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES"; field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS"; field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION"; + field @FlaggedApi("android.security.afl_api") public static final String MANAGE_FORENSIC_STATE = "android.permission.MANAGE_FORENSIC_STATE"; field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY"; field public static final String MANAGE_GAME_MODE = "android.permission.MANAGE_GAME_MODE"; field @FlaggedApi("android.media.tv.flags.media_quality_fw") public static final String MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE = "android.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE"; @@ -304,6 +306,7 @@ package android { field public static final String READ_CONTENT_RATING_SYSTEMS = "android.permission.READ_CONTENT_RATING_SYSTEMS"; field public static final String READ_DEVICE_CONFIG = "android.permission.READ_DEVICE_CONFIG"; field public static final String READ_DREAM_STATE = "android.permission.READ_DREAM_STATE"; + field @FlaggedApi("android.security.afl_api") public static final String READ_FORENSIC_STATE = "android.permission.READ_FORENSIC_STATE"; field public static final String READ_GLOBAL_APP_SEARCH_DATA = "android.permission.READ_GLOBAL_APP_SEARCH_DATA"; field @FlaggedApi("android.content.pm.get_resolved_apk_path") public static final String READ_INSTALLED_SESSION_PATHS = "android.permission.READ_INSTALLED_SESSION_PATHS"; field public static final String READ_INSTALL_SESSIONS = "android.permission.READ_INSTALL_SESSIONS"; @@ -5045,15 +5048,27 @@ package android.hardware.biometrics { package android.hardware.camera2 { + public final class CameraCharacteristics extends android.hardware.camera2.CameraMetadata<android.hardware.camera2.CameraCharacteristics.Key<?>> { + field @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.SharedSessionConfiguration> SHARED_SESSION_CONFIGURATION; + } + public abstract class CameraDevice implements java.lang.AutoCloseable { method @Deprecated public abstract void createCustomCaptureSession(android.hardware.camera2.params.InputConfiguration, @NonNull java.util.List<android.hardware.camera2.params.OutputConfiguration>, int, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback, @Nullable android.os.Handler) throws android.hardware.camera2.CameraAccessException; field public static final int SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED = 1; // 0x1 field public static final int SESSION_OPERATION_MODE_NORMAL = 0; // 0x0 + field @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public static final int SESSION_OPERATION_MODE_SHARED = 2; // 0x2 field public static final int SESSION_OPERATION_MODE_VENDOR_START = 32768; // 0x8000 } + public abstract static class CameraDevice.StateCallback { + method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public void onClientSharedAccessPriorityChanged(@NonNull android.hardware.camera2.CameraDevice, boolean); + method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public void onOpenedInSharedMode(@NonNull android.hardware.camera2.CameraDevice, boolean); + } + public final class CameraManager { + method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public boolean isCameraDeviceSharingSupported(@NonNull String) throws android.hardware.camera2.CameraAccessException; method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; + method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openSharedCamera(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException; } public abstract static class CameraManager.AvailabilityCallback { @@ -5061,6 +5076,12 @@ package android.hardware.camera2 { method @RequiresPermission(android.Manifest.permission.CAMERA_OPEN_CLOSE_LISTENER) public void onCameraOpened(@NonNull String, @NonNull String); } + @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public abstract class CameraSharedCaptureSession extends android.hardware.camera2.CameraCaptureSession { + ctor public CameraSharedCaptureSession(); + method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public abstract int startStreaming(@NonNull java.util.List<android.view.Surface>, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.CaptureCallback) throws android.hardware.camera2.CameraAccessException; + method @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public abstract void stopStreaming() throws android.hardware.camera2.CameraAccessException; + } + } package android.hardware.camera2.extension { @@ -5169,6 +5190,50 @@ package android.hardware.camera2.params { field public static final int ROTATION_90 = 1; // 0x1 } + public final class SessionConfiguration implements android.os.Parcelable { + field @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public static final int SESSION_SHARED = 2; // 0x2 + } + + @FlaggedApi("com.android.internal.camera.flags.camera_multi_client") public final class SharedSessionConfiguration { + method @Nullable public android.graphics.ColorSpace getColorSpace(); + method @NonNull public java.util.List<android.hardware.camera2.params.SharedSessionConfiguration.SharedOutputConfiguration> getOutputStreamsInformation(); + } + + public static final class SharedSessionConfiguration.SharedOutputConfiguration { + method public int getDataspace(); + method public int getFormat(); + method public int getMirrorMode(); + method @Nullable public String getPhysicalCameraId(); + method @NonNull public android.util.Size getSize(); + method public long getStreamUseCase(); + method public int getSurfaceType(); + method public int getTimestampBase(); + method public long getUsage(); + method public boolean isReadoutTimestampEnabled(); + } + +} + +package android.hardware.contexthub { + + @FlaggedApi("android.chre.flags.offload_api") public class HubDiscoveryInfo { + method @NonNull public android.hardware.contexthub.HubEndpointInfo getHubEndpointInfo(); + } + + @FlaggedApi("android.chre.flags.offload_api") public final class HubEndpointInfo implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier getIdentifier(); + method @NonNull public String getName(); + method @Nullable public String getTag(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.contexthub.HubEndpointInfo> CREATOR; + } + + public static class HubEndpointInfo.HubEndpointIdentifier { + method public long getEndpoint(); + method public long getHub(); + } + } package android.hardware.devicestate { @@ -6162,6 +6227,7 @@ package android.hardware.location { method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubClient createClient(@NonNull android.hardware.location.ContextHubInfo, @NonNull android.app.PendingIntent, long); method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> disableNanoApp(@NonNull android.hardware.location.ContextHubInfo, long); method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> enableNanoApp(@NonNull android.hardware.location.ContextHubInfo, long); + method @FlaggedApi("android.chre.flags.offload_api") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public java.util.List<android.hardware.contexthub.HubDiscoveryInfo> findEndpoints(long); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int[] findNanoAppOnHub(int, @NonNull android.hardware.location.NanoAppFilter); method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int[] getContextHubHandles(); method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubInfo getContextHubInfo(int); @@ -7464,6 +7530,7 @@ package android.media { } public final class AudioPlaybackConfiguration implements android.os.Parcelable { + method @FlaggedApi("android.media.audio.routed_device_ids") @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceInfo> getAudioDeviceInfos(); method public int getChannelMask(); method public int getClientPid(); method public int getClientUid(); @@ -7618,7 +7685,7 @@ package android.media { public final class MediaCas implements java.lang.AutoCloseable { method @FlaggedApi("android.media.tv.flags.set_resource_holder_retain") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void setResourceHolderRetain(boolean); - method @FlaggedApi("com.android.media.flags.update_client_profile_priority") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public boolean updateResourcePriority(int, int); + method @FlaggedApi("android.media.tv.flags.mediacas_update_client_profile_priority") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public boolean updateResourcePriority(int, int); } public final class MediaCodec { @@ -12570,6 +12637,29 @@ package android.security.advancedprotection { } +package android.security.forensic { + + @FlaggedApi("android.security.afl_api") public class ForensicManager { + method @RequiresPermission(android.Manifest.permission.READ_FORENSIC_STATE) public void addStateCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.MANAGE_FORENSIC_STATE) public void disable(@NonNull java.util.concurrent.Executor, @NonNull android.security.forensic.ForensicManager.CommandCallback); + method @RequiresPermission(android.Manifest.permission.MANAGE_FORENSIC_STATE) public void enable(@NonNull java.util.concurrent.Executor, @NonNull android.security.forensic.ForensicManager.CommandCallback); + method @RequiresPermission(android.Manifest.permission.READ_FORENSIC_STATE) public void removeStateCallback(@NonNull java.util.function.Consumer<java.lang.Integer>); + field public static final int ERROR_DATA_SOURCE_UNAVAILABLE = 4; // 0x4 + field public static final int ERROR_PERMISSION_DENIED = 1; // 0x1 + field public static final int ERROR_TRANSPORT_UNAVAILABLE = 3; // 0x3 + field public static final int ERROR_UNKNOWN = 0; // 0x0 + field public static final int STATE_DISABLED = 1; // 0x1 + field public static final int STATE_ENABLED = 2; // 0x2 + field public static final int STATE_UNKNOWN = 0; // 0x0 + } + + public static interface ForensicManager.CommandCallback { + method public void onFailure(int); + method public void onSuccess(); + } + +} + package android.security.keystore { public class AndroidKeyStoreProvider extends java.security.Provider { diff --git a/core/java/Android.bp b/core/java/Android.bp index cf5ebbaa37b4..bc38294279a8 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -232,6 +232,8 @@ aidl_interface { "android.hardware.power-aidl", ], srcs: [ + "android/os/CpuHeadroomParamsInternal.aidl", + "android/os/GpuHeadroomParamsInternal.aidl", "android/os/IHintManager.aidl", "android/os/IHintSession.aidl", ], diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 8b37dbd04bec..6c03b32a4816 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1624,9 +1624,13 @@ public class AppOpsManager { /** @hide Access to read oxygen saturation. */ public static final int OP_READ_OXYGEN_SATURATION = AppOpEnums.APP_OP_READ_OXYGEN_SATURATION; + /** @hide Access to write system preferences. */ + public static final int OP_WRITE_SYSTEM_PREFERENCES = + AppOpEnums.APP_OP_WRITE_SYSTEM_PREFERENCES; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 153; + public static final int _NUM_OP = 154; /** * All app ops represented as strings. @@ -1783,6 +1787,7 @@ public class AppOpsManager { OPSTR_READ_SKIN_TEMPERATURE, OPSTR_RANGING, OPSTR_READ_OXYGEN_SATURATION, + OPSTR_WRITE_SYSTEM_PREFERENCES, }) public @interface AppOpString {} @@ -2540,6 +2545,9 @@ public class AppOpsManager { @FlaggedApi(Flags.FLAG_RANGING_PERMISSION_ENABLED) public static final String OPSTR_RANGING = "android:ranging"; + /** @hide Access to system preferences write services */ + public static final String OPSTR_WRITE_SYSTEM_PREFERENCES = "android:write_system_preferences"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2656,6 +2664,7 @@ public class AppOpsManager { OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, + OP_WRITE_SYSTEM_PREFERENCES, }; @SuppressWarnings("FlaggedApi") @@ -3144,6 +3153,10 @@ public class AppOpsManager { Flags.replaceBodySensorPermissionEnabled() ? HealthPermissions.READ_OXYGEN_SATURATION : null) .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_WRITE_SYSTEM_PREFERENCES, OPSTR_WRITE_SYSTEM_PREFERENCES, + "WRITE_SYSTEM_PREFERENCES").setPermission( + com.android.settingslib.flags.Flags.writeSystemPreferencePermissionEnabled() + ? Manifest.permission.WRITE_SYSTEM_PREFERENCES : null).build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 961501503348..7a329cd541a2 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -112,8 +112,8 @@ interface IActivityClientController { oneway void requestMultiwindowFullscreen(in IBinder token, in int request, in IRemoteCallback callback); - oneway void startLockTaskModeByToken(in IBinder token); - oneway void stopLockTaskModeByToken(in IBinder token); + void startLockTaskModeByToken(in IBinder token); + void stopLockTaskModeByToken(in IBinder token); oneway void showLockTaskEscapeMessage(in IBinder token); void setTaskDescription(in IBinder token, in ActivityManager.TaskDescription values); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index e451116081fa..a0639177266c 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -163,6 +163,7 @@ import android.media.tv.tunerresourcemanager.ITunerResourceManager; import android.media.tv.tunerresourcemanager.TunerResourceManager; import android.nearby.NearbyFrameworkInitializer; import android.net.ConnectivityFrameworkInitializer; +import android.net.ConnectivityFrameworkInitializerBaklava; import android.net.ConnectivityFrameworkInitializerTiramisu; import android.net.INetworkPolicyManager; import android.net.IPacProxyManager; @@ -173,7 +174,6 @@ import android.net.NetworkWatchlistManager; import android.net.PacProxyManager; import android.net.TetheringManager; import android.net.VpnManager; -import android.net.vcn.VcnFrameworkInitializer; import android.net.wifi.WifiFrameworkInitializer; import android.net.wifi.nl80211.WifiNl80211Manager; import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager; @@ -190,6 +190,7 @@ import android.os.IBatteryPropertiesRegistrar; import android.os.IBinder; import android.os.IDumpstate; import android.os.IHardwarePropertiesManager; +import android.os.IHintManager; import android.os.IPowerManager; import android.os.IPowerStatsService; import android.os.IRecoverySystem; @@ -238,6 +239,8 @@ import android.security.advancedprotection.AdvancedProtectionManager; import android.security.advancedprotection.IAdvancedProtectionService; import android.security.attestationverification.AttestationVerificationManager; import android.security.attestationverification.IAttestationVerificationManagerService; +import android.security.forensic.ForensicManager; +import android.security.forensic.IForensicService; import android.security.keystore.KeyStoreManager; import android.service.oemlock.IOemLockService; import android.service.oemlock.OemLockManager; @@ -1195,8 +1198,10 @@ public final class SystemServiceRegistry { public SystemHealthManager createService(ContextImpl ctx) throws ServiceNotFoundException { IBinder batteryStats = ServiceManager.getServiceOrThrow(BatteryStats.SERVICE_NAME); IBinder powerStats = ServiceManager.getService(Context.POWER_STATS_SERVICE); + IBinder perfHint = ServiceManager.getService(Context.PERFORMANCE_HINT_SERVICE); return new SystemHealthManager(IBatteryStats.Stub.asInterface(batteryStats), - IPowerStatsService.Stub.asInterface(powerStats)); + IPowerStatsService.Stub.asInterface(powerStats), + IHintManager.Stub.asInterface(perfHint)); }}); registerService(Context.CONTEXTHUB_SERVICE, ContextHubManager.class, @@ -1790,6 +1795,18 @@ public final class SystemServiceRegistry { } }); + registerService(Context.FORENSIC_SERVICE, ForensicManager.class, + new CachedServiceFetcher<ForensicManager>() { + @Override + public ForensicManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow( + Context.FORENSIC_SERVICE); + IForensicService service = IForensicService.Stub.asInterface(b); + return new ForensicManager(service); + } + }); + sInitializing = true; try { // Note: the following functions need to be @SystemApis, once they become mainline @@ -1818,7 +1835,7 @@ public final class SystemServiceRegistry { OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); DeviceLockFrameworkInitializer.registerServiceWrappers(); VirtualizationFrameworkInitializer.registerServiceWrappers(); - VcnFrameworkInitializer.registerServiceWrappers(); + ConnectivityFrameworkInitializerBaklava.registerServiceWrappers(); if (com.android.server.telecom.flags.Flags.telecomMainlineBlockedNumbersManager()) { ProviderFrameworkInitializer.registerServiceWrappers(); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index c789c41ec431..4e68b5af72d2 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -12196,6 +12196,33 @@ public class DevicePolicyManager { } /** + * Adds a user restriction globally, specified by the {@code key}. + * + * <p>Called by a system service only, meaning that the caller's UID must be equal to + * {@link Process#SYSTEM_UID}. + * + * @param systemEntity The service entity that adds the restriction. A user restriction set by + * a service entity can only be cleared by the same entity. This can be + * just the calling package name, or any string of the caller's choice + * can be used. + * @param key The key of the restriction. + * @throws SecurityException if the caller is not a system service. + * + * @hide + */ + public void addUserRestrictionGlobally(@NonNull String systemEntity, + @NonNull @UserManager.UserRestrictionKey String key) { + if (mService != null) { + try { + mService.setUserRestrictionGloballyFromSystem(systemEntity, key, + /* enable= */ true); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** * Called by a profile owner, device owner or a holder of any permission that is associated with * a user restriction to clear a user restriction specified by the key. * <p> @@ -12281,6 +12308,33 @@ public class DevicePolicyManager { } /** + * Clears a user restriction globally, specified by the {@code key}. + * + * <p>Called by a system service only, meaning that the caller's UID must be equal to + * {@link Process#SYSTEM_UID}. + * + * @param systemEntity The system entity that clears the restriction. A user restriction + * set by a system entity can only be cleared by the same entity. This + * can be just the calling package name, or any string of the caller's + * choice can be used. + * @param key The key of the restriction. + * @throws SecurityException if the caller is not a system service. + * + * @hide + */ + public void clearUserRestrictionGlobally(@NonNull String systemEntity, + @NonNull @UserManager.UserRestrictionKey String key) { + if (mService != null) { + try { + mService.setUserRestrictionGloballyFromSystem(systemEntity, key, + /* enable= */ false); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** * Called by an admin to get user restrictions set by themselves with * {@link #addUserRestriction(ComponentName, String)}. * <p> diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index a40680218039..fa984af68016 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -257,6 +257,7 @@ interface IDevicePolicyManager { void setUserRestriction(in ComponentName who, in String callerPackage, in String key, boolean enable, boolean parent); void setUserRestrictionForUser(in String systemEntity, in String key, boolean enable, int targetUser); void setUserRestrictionGlobally(in String callerPackage, in String key); + void setUserRestrictionGloballyFromSystem(in String systemEntity, in String key, boolean enable); Bundle getUserRestrictions(in ComponentName who, in String callerPackage, boolean parent); Bundle getUserRestrictionsGlobally(in String callerPackage); diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/core/java/android/app/supervision/SupervisionManagerInternal.java index 5df9dd521092..d571e14ff5fa 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java +++ b/core/java/android/app/supervision/SupervisionManagerInternal.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.server.supervision; +package android.app.supervision; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.os.Bundle; +import android.os.PersistableBundle; /** * Local system service interface for {@link SupervisionService}. @@ -35,6 +35,11 @@ public abstract class SupervisionManagerInternal { public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId); /** + * Returns whether the supervision lock screen needs to be shown. + */ + public abstract boolean isSupervisionLockscreenEnabledForUser(@UserIdInt int userId); + + /** * Set whether supervision is enabled for the specified user. * * @param userId The user to set the supervision state for @@ -50,5 +55,5 @@ public abstract class SupervisionManagerInternal { * @param options Optional configuration parameters for the supervision lock screen */ public abstract void setSupervisionLockscreenEnabledForUser( - @UserIdInt int userId, boolean enabled, @Nullable Bundle options); + @UserIdInt int userId, boolean enabled, @Nullable PersistableBundle options); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 3152ff4564fe..13b13b9e4179 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -292,6 +292,10 @@ public abstract class PackageManager { * <p> * The value of a property will only have a single type, as defined by * the property itself. + * + * <p class="note"><strong>Note:</strong> + * In android version {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and earlier, + * the {@code equals} and {@code hashCode} methods for this class may not function as expected. */ public static final class Property implements Parcelable { private static final int TYPE_BOOLEAN = 1; @@ -523,6 +527,40 @@ public abstract class PackageManager { return new Property[size]; } }; + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Property)) { + return false; + } + final Property property = (Property) obj; + return mType == property.mType && + Objects.equals(mName, property.mName) && + Objects.equals(mClassName, property.mClassName) && + Objects.equals(mPackageName, property.mPackageName) && + (mType == TYPE_BOOLEAN ? mBooleanValue == property.mBooleanValue : + mType == TYPE_FLOAT ? Float.compare(mFloatValue, property.mFloatValue) == 0 : + mType == TYPE_INTEGER ? mIntegerValue == property.mIntegerValue : + mType == TYPE_RESOURCE ? mIntegerValue == property.mIntegerValue : + mStringValue.equals(property.mStringValue)); + } + + @Override + public int hashCode() { + int result = Objects.hash(mName, mType, mClassName, mPackageName); + if (mType == TYPE_BOOLEAN) { + result = 31 * result + (mBooleanValue ? 1 : 0); + } else if (mType == TYPE_FLOAT) { + result = 31 * result + Float.floatToIntBits(mFloatValue); + } else if (mType == TYPE_INTEGER) { + result = 31 * result + mIntegerValue; + } else if (mType == TYPE_RESOURCE) { + result = 31 * result + mIntegerValue; + } else if (mType == TYPE_STRING) { + result = 31 * result + mStringValue.hashCode(); + } + return result; + } } /** diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 9ba5a352358b..e181ae8ef3c7 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -366,3 +366,13 @@ flag { description: "Block app installations that specify an incompatible minor SDK version" bug: "377474232" } + +flag { + name: "app_compat_option_16kb" + is_exported: true + namespace: "devoptions_settings" + description: "Feature flag to enable page size app compat mode from manifest, package manager and settings level." + bug: "371049373" + is_fixed_read_only: true +} + diff --git a/core/java/android/hardware/DisplayLuts.java b/core/java/android/hardware/DisplayLuts.java index b162ad6e2d15..6343ba19f569 100644 --- a/core/java/android/hardware/DisplayLuts.java +++ b/core/java/android/hardware/DisplayLuts.java @@ -16,116 +16,294 @@ package android.hardware; +import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.hardware.flags.Flags; import android.util.IntArray; import java.util.ArrayList; -import java.util.List; /** - * @hide + * DisplayLuts provides the developers to apply Lookup Tables (Luts) to a + * {@link android.view.SurfaceControl}. Luts provides ways to control tonemapping + * for specific content. + * + * The general flow is as follows: + * <p> + * <img src="{@docRoot}reference/android/images/graphics/DisplayLuts.png" /> + * <figcaption style="text-align: center;">DisplayLuts flow</figcaption> + * </p> + * + * @see LutProperties */ +@FlaggedApi(Flags.FLAG_LUTS_API) public final class DisplayLuts { + private ArrayList<Entry> mEntries; private IntArray mOffsets; private int mTotalLength; - private List<float[]> mLutBuffers; - private IntArray mLutDimensions; - private IntArray mLutSizes; - private IntArray mLutSamplingKeys; - private static final int LUT_LENGTH_LIMIT = 100000; - + /** + * Create a {@link DisplayLuts} instance. + */ + @FlaggedApi(Flags.FLAG_LUTS_API) public DisplayLuts() { + mEntries = new ArrayList<>(); mOffsets = new IntArray(); mTotalLength = 0; - - mLutBuffers = new ArrayList<>(); - mLutDimensions = new IntArray(); - mLutSizes = new IntArray(); - mLutSamplingKeys = new IntArray(); } - /** - * Add the lut to be applied. - * - * @param buffer - * @param dimension either 1D or 3D - * @param size - * @param samplingKey - */ - public void addLut(@NonNull float[] buffer, @LutProperties.Dimension int dimension, - int size, @LutProperties.SamplingKey int samplingKey) { + @FlaggedApi(Flags.FLAG_LUTS_API) + public static class Entry { + private float[] mBuffer; + private @LutProperties.Dimension int mDimension; + private int mSize; + private @LutProperties.SamplingKey int mSamplingKey; + + private static final int LUT_LENGTH_LIMIT = 100000; + + /** + * Create a Lut entry. + * + * <p> + * Noted that 1D Lut(s) are treated as gain curves. + * For 3D Lut(s), 3D Lut(s) are used for direct color manipulations. + * The values of 3D Lut(s) data should be normalized to the range {@code 0.0} + * to {@code 1.0}, inclusive. And 3D Lut(s) data is organized in the order of + * R, G, B channels. + * + * @param buffer The raw lut data + * @param dimension Either 1D or 3D + * @param samplingKey The sampling kay used for the Lut + */ + @FlaggedApi(Flags.FLAG_LUTS_API) + public Entry(@NonNull float[] buffer, + @LutProperties.Dimension int dimension, + @LutProperties.SamplingKey int samplingKey) { + if (buffer == null || buffer.length < 1) { + throw new IllegalArgumentException("The buffer cannot be empty!"); + } + + if (buffer.length >= LUT_LENGTH_LIMIT) { + throw new IllegalArgumentException("The lut length is too big to handle!"); + } + + if (dimension != LutProperties.ONE_DIMENSION + && dimension != LutProperties.THREE_DIMENSION) { + throw new IllegalArgumentException("The dimension should be either 1D or 3D!"); + } + + if (dimension == LutProperties.THREE_DIMENSION) { + if (buffer.length <= 3) { + throw new IllegalArgumentException( + "The 3d lut size of each dimension should be over 1!"); + } + int lengthPerChannel = buffer.length; + if (lengthPerChannel % 3 != 0) { + throw new IllegalArgumentException( + "The lut buffer of 3dlut should have 3 channels!"); + } + lengthPerChannel /= 3; - int lutLength = 0; - if (dimension == LutProperties.ONE_DIMENSION) { - lutLength = size; - } else if (dimension == LutProperties.THREE_DIMENSION) { - lutLength = size * size * size; - } else { - clear(); - throw new IllegalArgumentException("The dimension is either 1D or 3D!"); + double size = Math.cbrt(lengthPerChannel); + if (size == (int) size) { + mSize = (int) size; + } else { + throw new IllegalArgumentException( + "Cannot get the cube root of the 3d lut buffer!"); + } + } else { + mSize = buffer.length; + } + + mBuffer = buffer; + mDimension = dimension; + mSamplingKey = samplingKey; } - if (lutLength >= LUT_LENGTH_LIMIT) { - clear(); - throw new IllegalArgumentException("The lut length is too big to handle!"); + /** + * @return the dimension of the lut entry + */ + @FlaggedApi(Flags.FLAG_LUTS_API) + public int getDimension() { + return mDimension; } - mOffsets.add(mTotalLength); - mTotalLength += lutLength; + /** + * @return the size of the lut for each dimension + * @hide + */ + public int getSize() { + return mSize; + } + + /** + * @return the lut raw data of the lut + */ + @FlaggedApi(Flags.FLAG_LUTS_API) + public @NonNull float[] getBuffer() { + return mBuffer; + } + + /** + * @return the sampling key used by the lut + */ + @FlaggedApi(Flags.FLAG_LUTS_API) + public int getSamplingKey() { + return mSamplingKey; + } + + @Override + public String toString() { + return "Entry{" + + "dimension=" + DisplayLuts.Entry.dimensionToString(getDimension()) + + ", size(each dimension)=" + getSize() + + ", samplingKey=" + samplingKeyToString(getSamplingKey()) + "}"; + } + + private static String dimensionToString(int dimension) { + switch(dimension) { + case LutProperties.ONE_DIMENSION: + return "ONE_DIMENSION"; + case LutProperties.THREE_DIMENSION: + return "THREE_DIMENSION"; + default: + return ""; + } + } + + private static String samplingKeyToString(int key) { + switch(key) { + case LutProperties.SAMPLING_KEY_RGB: + return "SAMPLING_KEY_RGB"; + case LutProperties.SAMPLING_KEY_MAX_RGB: + return "SAMPLING_KEY_MAX_RGB"; + default: + return ""; + } + } + } - mLutBuffers.add(buffer); - mLutDimensions.add(dimension); - mLutSizes.add(size); - mLutSamplingKeys.add(samplingKey); + @Override + public String toString() { + StringBuilder sb = new StringBuilder("DisplayLuts{"); + sb.append("\n"); + for (DisplayLuts.Entry entry: mEntries) { + sb.append(entry.toString()); + sb.append("\n"); + } + sb.append("}"); + return sb.toString(); + } + + private void addEntry(Entry entry) { + mEntries.add(entry); + mOffsets.add(mTotalLength); + mTotalLength += entry.getBuffer().length; } private void clear() { - mTotalLength = 0; mOffsets.clear(); - mLutBuffers.clear(); - mLutDimensions.clear(); - mLutSamplingKeys.clear(); + mTotalLength = 0; + mEntries.clear(); + } + + /** + * Set a Lut to be applied. + * + * <p>Use either this or {@link #set(Entry, Entry)}. The function will + * replace any previously set lut(s).</p> + * + * @param entry Either an 1D Lut or a 3D Lut + */ + @FlaggedApi(Flags.FLAG_LUTS_API) + public void set(@NonNull Entry entry) { + if (entry == null) { + throw new IllegalArgumentException("The entry is null!"); + } + clear(); + addEntry(entry); + } + + /** + * Set Luts in order to be applied. + * + * <p> An 1D Lut and 3D Lut will be applied in order. Use either this or + * {@link #set(Entry)}. The function will replace any previously set lut(s)</p> + * + * @param first An 1D Lut + * @param second A 3D Lut + */ + @FlaggedApi(Flags.FLAG_LUTS_API) + public void set(@NonNull Entry first, @NonNull Entry second) { + if (first == null || second == null) { + throw new IllegalArgumentException("The entry is null!"); + } + if (first.getDimension() != LutProperties.ONE_DIMENSION + || second.getDimension() != LutProperties.THREE_DIMENSION) { + throw new IllegalArgumentException("The entries should be 1D and 3D in order!"); + } + clear(); + addEntry(first); + addEntry(second); } /** - * @return the array of Lut buffers + * @hide + */ + public boolean valid() { + return mEntries.size() > 0; + } + + /** + * @hide */ public float[] getLutBuffers() { float[] buffer = new float[mTotalLength]; - for (int i = 0; i < mLutBuffers.size(); i++) { - float[] lutBuffer = mLutBuffers.get(i); + for (int i = 0; i < mEntries.size(); i++) { + float[] lutBuffer = mEntries.get(i).getBuffer(); System.arraycopy(lutBuffer, 0, buffer, mOffsets.get(i), lutBuffer.length); } return buffer; } /** - * @return the starting point of each lut memory region of the lut buffer + * @hide */ public int[] getOffsets() { return mOffsets.toArray(); } /** - * @return the array of Lut size + * @hide */ public int[] getLutSizes() { - return mLutSizes.toArray(); + int[] sizes = new int[mEntries.size()]; + for (int i = 0; i < mEntries.size(); i++) { + sizes[i] = mEntries.get(i).getSize(); + } + return sizes; } /** - * @return the array of Lut dimension + * @hide */ public int[] getLutDimensions() { - return mLutDimensions.toArray(); + int[] dimensions = new int[mEntries.size()]; + for (int i = 0; i < mEntries.size(); i++) { + dimensions[i] = mEntries.get(i).getDimension(); + } + return dimensions; } /** - * @return the array of sampling key + * @hide */ public int[] getLutSamplingKeys() { - return mLutSamplingKeys.toArray(); + int[] samplingKeys = new int[mEntries.size()]; + for (int i = 0; i < mEntries.size(); i++) { + samplingKeys[i] = mEntries.get(i).getSamplingKey(); + } + return samplingKeys; } } diff --git a/core/java/android/hardware/LutProperties.java b/core/java/android/hardware/LutProperties.java index c9c6d6d08ed2..bf40a415b0f7 100644 --- a/core/java/android/hardware/LutProperties.java +++ b/core/java/android/hardware/LutProperties.java @@ -16,23 +16,31 @@ package android.hardware; +import android.annotation.FlaggedApi; import android.annotation.IntDef; +import android.annotation.NonNull; +import android.hardware.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Lut properties class. + * Provides Lut properties of the device. * - * A Lut (Look-Up Table) is a pre-calculated table for color transformation. - * - * @hide + * <p> + * A Lut (Look-Up Table) is a pre-calculated table for color correction. + * Applications may be interested in the Lut properties exposed by + * this class to determine if the Lut(s) they select using + * {@link android.view.SurfaceControl.Transaction#setLuts} are by the HWC. + * </p> */ +@FlaggedApi(Flags.FLAG_LUTS_API) public final class LutProperties { private final @Dimension int mDimension; private final int mSize; private final @SamplingKey int[] mSamplingKeys; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"SAMPLING_KEY_"}, value = { SAMPLING_KEY_RGB, @@ -42,11 +50,14 @@ public final class LutProperties { } /** use r,g,b channel as the gain value of a Lut */ + @FlaggedApi(Flags.FLAG_LUTS_API) public static final int SAMPLING_KEY_RGB = 0; /** use max of r,g,b channel as the gain value of a Lut */ + @FlaggedApi(Flags.FLAG_LUTS_API) public static final int SAMPLING_KEY_MAX_RGB = 1; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = { ONE_DIMENSION, @@ -56,18 +67,22 @@ public final class LutProperties { } /** The Lut is one dimensional */ + @FlaggedApi(Flags.FLAG_LUTS_API) public static final int ONE_DIMENSION = 1; /** The Lut is three dimensional */ + @FlaggedApi(Flags.FLAG_LUTS_API) public static final int THREE_DIMENSION = 3; + @FlaggedApi(Flags.FLAG_LUTS_API) public @Dimension int getDimension() { return mDimension; } /** - * @return the size of the Lut. + * @return the size of the Lut for each dimension */ + @FlaggedApi(Flags.FLAG_LUTS_API) public int getSize() { return mSize; } @@ -75,6 +90,8 @@ public final class LutProperties { /** * @return the list of sampling keys */ + @FlaggedApi(Flags.FLAG_LUTS_API) + @NonNull public @SamplingKey int[] getSamplingKeys() { if (mSamplingKeys.length == 0) { throw new IllegalStateException("no sampling key!"); diff --git a/core/java/android/hardware/OverlayProperties.java b/core/java/android/hardware/OverlayProperties.java index 24cfc1b53e00..d42bfae23d2b 100644 --- a/core/java/android/hardware/OverlayProperties.java +++ b/core/java/android/hardware/OverlayProperties.java @@ -18,6 +18,7 @@ package android.hardware; import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.SuppressLint; import android.hardware.flags.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -72,9 +73,11 @@ public final class OverlayProperties implements Parcelable { } /** - * Gets the lut properties of the display. - * @hide + * Returns the lut properties of the device. */ + @FlaggedApi(Flags.FLAG_LUTS_API) + @SuppressLint("ArrayReturn") + @NonNull public LutProperties[] getLutProperties() { if (mNativeObject == 0) { return null; diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 58e524e741b3..5533a640b9d8 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -19,9 +19,9 @@ package android.hardware.camera2; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.impl.ExtensionKey; import android.hardware.camera2.impl.PublicKey; import android.hardware.camera2.impl.SyntheticKey; import android.hardware.camera2.params.DeviceStateSensorOrientationMap; @@ -6172,6 +6172,66 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> JPEGR_AVAILABLE_JPEG_R_STALL_DURATIONS_MAXIMUM_RESOLUTION = new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.jpegr.availableJpegRStallDurationsMaximumResolution", android.hardware.camera2.params.StreamConfigurationDuration[].class); + /** + * <p>Color space used for shared session configuration for all the output targets + * when camera is opened in shared mode. This should be one of the values specified in + * availableColorSpaceProfilesMap.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #SHARED_SESSION_COLOR_SPACE_UNSPECIFIED UNSPECIFIED}</li> + * <li>{@link #SHARED_SESSION_COLOR_SPACE_SRGB SRGB}</li> + * <li>{@link #SHARED_SESSION_COLOR_SPACE_DISPLAY_P3 DISPLAY_P3}</li> + * <li>{@link #SHARED_SESSION_COLOR_SPACE_BT2020_HLG BT2020_HLG}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * @see #SHARED_SESSION_COLOR_SPACE_UNSPECIFIED + * @see #SHARED_SESSION_COLOR_SPACE_SRGB + * @see #SHARED_SESSION_COLOR_SPACE_DISPLAY_P3 + * @see #SHARED_SESSION_COLOR_SPACE_BT2020_HLG + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final Key<Integer> SHARED_SESSION_COLOR_SPACE = + new Key<Integer>("android.sharedSession.colorSpace", int.class); + + /** + * <p>List of shared output configurations that this camera device supports when + * camera is opened in shared mode. Array contains following entries for each supported + * shared configuration: + * 1) surface type + * 2) width + * 3) height + * 4) format + * 5) mirrorMode + * 6) useReadoutTimestamp + * 7) timestampBase + * 8) dataspace + * 9) usage + * 10) streamUsecase + * 11) physical camera id len + * 12) physical camera id as UTF-8 null terminated string.</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final Key<long[]> SHARED_SESSION_OUTPUT_CONFIGURATIONS = + new Key<long[]>("android.sharedSession.outputConfigurations", long[].class); + + /** + * <p>The available stream configurations that this camera device supports for + * shared capture session when camera is opened in shared mode. Android camera framework + * will generate this tag if the camera device can be opened in shared mode.</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * @hide + */ + @SystemApi + @NonNull + @SyntheticKey + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final Key<android.hardware.camera2.params.SharedSessionConfiguration> SHARED_SESSION_CONFIGURATION = + new Key<android.hardware.camera2.params.SharedSessionConfiguration>("android.sharedSession.configuration", android.hardware.camera2.params.SharedSessionConfiguration.class); + /** * Mapping from INFO_SESSION_CONFIGURATION_QUERY_VERSION to session characteristics key. diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index fb381d97adc3..852f04793f15 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -446,6 +446,17 @@ public abstract class CameraDevice implements AutoCloseable { public static final int SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED = 1; // ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE; + /** + * Shared camera operation mode. + * + * @see #CameraSharedCaptureSession + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final int SESSION_OPERATION_MODE_SHARED = + 2; // ICameraDeviceUser.SHARED_MODE; + /** * First vendor-specific operating mode * @@ -461,6 +472,7 @@ public abstract class CameraDevice implements AutoCloseable { @IntDef(prefix = {"SESSION_OPERATION_MODE"}, value = {SESSION_OPERATION_MODE_NORMAL, SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED, + SESSION_OPERATION_MODE_SHARED, SESSION_OPERATION_MODE_VENDOR_START}) public @interface SessionOperatingMode {}; @@ -1240,7 +1252,6 @@ public abstract class CameraDevice implements AutoCloseable { * * </ul> * - * * @param config A session configuration (see {@link SessionConfiguration}). * * @throws IllegalArgumentException In case the session configuration is invalid; or the output @@ -1559,6 +1570,48 @@ public abstract class CameraDevice implements AutoCloseable { public abstract void onOpened(@NonNull CameraDevice camera); // Must implement /** + * The method called when a camera device has finished opening in shared mode, + * where there can be more than one client accessing the same camera. + * + * <p>At this point, the camera device is ready to use, and + * {@link CameraDevice#createCaptureSession} can be called to set up the shared capture + * session.</p> + * + * @param camera the camera device that has become opened + * @param isPrimaryClient true if the client opening the camera is currently the primary + * client. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public void onOpenedInSharedMode(@NonNull CameraDevice camera, boolean isPrimaryClient) { + // Default empty implementation + } + + /** + * The method called when client access priorities have changed for a camera device opened + * in shared mode where there can be more than one client accessing the same camera. + * + * If the client priority changed from secondary to primary, then it can now + * create capture request and change the capture request parameters. If client priority + * changed from primary to secondary, that implies that a higher priority client has also + * opened the camera in shared mode and the new client is now a primary client + * + * @param camera the camera device whose access priorities have changed. + * @param isPrimaryClient true if the client is now the primary client. + * false if another higher priority client also opened the + * camera and is now the new primary client and this client is + * now a secondary client. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public void onClientSharedAccessPriorityChanged(@NonNull CameraDevice camera, + boolean isPrimaryClient) { + // Default empty implementation + } + + /** * The method called when a camera device has been closed with * {@link CameraDevice#close}. * diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 75e20582b7b4..266efb7b759c 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -976,6 +976,46 @@ public final class CameraManager { } /** + * Checks if a camera device can be opened in a shared mode for a given {@code cameraId}. + * If this method returns false for a {@code cameraId}, calling {@link #openSharedCamera} + * for that {@code cameraId} will throw an {@link UnsupportedOperationException}. + * + * @param cameraId The unique identifier of the camera device for which sharing support is + * being queried. This identifier must be present in + * {@link #getCameraIdList()}. + * + * @return {@code true} if camera can be opened in shared mode + * for the provided {@code cameraId}; {@code false} otherwise. + * + * @throws IllegalArgumentException If {@code cameraId} is null, or if {@code cameraId} does not + * match any device in {@link #getCameraIdList()}. + * @throws CameraAccessException if the camera device has been disconnected. + * + * @see #getCameraIdList() + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + @SystemApi + public boolean isCameraDeviceSharingSupported(@NonNull String cameraId) + throws CameraAccessException { + if (cameraId == null) { + throw new IllegalArgumentException("Camera ID was null"); + } + + if (CameraManagerGlobal.sCameraServiceDisabled + || !Arrays.asList(CameraManagerGlobal.get().getCameraIdList(mContext.getDeviceId(), + getDevicePolicyFromContext(mContext))).contains(cameraId)) { + throw new IllegalArgumentException( + "Camera ID '" + cameraId + "' not available on device."); + } + + CameraCharacteristics chars = getCameraCharacteristics(cameraId); + long[] sharedOutputConfiguration = + chars.get(CameraCharacteristics.SHARED_SESSION_OUTPUT_CONFIGURATIONS); + return (sharedOutputConfiguration != null); + } + + /** * Retrieves the AttributionSourceState to pass to the CameraService. * * @param deviceIdOverride An override of the AttributionSource's deviceId, if not equal to @@ -1036,6 +1076,9 @@ public final class CameraManager { * @param cameraId The unique identifier of the camera device to open * @param callback The callback for the camera. Must not be null. * @param executor The executor to invoke the callback with. Must not be null. + * @param oomScoreOffset The minimum oom score that cameraservice must see for this client. + * @param rotationOverride The type of rotation override. + * @param sharedMode Parameter specifying if the camera should be opened in shared mode. * * @throws CameraAccessException if the camera is disabled by device policy, * too many camera devices are already open, or the cameraId does not match @@ -1051,7 +1094,8 @@ public final class CameraManager { */ private CameraDevice openCameraDeviceUserAsync(String cameraId, CameraDevice.StateCallback callback, Executor executor, - final int oomScoreOffset, int rotationOverride) throws CameraAccessException { + final int oomScoreOffset, int rotationOverride, boolean sharedMode) + throws CameraAccessException { CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); CameraDevice device = null; synchronized (mLock) { @@ -1070,7 +1114,7 @@ public final class CameraManager { characteristics, this, mContext.getApplicationInfo().targetSdkVersion, - mContext, cameraDeviceSetup); + mContext, cameraDeviceSetup, sharedMode); ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks(); try { @@ -1091,7 +1135,7 @@ public final class CameraManager { mContext.getApplicationInfo().targetSdkVersion, rotationOverride, clientAttribution, - getDevicePolicyFromContext(mContext)); + getDevicePolicyFromContext(mContext), sharedMode); } catch (ServiceSpecificException e) { if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) { throw new AssertionError("Should've gone down the shim path"); @@ -1218,7 +1262,8 @@ public final class CameraManager { @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler) throws CameraAccessException { - openCameraImpl(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler)); + openCameraImpl(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler), + /*oomScoreOffset*/0, getRotationOverride(mContext), /*sharedMode*/false); } /** @@ -1258,7 +1303,7 @@ public final class CameraManager { /*oomScoreOffset*/0, overrideToPortrait ? ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT - : ICameraService.ROTATION_OVERRIDE_NONE); + : ICameraService.ROTATION_OVERRIDE_NONE, /*sharedMode*/false); } /** @@ -1303,9 +1348,56 @@ public final class CameraManager { if (executor == null) { throw new IllegalArgumentException("executor was null"); } - openCameraImpl(cameraId, callback, executor); + openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0, + getRotationOverride(mContext), /*sharedMode*/false); + } + + /** + * Opens a shared connection to a camera with the given ID. + * + * <p>The behavior of this method matches that of + * {@link #openCamera(String, Executor, StateCallback)}, except that it opens the camera in + * shared mode where more than one client can access the camera at the same time.</p> + * + * @param cameraId The unique identifier of the camera device to open. + * @param executor The executor which will be used when invoking the callback. + * @param callback The callback which is invoked once the camera is opened + * + * @throws CameraAccessException if the camera is disabled by device policy, or is being used + * by a higher-priority client in non-shared mode or the device + * has reached its maximal resource and cannot open this camera + * device. + * + * @throws IllegalArgumentException if cameraId, the callback or the executor was null, + * or the cameraId does not match any currently or previously + * available camera device. + * + * @throws SecurityException if the application does not have permission to + * access the camera + * + * @see #getCameraIdList + * @see android.app.admin.DevicePolicyManager#setCameraDisabled + * + * @hide + */ + @SystemApi + @RequiresPermission(allOf = { + android.Manifest.permission.SYSTEM_CAMERA, + android.Manifest.permission.CAMERA, + }) + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public void openSharedCamera(@NonNull String cameraId, + @NonNull @CallbackExecutor Executor executor, + @NonNull final CameraDevice.StateCallback callback) + throws CameraAccessException { + if (executor == null) { + throw new IllegalArgumentException("executor was null"); + } + openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0, + getRotationOverride(mContext), /*sharedMode*/true); } + /** * Open a connection to a camera with the given ID. Also specify what oom score must be offset * by cameraserver for this client. This api can be useful for system @@ -1372,29 +1464,35 @@ public final class CameraManager { "oomScoreOffset < 0, cannot increase priority of camera client"); } openCameraImpl(cameraId, callback, executor, oomScoreOffset, - getRotationOverride(mContext)); + getRotationOverride(mContext), /*sharedMode*/false); } /** * Open a connection to a camera with the given ID, on behalf of another application. - * Also specify the minimum oom score and process state the application - * should have, as seen by the cameraserver. - * - * <p>The behavior of this method matches that of {@link #openCamera}, except that it allows - * the caller to specify the UID to use for permission/etc verification. This can only be - * done by services trusted by the camera subsystem to act on behalf of applications and - * to forward the real UID.</p> * + * @param cameraId + * The unique identifier of the camera device to open + * @param callback + * The callback which is invoked once the camera is opened + * @param executor + * The executor which will be used when invoking the callback. * @param oomScoreOffset * The minimum oom score that cameraservice must see for this client. * @param rotationOverride * The type of rotation override (none, override_to_portrait, rotation_only) * that should be followed for this camera id connection + * @param sharedMode + * Parameter specifying if the camera should be opened in shared mode. + * + * @throws CameraAccessException if the camera is disabled by device policy, + * has been disconnected, or is being used by a higher-priority camera API client in + * non shared mode. + * * @hide */ public void openCameraImpl(@NonNull String cameraId, @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor, - int oomScoreOffset, int rotationOverride) + int oomScoreOffset, int rotationOverride, boolean sharedMode) throws CameraAccessException { if (cameraId == null) { @@ -1407,24 +1505,7 @@ public final class CameraManager { } openCameraDeviceUserAsync(cameraId, callback, executor, oomScoreOffset, - rotationOverride); - } - - /** - * Open a connection to a camera with the given ID, on behalf of another application. - * - * <p>The behavior of this method matches that of {@link #openCamera}, except that it allows - * the caller to specify the UID to use for permission/etc verification. This can only be - * done by services trusted by the camera subsystem to act on behalf of applications and - * to forward the real UID.</p> - * - * @hide - */ - public void openCameraImpl(@NonNull String cameraId, - @NonNull final CameraDevice.StateCallback callback, @NonNull Executor executor) - throws CameraAccessException { - openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0, - getRotationOverride(mContext)); + rotationOverride, sharedMode); } /** @@ -2541,6 +2622,10 @@ public final class CameraManager { } @Override public void onCameraClosed(String id, int deviceId) { + } + @Override + public void onCameraOpenedInSharedMode(String id, String clientPackageId, + int deviceId, boolean primaryClient) { }}; String[] cameraIds; @@ -3325,6 +3410,11 @@ public final class CameraManager { } @Override + public void onCameraOpenedInSharedMode(String cameraId, String clientPackageId, + int deviceId, boolean primaryClient) { + } + + @Override public void onCameraOpened(String cameraId, String clientPackageId, int deviceId) { synchronized (mLock) { onCameraOpenedLocked(new DeviceCameraInfo(cameraId, deviceId), clientPackageId); diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 8d36fbdc8b10..d2fcfd62bfca 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -2121,6 +2121,38 @@ public abstract class CameraMetadata<TKey> { public static final int AUTOMOTIVE_LOCATION_EXTRA_RIGHT = 10; // + // Enumeration values for CameraCharacteristics#SHARED_SESSION_COLOR_SPACE + // + + /** + * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final int SHARED_SESSION_COLOR_SPACE_UNSPECIFIED = -1; + + /** + * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final int SHARED_SESSION_COLOR_SPACE_SRGB = 0; + + /** + * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final int SHARED_SESSION_COLOR_SPACE_DISPLAY_P3 = 7; + + /** + * @see CameraCharacteristics#SHARED_SESSION_COLOR_SPACE + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public static final int SHARED_SESSION_COLOR_SPACE_BT2020_HLG = 16; + + // // Enumeration values for CaptureRequest#COLOR_CORRECTION_MODE // diff --git a/core/java/android/hardware/camera2/CameraSharedCaptureSession.java b/core/java/android/hardware/camera2/CameraSharedCaptureSession.java new file mode 100644 index 000000000000..5426d4d10bec --- /dev/null +++ b/core/java/android/hardware/camera2/CameraSharedCaptureSession.java @@ -0,0 +1,180 @@ +/* + * 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 android.hardware.camera2; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.view.Surface; + +import com.android.internal.camera.flags.Flags; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * A shared capture session for a {@link CameraDevice}, when a camera device is opened in shared + * mode possibly by multiple clients at the same time. + * + * <p>An active shared capture session is a special type of capture session used exclusively + * for shared camera access by multiple applications, provided the camera device supports this + * mode. To determine if a camera device supports shared mode, use the + * {@link android.hardware.camera2.CameraManager#isCameraDeviceSharingSupported} API. + * If supported, multiple clients can open the camera by calling + * {@link android.hardware.camera2.CameraManager#openSharedCamera} and create a shared capture + * session by calling {@link CameraDevice#createCaptureSession(SessionConfiguration)} and using + * session type as {@link android.hardware.camera2.params.SessionConfiguration#SESSION_SHARED}</p> + * + * <p>When an application has opened a camera device in shared mode, it can only create a shared + * capture session using session type as + * {@link android.hardware.camera2.params.SessionConfiguration#SESSION_SHARED}. Any other session + * type value will trigger {@link IllegalArgumentException}. Once the configuration is complete and + * the session is ready to actually capture data, the provided + * {@link CameraCaptureSession.StateCallback}'s + * {@link CameraCaptureSession.StateCallback#onConfigured} callback will be called and will + * receive a CameraCaptureSession (castable to {@link CameraSharedCaptureSession}).</p> + * + * <p>Shared capture sessions uses a predefined configuration detailed in + * {@link CameraCharacteristics#SHARED_SESSION_CONFIGURATION}. Using different configuration values + * when creating session will result in an {@link IllegalArgumentException}.</p> + * + * <p>When camera is opened in shared mode, the highest priority client among all the clients will + * be the primary client while the others would be secondary clients. Clients will know if they are + * primary or secondary by the device state callback + * {@link CameraDevice.StateCallback#onOpenedInSharedMode}. Once the camera has been opened in + * shared mode, their access priorities of being a primary or secondary client can change if + * another higher priority client opens the camera later. Once the camera has been opened, + * any change in primary client status will be shared by the device state callback + * {@link CameraDevice.StateCallback#onClientSharedAccessPriorityChanged}.</p> + * + * <p>The priority of client access is determined by considering two factors: its current process + * state and its "out of memory" score. Clients operating in the background are assigned a lower + * priority. In contrast, clients running in the foreground, along with system-level clients, are + * given a higher priority.</p> + * + * <p>Primary clients can create capture requests, modify any capture parameters and send them to + * the capture session for a one-shot capture or as a repeating request using the following apis: + * </p> + * + * <ul> + * + * <li>{@link CameraSharedCaptureSession#capture}</li> + * + * <li>{@link CameraSharedCaptureSession#captureSingleRequest}</li> + * + * <li>{@link CameraSharedCaptureSession#setRepeatingRequest}</li> + * + * <li>{@link CameraSharedCaptureSession#setSingleRepeatingRequest}</li> + * + * <li>{@link CameraSharedCaptureSession#stopRepeating}</li> + * + * </ul> + * + * <p>Secondary clients cannot create a capture request and modify any capture parameters. However, + * they can start the camera streaming to desired surface targets using + * {@link CameraSharedCaptureSession#startStreaming}, which will apply default parameters. Once the + * streaming has successfully started, then they can stop the streaming using + * {@link CameraSharedCaptureSession#stopStreaming}.</p> + * + * <p>The following APIs are not supported in shared capture sessions by either the primary or + * secondary client.</p> + * + * <ul> + * + * <li>{@link CameraSharedCaptureSession#captureBurst}</li> + * + * <li>{@link CameraSharedCaptureSession#captureBurstRequests}</li> + * + * <li>{@link CameraSharedCaptureSession#setRepeatingBurst}</li> + * + * <li>{@link CameraSharedCaptureSession#setRepeatingBurstRequests}</li> + * + * <li>{@link CameraSharedCaptureSession#switchToOffline}</li> + * + * <li>{@link CameraSharedCaptureSession#updateOutputConfiguration}</li> + * + * <li>{@link CameraSharedCaptureSession#finalizeOutputConfigurations}</li> + * + * <li>{@link CameraSharedCaptureSession#prepare}</li> + * + * </ul> + * + * @hide + */ +@FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) +@SystemApi +public abstract class CameraSharedCaptureSession extends CameraCaptureSession { + + /** + * Request start of the streaming of camera images by this shared capture session. + * + * <p>With this method, the camera device will continually capture images + * using the settings provided by primary client if there is ongoing repeating request + * by the primary client or default settings if no ongoing streaming request in progress.</p> + * + * <p> startStreaming has lower priority than the capture requests submitted + * through {@link #capture} by primary client, so if {@link #capture} is called when a + * streaming is active, the capture request will be processed before any further + * streaming requests are processed.</p> + * + * <p>To stop the streaming, call {@link #stopStreaming}</p> + * + * <p>Calling this method will replace any earlier streaming set up by this method.</p> + * + * @param surfaces List of target surfaces to use for streaming. + * @param executor The executor which will be used for invoking the listener. + * @param listener The callback object to notify the status and progress of the image capture. + * + * @return int A unique capture sequence ID used by + * {@link CaptureCallback#onCaptureSequenceCompleted}. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. + * @throws IllegalArgumentException If the request references no surfaces or references surfaces + * that are not currently configured as outputs; or + * the executor is null, or the listener is null. + * @see #stopStreaming + * + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + @SystemApi + public abstract int startStreaming(@NonNull List<Surface> surfaces, + @NonNull @CallbackExecutor Executor executor, @NonNull CaptureCallback listener) + throws CameraAccessException; + + /** + * <p>Cancel any ongoing streaming started by {@link #startStreaming}</p> + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because the session + * was explicitly closed, a new session has been created + * or the camera device has been closed. + * + * @see #startStreaming + * + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + @SystemApi + public abstract void stopStreaming() throws CameraAccessException; +} diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 84072585d7f0..ea70abb55b48 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -18,6 +18,7 @@ package android.hardware.camera2.impl; import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.compat.CompatChanges; @@ -41,12 +42,15 @@ import android.hardware.camera2.ICameraDeviceCallbacks; import android.hardware.camera2.ICameraDeviceUser; import android.hardware.camera2.ICameraOfflineSession; import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.DynamicRangeProfiles; import android.hardware.camera2.params.ExtensionSessionConfiguration; import android.hardware.camera2.params.InputConfiguration; import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap; import android.hardware.camera2.params.MultiResolutionStreamInfo; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.params.SharedSessionConfiguration; +import android.hardware.camera2.params.SharedSessionConfiguration.SharedOutputConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.hardware.camera2.utils.SubmitInfo; import android.hardware.camera2.utils.SurfaceUtils; @@ -188,6 +192,8 @@ public class CameraDeviceImpl extends CameraDevice private ExecutorService mOfflineSwitchService; private CameraOfflineSessionImpl mOfflineSessionImpl; + private boolean mSharedMode; + private boolean mIsPrimaryClient; // Runnables for all state transitions, except error, which needs the // error code argument @@ -208,6 +214,25 @@ public class CameraDeviceImpl extends CameraDevice } }; + private final Runnable mCallOnOpenedInSharedMode = new Runnable() { + @Override + public void run() { + if (!Flags.cameraMultiClient()) { + return; + } + StateCallbackKK sessionCallback = null; + synchronized (mInterfaceLock) { + if (mRemoteDevice == null) return; // Camera already closed + + sessionCallback = mSessionStateCallback; + } + if (sessionCallback != null) { + sessionCallback.onOpenedInSharedMode(CameraDeviceImpl.this, mIsPrimaryClient); + } + mDeviceCallback.onOpenedInSharedMode(CameraDeviceImpl.this, mIsPrimaryClient); + } + }; + private final Runnable mCallOnUnconfigured = new Runnable() { @Override public void run() { @@ -322,6 +347,32 @@ public class CameraDeviceImpl extends CameraDevice } }); } + + public void onOpenedInSharedMode(@NonNull CameraDevice camera, boolean primaryClient) { + if (!Flags.cameraMultiClient()) { + return; + } + mClientExecutor.execute(new Runnable() { + @Override + public void run() { + mClientStateCallback.onOpenedInSharedMode(camera, primaryClient); + } + }); + } + + public void onClientSharedAccessPriorityChanged(@NonNull CameraDevice camera, + boolean primaryClient) { + if (!Flags.cameraMultiClient()) { + return; + } + mClientExecutor.execute(new Runnable() { + @Override + public void run() { + mClientStateCallback.onClientSharedAccessPriorityChanged(camera, primaryClient); + } + }); + } + @Override public void onOpened(@NonNull CameraDevice camera) { mClientExecutor.execute(new Runnable() { @@ -358,7 +409,8 @@ public class CameraDeviceImpl extends CameraDevice @NonNull CameraManager manager, int appTargetSdkVersion, Context ctx, - @Nullable CameraDevice.CameraDeviceSetup cameraDeviceSetup) { + @Nullable CameraDevice.CameraDeviceSetup cameraDeviceSetup, + boolean sharedMode) { if (cameraId == null || callback == null || executor == null || characteristics == null || manager == null) { throw new IllegalArgumentException("Null argument given"); @@ -375,6 +427,7 @@ public class CameraDeviceImpl extends CameraDevice mAppTargetSdkVersion = appTargetSdkVersion; mContext = ctx; mCameraDeviceSetup = cameraDeviceSetup; + mSharedMode = sharedMode; final int MAX_TAG_LEN = 23; String tag = String.format("CameraDevice-JV-%s", mCameraId); @@ -438,7 +491,12 @@ public class CameraDeviceImpl extends CameraDevice } } - mDeviceExecutor.execute(mCallOnOpened); + if (Flags.cameraMultiClient() && mSharedMode) { + mIsPrimaryClient = mRemoteDevice.isPrimaryClient(); + mDeviceExecutor.execute(mCallOnOpenedInSharedMode); + } else { + mDeviceExecutor.execute(mCallOnOpened); + } mDeviceExecutor.execute(mCallOnUnconfigured); mRemoteDeviceInit = true; @@ -576,7 +634,11 @@ public class CameraDeviceImpl extends CameraDevice stopRepeating(); try { - waitUntilIdle(); + // if device is opened in shared mode, there can be multiple clients accessing the + // camera device. So do not wait for idle if the device is opened in shared mode. + if (!mSharedMode) { + waitUntilIdle(); + } mRemoteDevice.beginConfigure(); @@ -764,6 +826,54 @@ public class CameraDeviceImpl extends CameraDevice checkAndWrapHandler(handler), operatingMode, /*sessionParams*/ null); } + private boolean checkSharedOutputConfiguration(OutputConfiguration outConfig) { + if (!Flags.cameraMultiClient()) { + return false; + } + SharedSessionConfiguration sharedSessionConfiguration = + mCharacteristics.get(CameraCharacteristics.SHARED_SESSION_CONFIGURATION); + if (sharedSessionConfiguration == null) { + return false; + } + + List<SharedOutputConfiguration> sharedConfigs = + sharedSessionConfiguration.getOutputStreamsInformation(); + for (SharedOutputConfiguration sharedConfig : sharedConfigs) { + if (outConfig.getConfiguredSize().equals(sharedConfig.getSize()) + && (outConfig.getConfiguredFormat() == sharedConfig.getFormat()) + && (outConfig.getSurfaceGroupId() == OutputConfiguration.SURFACE_GROUP_ID_NONE) + && (outConfig.getSurfaceType() == sharedConfig.getSurfaceType()) + && (outConfig.getMirrorMode() == sharedConfig.getMirrorMode()) + && (outConfig.getUsage() == sharedConfig.getUsage()) + && (outConfig.isReadoutTimestampEnabled() + == sharedConfig.isReadoutTimestampEnabled()) + && (outConfig.getTimestampBase() == sharedConfig.getTimestampBase()) + && (outConfig.getStreamUseCase() == sharedConfig.getStreamUseCase()) + && (outConfig.getColorSpace().equals( + sharedSessionConfiguration.getColorSpace())) + && (outConfig.getDynamicRangeProfile() + == DynamicRangeProfiles.STANDARD) + && (outConfig.getConfiguredDataspace() == sharedConfig.getDataspace()) + && (Objects.equals(outConfig.getPhysicalCameraId(), + sharedConfig.getPhysicalCameraId())) + && (outConfig.getSensorPixelModes().isEmpty()) + && (!outConfig.isShared())) { + //Found valid config, return true + return true; + } + } + return false; + } + + private boolean checkSharedSessionConfiguration(List<OutputConfiguration> outputConfigs) { + for (OutputConfiguration out : outputConfigs) { + if (!checkSharedOutputConfiguration(out)) { + return false; + } + } + return true; + } + @Override public void createCaptureSession(SessionConfiguration config) throws CameraAccessException { @@ -778,6 +888,14 @@ public class CameraDeviceImpl extends CameraDevice if (config.getExecutor() == null) { throw new IllegalArgumentException("Invalid executor"); } + if (mSharedMode) { + if (config.getSessionType() != SessionConfiguration.SESSION_SHARED) { + throw new IllegalArgumentException("Invalid session type"); + } + if (!checkSharedSessionConfiguration(outputConfigs)) { + throw new IllegalArgumentException("Invalid output configurations"); + } + } createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs, config.getStateCallback(), config.getExecutor(), config.getSessionType(), config.getSessionParameters()); @@ -801,6 +919,11 @@ public class CameraDeviceImpl extends CameraDevice throw new IllegalArgumentException("Constrained high speed session doesn't support" + " input configuration yet."); } + boolean isSharedSession = (operatingMode == ICameraDeviceUser.SHARED_MODE); + if (isSharedSession && inputConfig != null) { + throw new IllegalArgumentException("Shared capture session doesn't support" + + " input configuration yet."); + } if (mCurrentExtensionSession != null) { mCurrentExtensionSession.commitStats(); @@ -860,6 +983,10 @@ public class CameraDeviceImpl extends CameraDevice newSession = new CameraConstrainedHighSpeedCaptureSessionImpl(mNextSessionId++, callback, executor, this, mDeviceExecutor, configureSuccess, mCharacteristics); + } else if (isSharedSession) { + newSession = new CameraSharedCaptureSessionImpl(mNextSessionId++, + callback, executor, this, mDeviceExecutor, configureSuccess, + mIsPrimaryClient); } else { newSession = new CameraCaptureSessionImpl(mNextSessionId++, input, callback, executor, this, mDeviceExecutor, configureSuccess); @@ -1882,6 +2009,40 @@ public class CameraDeviceImpl extends CameraDevice } } + /** + * Callback when client access priorities change when camera is opened in shared mode. + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public void onClientSharedAccessPriorityChanged(boolean primaryClient) { + if (DEBUG) { + Log.d(TAG, String.format( + "onClientSharedAccessPriorityChanged received, primary client = " + + primaryClient)); + } + synchronized (mInterfaceLock) { + if (mRemoteDevice == null && mRemoteDeviceInit) { + return; // Camera already closed, user is not interested in this callback anymore. + } + final long ident = Binder.clearCallingIdentity(); + try { + mDeviceExecutor.execute(obtainRunnable( + CameraDeviceImpl::notifyClientSharedAccessPriorityChanged, this, + primaryClient).recycleOnUse()); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } + + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + private void notifyClientSharedAccessPriorityChanged(boolean primaryClient) { + if (!CameraDeviceImpl.this.isClosed()) { + mIsPrimaryClient = primaryClient; + mDeviceCallback.onClientSharedAccessPriorityChanged(CameraDeviceImpl.this, + primaryClient); + } + } + public void onDeviceError(final int errorCode, CaptureResultExtras resultExtras) { if (DEBUG) { Log.d(TAG, String.format( @@ -2447,6 +2608,12 @@ public class CameraDeviceImpl extends CameraDevice } @Override + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public void onClientSharedAccessPriorityChanged(boolean primaryClient) { + CameraDeviceImpl.this.onClientSharedAccessPriorityChanged(primaryClient); + } + + @Override public void onPrepared(int streamId) { final OutputConfiguration output; final StateCallbackKK sessionCallback; diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index 1cc085658bfa..c0a5928a369b 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -63,6 +63,7 @@ import android.hardware.camera2.params.OisSample; import android.hardware.camera2.params.RecommendedStreamConfiguration; import android.hardware.camera2.params.RecommendedStreamConfigurationMap; import android.hardware.camera2.params.ReprocessFormatsMap; +import android.hardware.camera2.params.SharedSessionConfiguration; import android.hardware.camera2.params.StreamConfiguration; import android.hardware.camera2.params.StreamConfigurationDuration; import android.hardware.camera2.params.StreamConfigurationMap; @@ -866,6 +867,15 @@ public class CameraMetadataNative implements Parcelable { return (T) metadata.getLensIntrinsicSamples(); } }); + sGetCommandMap.put( + CameraCharacteristics.SHARED_SESSION_CONFIGURATION.getNativeKey(), + new GetCommand() { + @Override + @SuppressWarnings("unchecked") + public <T> T getValue(CameraMetadataNative metadata, Key<T> key) { + return (T) metadata.getSharedSessionConfiguration(); + } + }); } private int[] getAvailableFormats() { @@ -1658,6 +1668,22 @@ public class CameraMetadataNative implements Parcelable { listHighResolution); } + private SharedSessionConfiguration getSharedSessionConfiguration() { + if (!Flags.cameraMultiClient()) { + return null; + } + Integer sharedSessionColorSpace = getBase( + CameraCharacteristics.SHARED_SESSION_COLOR_SPACE); + long[] sharedOutputConfigurations = getBase( + CameraCharacteristics.SHARED_SESSION_OUTPUT_CONFIGURATIONS); + + if ((sharedSessionColorSpace == null) || (sharedOutputConfigurations == null)) { + return null; + } + + return new SharedSessionConfiguration(sharedSessionColorSpace, sharedOutputConfigurations); + } + private StreamConfigurationMap getStreamConfigurationMapMaximumResolution() { StreamConfiguration[] configurations = getBase( CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS_MAXIMUM_RESOLUTION); diff --git a/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java index eb2ff88ec1b2..1769c4638805 100644 --- a/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraOfflineSessionImpl.java @@ -16,11 +16,13 @@ package android.hardware.camera2.impl; +import static com.android.internal.util.Preconditions.checkNotNull; + +import android.annotation.FlaggedApi; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraOfflineSession; import android.hardware.camera2.CameraOfflineSession.CameraOfflineSessionCallback; import android.hardware.camera2.CaptureFailure; @@ -40,15 +42,15 @@ import android.util.Range; import android.util.SparseArray; import android.view.Surface; +import com.android.internal.camera.flags.Flags; + import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.Executor; - -import static com.android.internal.util.Preconditions.*; +import java.util.concurrent.atomic.AtomicBoolean; public class CameraOfflineSessionImpl extends CameraOfflineSession implements IBinder.DeathRecipient { @@ -176,6 +178,12 @@ public class CameraOfflineSessionImpl extends CameraOfflineSession } @Override + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + public void onClientSharedAccessPriorityChanged(boolean primaryClient) { + Log.v(TAG, "onClientSharedAccessPriorityChanged primaryClient = " + primaryClient); + } + + @Override public void onDeviceIdle() { synchronized(mInterfaceLock) { if (mRemoteSession == null) { diff --git a/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java new file mode 100644 index 000000000000..a1f31c0ced5e --- /dev/null +++ b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java @@ -0,0 +1,242 @@ +/* + * 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.hardware.camera2.impl; + +import android.annotation.FlaggedApi; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraSharedCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.OutputConfiguration; +import android.os.ConditionVariable; +import android.os.Handler; +import android.view.Surface; + +import com.android.internal.camera.flags.Flags; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Standard implementation of CameraSharedCaptureSession. + * + * <p> + * Mostly just forwards calls to an instance of CameraCaptureSessionImpl, + * but implements the few necessary behavior changes and additional methods required + * for the shared session mode. + * </p> + */ +@FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) +public class CameraSharedCaptureSessionImpl + extends CameraSharedCaptureSession implements CameraCaptureSessionCore { + private static final String TAG = "CameraSharedCaptureSessionImpl"; + private final CameraCaptureSessionImpl mSessionImpl; + private final ConditionVariable mInitialized = new ConditionVariable(); + private boolean mIsPrimary; + + /** + * Create a new CameraCaptureSession. + */ + CameraSharedCaptureSessionImpl(int id, + CameraCaptureSession.StateCallback callback, Executor stateExecutor, + android.hardware.camera2.impl.CameraDeviceImpl deviceImpl, + Executor deviceStateExecutor, boolean configureSuccess, boolean isPrimary) { + CameraCaptureSession.StateCallback wrapperCallback = new WrapperCallback(callback); + mSessionImpl = new CameraCaptureSessionImpl(id, /*input*/null, wrapperCallback, + stateExecutor, deviceImpl, deviceStateExecutor, configureSuccess); + mIsPrimary = isPrimary; + mInitialized.open(); + } + + @Override + public int startStreaming(List<Surface> surfaces, Executor executor, CaptureCallback listener) + throws CameraAccessException { + // Todo: Need to add implementation. + return 0; + } + + @Override + public void stopStreaming() throws CameraAccessException { + // Todo: Need to add implementation. + } + + @Override + public void close() { + mSessionImpl.close(); + } + + @Override + public Surface getInputSurface() { + return null; + } + + @Override + public boolean isReprocessable() { + return false; + } + + @Override + public void abortCaptures() throws CameraAccessException { + if (mIsPrimary) { + mSessionImpl.abortCaptures(); + } + } + + @Override + public int setRepeatingRequest(CaptureRequest request, CaptureCallback listener, + Handler handler) throws CameraAccessException { + if (mIsPrimary) { + return mSessionImpl.setRepeatingRequest(request, listener, handler); + } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); + } + + @Override + public void stopRepeating() throws CameraAccessException { + if (mIsPrimary) { + mSessionImpl.stopRepeating(); + } + } + + @Override + public int capture(CaptureRequest request, CaptureCallback listener, Handler handler) + throws CameraAccessException { + if (mIsPrimary) { + return mSessionImpl.capture(request, listener, handler); + } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); + } + + @Override + public void tearDown(Surface surface) throws CameraAccessException { + mSessionImpl.tearDown(surface); + } + + @Override + public CameraDevice getDevice() { + return mSessionImpl.getDevice(); + } + + @Override + public boolean isAborting() { + return mSessionImpl.isAborting(); + } + + @Override + public CameraDeviceImpl.StateCallbackKK getDeviceStateCallback() { + return mSessionImpl.getDeviceStateCallback(); + } + + @Override + public void replaceSessionClose() { + mSessionImpl.replaceSessionClose(); + } + + @Override + public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback listener, + Handler handler) throws CameraAccessException { + throw new UnsupportedOperationException("Shared Capture session doesn't support" + + " this method"); + } + + @Override + public int captureBurst(List<CaptureRequest> requests, CaptureCallback listener, + Handler handler) throws CameraAccessException { + throw new UnsupportedOperationException("Shared Capture session doesn't support" + + " this method"); + } + + @Override + public void updateOutputConfiguration(OutputConfiguration config) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared capture session doesn't support" + + " this method"); + } + + @Override + public void finalizeOutputConfigurations(List<OutputConfiguration> deferredOutputConfigs) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared capture session doesn't support" + + " this method"); + } + + @Override + public void prepare(Surface surface) throws CameraAccessException { + throw new UnsupportedOperationException("Shared capture session doesn't support" + + " this method"); + } + + @Override + public void prepare(int maxCount, Surface surface) throws CameraAccessException { + throw new UnsupportedOperationException("Shared capture session doesn't support" + + " this method"); + } + + @Override + public void closeWithoutDraining() { + throw new UnsupportedOperationException("Shared capture session doesn't support" + + " this method"); + } + + private class WrapperCallback extends StateCallback { + private final StateCallback mCallback; + + WrapperCallback(StateCallback callback) { + mCallback = callback; + } + + @Override + public void onConfigured(CameraCaptureSession session) { + mInitialized.block(); + mCallback.onConfigured(CameraSharedCaptureSessionImpl.this); + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + mInitialized.block(); + mCallback.onConfigureFailed(CameraSharedCaptureSessionImpl.this); + } + + @Override + public void onReady(CameraCaptureSession session) { + mCallback.onReady(CameraSharedCaptureSessionImpl.this); + } + + @Override + public void onActive(CameraCaptureSession session) { + mCallback.onActive(CameraSharedCaptureSessionImpl.this); + } + + @Override + public void onCaptureQueueEmpty(CameraCaptureSession session) { + mCallback.onCaptureQueueEmpty(CameraSharedCaptureSessionImpl.this); + } + + @Override + public void onClosed(CameraCaptureSession session) { + mCallback.onClosed(CameraSharedCaptureSessionImpl.this); + } + + @Override + public void onSurfacePrepared(CameraCaptureSession session, Surface surface) { + mCallback.onSurfacePrepared(CameraSharedCaptureSessionImpl.this, + surface); + } + } +} diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index aec2cff61d99..831c75ec5d33 100644 --- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -301,4 +301,16 @@ public class ICameraDeviceUserWrapper { } } + /** + * API to check if the client is primary client when camera device is opened in shared mode. + */ + public boolean isPrimaryClient() throws CameraAccessException { + try { + return mRemoteDevice.isPrimaryClient(); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); + } + } } diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index d38be9b7b694..e12c46322d8c 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -29,6 +29,7 @@ import android.annotation.TestApi; import android.graphics.ColorSpace; import android.graphics.ImageFormat; import android.graphics.ImageFormat.Format; +import android.hardware.DataSpace.NamedDataSpace; import android.hardware.HardwareBuffer; import android.hardware.HardwareBuffer.Usage; import android.hardware.camera2.CameraCaptureSession; @@ -1729,6 +1730,79 @@ public final class OutputConfiguration implements Parcelable { } /** + * Get the configured format associated with this {@link OutputConfiguration}. + * + * @return {@link android.graphics.ImageFormat#Format} associated with this + * {@link OutputConfiguration}. + * + * @hide + */ + public @Format int getConfiguredFormat() { + return mConfiguredFormat; + } + + /** + * Get the usage flag associated with this {@link OutputConfiguration}. + * + * @return {@link HardwareBuffer#Usage} associated with this {@link OutputConfiguration}. + * + * @hide + */ + public @Usage long getUsage() { + return mUsage; + } + + /** + * Get the surface type associated with this {@link OutputConfiguration}. + * + * @return The surface type associated with this {@link OutputConfiguration}. + * + * @see #SURFACE_TYPE_SURFACE_VIEW + * @see #SURFACE_TYPE_SURFACE_TEXTURE + * @see #SURFACE_TYPE_MEDIA_RECORDER + * @see #SURFACE_TYPE_MEDIA_CODEC + * @see #SURFACE_TYPE_IMAGE_READER + * @see #SURFACE_TYPE_UNKNOWN + * @hide + */ + public int getSurfaceType() { + return mSurfaceType; + } + + /** + * Get the sensor pixel modes associated with this {@link OutputConfiguration}. + * + * @return List of {@link #SensorPixelMode} associated with this {@link OutputConfiguration}. + * + * @hide + */ + public @NonNull List<Integer> getSensorPixelModes() { + return mSensorPixelModesUsed; + } + + /** + * Get the sharing mode associated with this {@link OutputConfiguration}. + * + * @return true if surface sharing is enabled with this {@link OutputConfiguration}. + * + * @hide + */ + public boolean isShared() { + return mIsShared; + } + + /** + * Get the dataspace associated with this {@link OutputConfiguration}. + * + * @return {@link Dataspace#NamedDataSpace} for this {@link OutputConfiguration}. + * + * @hide + */ + public @NamedDataSpace int getConfiguredDataspace() { + return mConfiguredDataspace; + } + + /** * Get the physical camera ID associated with this {@link OutputConfiguration}. * * <p>If this OutputConfiguration isn't targeting a physical camera of a logical diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index 50c6b5b8b995..82aa64b1474c 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -23,6 +23,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.graphics.ColorSpace; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; @@ -78,6 +79,19 @@ public final class SessionConfiguration implements Parcelable { CameraDevice.SESSION_OPERATION_MODE_CONSTRAINED_HIGH_SPEED; /** + * A shared session type containing instances of {@link OutputConfiguration} from a set of + * predefined stream configurations. A shared session can be shared among multiple clients. + * Shared session does not have any {@link InputConfiguration} as it does not support + * reprocessable sessions. + * + * @see CameraDevice#createCaptureSession(SessionConfiguration) + * @hide + */ + @FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) + @SystemApi + public static final int SESSION_SHARED = CameraDevice.SESSION_OPERATION_MODE_SHARED; + + /** * First vendor-specific session mode * @hide */ diff --git a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java new file mode 100644 index 000000000000..cdcc92ce4404 --- /dev/null +++ b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.camera2.params; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.graphics.ColorSpace; +import android.graphics.ImageFormat.Format; +import android.hardware.DataSpace.NamedDataSpace; +import android.hardware.HardwareBuffer.Usage; +import android.hardware.camera2.params.OutputConfiguration.MirrorMode; +import android.hardware.camera2.params.OutputConfiguration.StreamUseCase; +import android.hardware.camera2.params.OutputConfiguration.TimestampBase; +import android.util.Log; +import android.util.Size; + +import com.android.internal.camera.flags.Flags; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Immutable class to store the shared session configuration + * {@link CameraCharacteristics#SHARED_SESSION_CONFIGURATION} to set up + * {@link android.view.Surface Surfaces} for creating a + * {@link android.hardware.camera2.CameraSharedCaptureSession capture session} using + * {@link android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)} and + * {@link android.hardware.camera2.params.SessionConfiguration#SESSION_SHARED + * shared capture session} when camera has been opened in shared mode using + * {@link #openSharedCamera(String, Executor, StateCallback)}. + * + * <p>This is the authoritative list for all output configurations that are supported by a camera + * device when opened in shared mode.</p> + * + * <p>An instance of this object is available from {@link CameraCharacteristics} using + * the {@link CameraCharacteristics#SHARED_SESSION_CONFIGURATION} key and the + * {@link CameraCharacteristics#get} method.</p> + * + * <pre><code>{@code + * CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + * StreamConfigurationMap configs = characteristics.get( + * CameraCharacteristics.SHARED_SESSION_CONFIGURATION); + * }</code></pre> + * + * @see CameraCharacteristics#SHARED_SESSION_CONFIGURATION + * @see CameraDevice#createCaptureSession(SessionConfiguration) + * @see SessionConfiguration#SESSION_SHARED + * @see CameraManager#openSharedCamera(String, Executor, StateCallback) + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_CAMERA_MULTI_CLIENT) +public final class SharedSessionConfiguration { + private static final String TAG = "SharedSessionConfiguration"; + // Metadata android.info.availableSharedOutputConfigurations has list of shared output + // configurations. Each output configuration has minimum of 11 entries of size long + // followed by the physical camera id if present. + // See android.info.availableSharedOutputConfigurations for details. + private static final int SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES = 11; + /** + * Immutable class to store shared output stream information. + */ + public static final class SharedOutputConfiguration { + private final int mSurfaceType; + private final Size mSize; + private final int mFormat; + private final int mDataspace; + private final long mStreamUseCase; + private String mPhysicalCameraId; + private final long mUsage; + private int mTimestampBase; + private int mMirrorMode; + private boolean mReadoutTimestampEnabled; + + /** + * Create a new {@link SharedOutputConfiguration}. + * + * @param surfaceType Surface Type for this output configuration. + * @param sz Size for this output configuration. + * @param format {@link android.graphics.ImageFormat#Format} associated with this + * {@link OutputConfiguration}. + * @param mirrorMode {@link OutputConfiguration#MirrorMode} for this output configuration. + * @param readoutTimeStampEnabled Flag indicating whether readout timestamp is enabled + * for this output configuration. + * @param timestampBase {@link OutputConfiguration#TimestampBase} for this output + * configuration. + * @param dataspace {@link Dataspace#NamedDataSpace} for this output configuration. + * @param usage {@link HardwareBuffer#Usage} for this output configuration. + * @param streamUseCase {@link OutputConfiguration#StreamUseCase} for this output + * configuration. + * @param physicalCamId Physical Camera Id for this output configuration. + * + * @hide + */ + public SharedOutputConfiguration(int surfaceType, @NonNull Size sz, @Format int format, + @MirrorMode int mirrorMode, boolean readoutTimeStampEnabled, + @TimestampBase int timestampBase, @NamedDataSpace int dataspace, @Usage long usage, + @StreamUseCase long streamUseCase, @Nullable String physicalCamId) { + mSurfaceType = surfaceType; + mSize = sz; + mFormat = format; + mMirrorMode = mirrorMode; + mReadoutTimestampEnabled = readoutTimeStampEnabled; + mTimestampBase = timestampBase; + mDataspace = dataspace; + mUsage = usage; + mStreamUseCase = streamUseCase; + mPhysicalCameraId = physicalCamId; + } + + /** + * Returns the surface type configured for the shared output configuration. + * @return SURFACE_TYPE_UNKNOWN = -1 + * SURFACE_TYPE_SURFACE_VIEW = 0 + * SURFACE_TYPE_SURFACE_TEXTURE = 1 + * SURFACE_TYPE_MEDIA_RECORDER = 2 + * SURFACE_TYPE_MEDIA_CODEC = 3 + * SURFACE_TYPE_IMAGE_READER = 4 + */ + public int getSurfaceType() { + return mSurfaceType; + } + + /** + * Returns the format of the shared output configuration. + * @return format The format of the configured output. This must be one of the + * {@link android.graphics.ImageFormat} or {@link android.graphics.PixelFormat} + * constants. Note that not all formats are supported by the camera device. + */ + public @Format int getFormat() { + return mFormat; + } + + /** + * Returns the configured size for the shared output configuration. + * @return surfaceSize Size for the shared output configuration + * + */ + public @NonNull Size getSize() { + return mSize; + } + + /** + * Return datatspace configured for the shared output configuration. + * + * @return {@link Dataspace#NamedDataSpace} configured for shared session + */ + public @NamedDataSpace int getDataspace() { + return mDataspace; + } + + /** + * Get the mirroring mode configured for the shared output configuration. + * + * @return {@link OutputConfiguration#MirrorMode} configured for the shared session + */ + public @MirrorMode int getMirrorMode() { + return mMirrorMode; + } + + /** + * Get the stream use case configured for the shared output configuration. + * + * @return {@link OutputConfiguration#StreamUseCase} configured for the shared session + */ + public @StreamUseCase long getStreamUseCase() { + return mStreamUseCase; + } + + /** + * Get the timestamp base configured for the shared output configuration. + * + * @return {@link OutputConfiguration#TimestampBase} configured for the shared session + */ + public @TimestampBase int getTimestampBase() { + return mTimestampBase; + } + + /** Whether readout timestamp is used for this shared output configuration. + * + */ + public boolean isReadoutTimestampEnabled() { + return mReadoutTimestampEnabled; + } + + /** Returns the usage if set for this shared output configuration. + * + * @return {@link HardwareBuffer#Usage} flags if set for shared output configuration with + * the ImageReader output surface. + */ + public @Usage long getUsage() { + return mUsage; + } + + public @Nullable String getPhysicalCameraId() { + return mPhysicalCameraId; + } + } + + /** + * Create a new {@link SharedSessionConfiguration}. + * + * <p>The array parameters ownership is passed to this object after creation; do not + * write to them after this constructor is invoked.</p> + * + * @param sharedColorSpace the colorspace to be used for the shared output configurations. + * @param sharedOutputConfigurations a non-{@code null} array of metadata + * android.info.availableSharedOutputConfigurations + * + * @hide + */ + public SharedSessionConfiguration(int sharedColorSpace, + @NonNull long[] sharedOutputConfigurations) { + mColorSpace = sharedColorSpace; + byte physicalCameraIdLen; + int surfaceType, width, height, format, mirrorMode, timestampBase, dataspace; + long usage, streamUseCase; + boolean isReadOutTimestampEnabled; + // Parse metadata android.info.availableSharedOutputConfigurations which contains + // list of shared output configurations. + int numOfEntries = sharedOutputConfigurations.length; + int i = 0; + while (numOfEntries >= SharedSessionConfiguration.SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES) { + surfaceType = (int) sharedOutputConfigurations[i]; + width = (int) sharedOutputConfigurations[i + 1]; + height = (int) sharedOutputConfigurations[i + 2]; + format = (int) sharedOutputConfigurations[i + 3]; + mirrorMode = (int) sharedOutputConfigurations[i + 4]; + isReadOutTimestampEnabled = (sharedOutputConfigurations[i + 5] != 0); + timestampBase = (int) sharedOutputConfigurations[i + 6]; + dataspace = (int) sharedOutputConfigurations[i + 7]; + usage = sharedOutputConfigurations[i + 8]; + streamUseCase = sharedOutputConfigurations[i + 9]; + physicalCameraIdLen = (byte) sharedOutputConfigurations[i + 10]; + numOfEntries -= SharedSessionConfiguration.SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES; + i += SharedSessionConfiguration.SHARED_OUTPUT_CONFIG_NUM_OF_ENTRIES; + if (numOfEntries < physicalCameraIdLen) { + Log.e(TAG, "Number of remaining data in shared configuration is less than" + + " physical camera id length . Malformed metadata" + + " android.info.availableSharedOutputConfigurations."); + break; + } + StringBuilder physicalCameraId = new StringBuilder(); + long asciiValue; + for (int j = 0; j < physicalCameraIdLen; j++) { + asciiValue = sharedOutputConfigurations[i + j]; + if (asciiValue == 0) { // Check for null terminator + break; + } + physicalCameraId.append((char) asciiValue); + } + SharedOutputConfiguration outputInfo; + outputInfo = new SharedOutputConfiguration(surfaceType, new Size(width, height), + format, mirrorMode, isReadOutTimestampEnabled, timestampBase, + dataspace, usage, streamUseCase, physicalCameraId.toString()); + mOutputStreamConfigurations.add(outputInfo); + i += physicalCameraIdLen; + numOfEntries -= physicalCameraIdLen; + } + if (numOfEntries != 0) { + Log.e(TAG, "Unexpected entries left in shared output configuration." + + " Malformed metadata android.info.availableSharedOutputConfigurations."); + } + } + + /** + * Return the shared session color space which is configured. + * + * @return the shared session color space + */ + @SuppressLint("MethodNameUnits") + public @Nullable ColorSpace getColorSpace() { + if (mColorSpace != ColorSpaceProfiles.UNSPECIFIED) { + return ColorSpace.get(ColorSpace.Named.values()[mColorSpace]); + } else { + return null; + } + } + /** + * Get information about each shared output configuarion in the shared session. + * + * @return Non-modifiable list of output configuration. + * + */ + public @NonNull List<SharedOutputConfiguration> getOutputStreamsInformation() { + return Collections.unmodifiableList(mOutputStreamConfigurations); + } + + private int mColorSpace; + private final ArrayList<SharedOutputConfiguration> mOutputStreamConfigurations = + new ArrayList<SharedOutputConfiguration>(); +} + diff --git a/core/java/android/hardware/contexthub/HubDiscoveryInfo.java b/core/java/android/hardware/contexthub/HubDiscoveryInfo.java new file mode 100644 index 000000000000..875c4b4182be --- /dev/null +++ b/core/java/android/hardware/contexthub/HubDiscoveryInfo.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.contexthub; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.chre.flags.Flags; +import android.hardware.location.ContextHubManager; + +/** + * Class that represents the result of from an hub endpoint discovery. + * + * <p>The type is returned from an endpoint discovery query via {@link + * ContextHubManager#findEndpoints}. Application may use the values {@link #getHubEndpointInfo} to + * retrieve the {@link HubEndpointInfo} that describes the endpoint that matches the query. The + * class provides flexibility in returning more information (e.g. service provided by the endpoint) + * in addition to the information about the endpoint. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_OFFLOAD_API) +public class HubDiscoveryInfo { + // TODO(b/375487784): Add ServiceInfo to the result. + android.hardware.contexthub.HubEndpointInfo mEndpointInfo; + + /** + * Constructor for internal use. + * + * @hide + */ + public HubDiscoveryInfo(android.hardware.contexthub.HubEndpointInfo endpointInfo) { + mEndpointInfo = endpointInfo; + } + + /** Get the {@link android.hardware.contexthub.HubEndpointInfo} for the endpoint found. */ + @NonNull + public HubEndpointInfo getHubEndpointInfo() { + return mEndpointInfo; + } +} diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.aidl b/core/java/android/hardware/contexthub/HubEndpointInfo.aidl new file mode 100644 index 000000000000..025b2b1f685a --- /dev/null +++ b/core/java/android/hardware/contexthub/HubEndpointInfo.aidl @@ -0,0 +1,20 @@ +/* + * 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.hardware.contexthub; + +/** @hide */ +parcelable HubEndpointInfo; diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.java b/core/java/android/hardware/contexthub/HubEndpointInfo.java new file mode 100644 index 000000000000..c17fc00433e6 --- /dev/null +++ b/core/java/android/hardware/contexthub/HubEndpointInfo.java @@ -0,0 +1,189 @@ +/* + * 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.hardware.contexthub; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.chre.flags.Flags; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Parcelable representing an endpoint from ContextHub or VendorHub. + * + * <p>HubEndpointInfo contains information about an endpoint, including its name, tag and other + * information. A HubEndpointInfo object can be used to accurately identify a specific endpoint. + * Application can use this object to identify and describe an endpoint. + * + * <p>See: {@link android.hardware.location.ContextHubManager#findEndpoints} for how to retrieve + * {@link HubEndpointInfo} for endpoints on a hub. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_OFFLOAD_API) +public final class HubEndpointInfo implements Parcelable { + /** + * A unique identifier for one endpoint. A unique identifier for one endpoint consists of two + * parts: (1) a unique long number for a hub and (2) a long number for the endpoint, unique + * within a hub. This class overrides equality methods and can be used to compare if two + * endpoints are the same. + */ + public static class HubEndpointIdentifier { + private final long mEndpointId; + private final long mHubId; + + /** @hide */ + public HubEndpointIdentifier(long hubId, long endpointId) { + mEndpointId = endpointId; + mHubId = hubId; + } + + /** @hide */ + public HubEndpointIdentifier(android.hardware.contexthub.EndpointId halEndpointId) { + mEndpointId = halEndpointId.id; + mHubId = halEndpointId.hubId; + } + + /** Get the endpoint portion of the identifier. */ + public long getEndpoint() { + return mEndpointId; + } + + /** Get the hub portion of the identifier. */ + public long getHub() { + return mHubId; + } + + /** + * Create an invalid endpoint id, to represent endpoint that are not yet registered with the + * HAL. + * + * @hide + */ + public static HubEndpointIdentifier invalid() { + return new HubEndpointIdentifier( + android.hardware.contexthub.HubInfo.HUB_ID_INVALID, + android.hardware.contexthub.EndpointId.ENDPOINT_ID_INVALID); + } + + @Override + public int hashCode() { + return Objects.hash(mEndpointId, mHubId); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof HubEndpointIdentifier other)) { + return false; + } + if (other.mHubId != mHubId) { + return false; + } + return other.mEndpointId == mEndpointId; + } + } + + private final HubEndpointIdentifier mId; + private final String mName; + @Nullable private final String mTag; + + // TODO(b/375487784): Add Service/version and other information to this object + + /** @hide */ + public HubEndpointInfo(android.hardware.contexthub.EndpointInfo endpointInfo) { + mId = new HubEndpointIdentifier(endpointInfo.id.hubId, endpointInfo.id.id); + mName = endpointInfo.name; + mTag = endpointInfo.tag; + } + + private HubEndpointInfo(Parcel in) { + long hubId = in.readLong(); + long endpointId = in.readLong(); + mName = in.readString(); + mTag = in.readString(); + + mId = new HubEndpointIdentifier(hubId, endpointId); + } + + /** Parcel implementation details */ + @Override + public int describeContents() { + return 0; + } + + /** Parcel implementation details */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(mId.getHub()); + dest.writeLong(mId.getEndpoint()); + dest.writeString(mName); + dest.writeString(mTag); + } + + /** Get a unique identifier for this endpoint. */ + @NonNull + public HubEndpointIdentifier getIdentifier() { + return mId; + } + + /** Get the human-readable name of this endpoint (for debugging purposes). */ + @NonNull + public String getName() { + return mName; + } + + /** + * Get the tag that further identifies the submodule that created this endpoint. For example, a + * single application could provide multiple endpoints. These endpoints will share the same + * name, but will have different tags. This tag can be used to identify the submodule within the + * application that provided the endpoint. + */ + @Nullable + public String getTag() { + return mTag; + } + + @Override + public String toString() { + StringBuilder out = new StringBuilder(); + out.append("Endpoint [0x"); + out.append(Long.toHexString(mId.getEndpoint())); + out.append("@ Hub 0x"); + out.append(Long.toHexString(mId.getHub())); + out.append("] Name="); + out.append(mName); + out.append(", Tag="); + out.append(mTag); + return out.toString(); + } + + public static final @android.annotation.NonNull Creator<HubEndpointInfo> CREATOR = + new Creator<>() { + public HubEndpointInfo createFromParcel(Parcel in) { + return new HubEndpointInfo(in); + } + + public HubEndpointInfo[] newArray(int size) { + return new HubEndpointInfo[size]; + } + }; +} diff --git a/core/java/android/hardware/contexthub/OWNERS b/core/java/android/hardware/contexthub/OWNERS new file mode 100644 index 000000000000..a65a2bf9ee36 --- /dev/null +++ b/core/java/android/hardware/contexthub/OWNERS @@ -0,0 +1,2 @@ +# ContextHub team +file:platform/system/chre:/OWNERS diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 28da644dd837..e6a1640781ed 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -21,6 +21,8 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.HdrCapabilities.HdrType; import static android.view.Display.INVALID_DISPLAY; +import static com.android.server.display.feature.flags.Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS; + import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.FloatRange; @@ -576,6 +578,8 @@ public final class DisplayManager { EVENT_FLAG_DISPLAY_ADDED, EVENT_FLAG_DISPLAY_CHANGED, EVENT_FLAG_DISPLAY_REMOVED, + EVENT_FLAG_DISPLAY_REFRESH_RATE, + EVENT_FLAG_DISPLAY_STATE }) @Retention(RetentionPolicy.SOURCE) public @interface EventFlag {} @@ -596,8 +600,8 @@ public final class DisplayManager { * * @see #registerDisplayListener(DisplayListener, Handler, long) * - * @hide */ + @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) public static final long EVENT_FLAG_DISPLAY_ADDED = 1L << 0; /** @@ -605,8 +609,8 @@ public final class DisplayManager { * * @see #registerDisplayListener(DisplayListener, Handler, long) * - * @hide */ + @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) public static final long EVENT_FLAG_DISPLAY_REMOVED = 1L << 1; /** @@ -614,10 +618,27 @@ public final class DisplayManager { * * @see #registerDisplayListener(DisplayListener, Handler, long) * - * @hide */ + @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2; + + /** + * Event flag to register for a display's refresh rate changes. + * + * @see #registerDisplayListener(DisplayListener, Handler, long) + */ + @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) + public static final long EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 3; + + /** + * Event flag to register for a display state changes. + * + * @see #registerDisplayListener(DisplayListener, Handler, long) + */ + @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) + public static final long EVENT_FLAG_DISPLAY_STATE = 1L << 4; + /** * Event flag to register for a display's brightness changes. This notification is sent * through the {@link DisplayListener#onDisplayChanged} callback method. New brightness @@ -787,9 +808,6 @@ public final class DisplayManager { * if the listener should be invoked on the calling thread's looper. * @param eventFlags A bitmask of the event types for which this listener is subscribed. * - * @see #EVENT_FLAG_DISPLAY_ADDED - * @see #EVENT_FLAG_DISPLAY_CHANGED - * @see #EVENT_FLAG_DISPLAY_REMOVED * @see #registerDisplayListener(DisplayListener, Handler) * @see #unregisterDisplayListener * @@ -806,18 +824,31 @@ public final class DisplayManager { * Registers a display listener to receive notifications about given display event types. * * @param listener The listener to register. + * @param executor Executor for the thread that will be receiving the callbacks. Cannot be null. + * @param eventFlags A bitmask of the event types for which this listener is subscribed. + * + * @see #registerDisplayListener(DisplayListener, Handler) + * @see #unregisterDisplayListener + * + */ + @FlaggedApi(FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) + public void registerDisplayListener(@NonNull Executor executor, @EventFlag long eventFlags, + @NonNull DisplayListener listener) { + mGlobal.registerDisplayListener(listener, executor, + mGlobal.mapFlagsToInternalEventFlag(eventFlags, 0), + ActivityThread.currentPackageName()); + } + + /** + * Registers a display listener to receive notifications about given display event types. + * + * @param listener The listener to register. * @param handler The handler on which the listener should be invoked, or null * if the listener should be invoked on the calling thread's looper. * @param eventFlags A bitmask of the event types for which this listener is subscribed. * @param privateEventFlags A bitmask of the private event types for which this listener * is subscribed. * - * @see #EVENT_FLAG_DISPLAY_ADDED - * @see #EVENT_FLAG_DISPLAY_CHANGED - * @see #EVENT_FLAG_DISPLAY_REMOVED - * @see #PRIVATE_EVENT_FLAG_DISPLAY_BRIGHTNESS - * @see #PRIVATE_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED - * @see #PRIVATE_EVENT_FLAG_HDR_SDR_RATIO_CHANGED * @see #registerDisplayListener(DisplayListener, Handler) * @see #unregisterDisplayListener * diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 03b44f63e3b7..be710b1328e7 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -62,6 +62,7 @@ import android.view.DisplayInfo; import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.display.feature.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -108,6 +109,8 @@ public final class DisplayManagerGlobal { EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED, EVENT_DISPLAY_CONNECTED, EVENT_DISPLAY_DISCONNECTED, + EVENT_DISPLAY_REFRESH_RATE_CHANGED, + EVENT_DISPLAY_STATE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface DisplayEvent {} @@ -119,6 +122,8 @@ public final class DisplayManagerGlobal { public static final int EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED = 5; public static final int EVENT_DISPLAY_CONNECTED = 6; public static final int EVENT_DISPLAY_DISCONNECTED = 7; + public static final int EVENT_DISPLAY_REFRESH_RATE_CHANGED = 8; + public static final int EVENT_DISPLAY_STATE_CHANGED = 9; @LongDef(prefix = {"INTERNAL_EVENT_DISPLAY"}, flag = true, value = { INTERNAL_EVENT_FLAG_DISPLAY_ADDED, @@ -127,6 +132,8 @@ public final class DisplayManagerGlobal { INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED, + INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, + INTERNAL_EVENT_FLAG_DISPLAY_STATE }) @Retention(RetentionPolicy.SOURCE) public @interface InternalEventFlag {} @@ -137,6 +144,8 @@ public final class DisplayManagerGlobal { public static final long INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED = 1L << 3; public static final long INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED = 1L << 4; public static final long INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED = 1L << 5; + public static final long INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE = 1L << 6; + public static final long INTERNAL_EVENT_FLAG_DISPLAY_STATE = 1L << 7; @UnsupportedAppUsage private static DisplayManagerGlobal sInstance; @@ -1427,6 +1436,18 @@ public final class DisplayManagerGlobal { mListener.onDisplayDisconnected(displayId); } break; + case EVENT_DISPLAY_REFRESH_RATE_CHANGED: + if ((mInternalEventFlagsMask + & INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE) != 0) { + mListener.onDisplayChanged(displayId); + } + break; + case EVENT_DISPLAY_STATE_CHANGED: + if ((mInternalEventFlagsMask + & INTERNAL_EVENT_FLAG_DISPLAY_STATE) != 0) { + mListener.onDisplayChanged(displayId); + } + break; } if (DEBUG) { Trace.endSection(); @@ -1566,6 +1587,10 @@ public final class DisplayManagerGlobal { return "EVENT_DISPLAY_CONNECTED"; case EVENT_DISPLAY_DISCONNECTED: return "EVENT_DISPLAY_DISCONNECTED"; + case EVENT_DISPLAY_REFRESH_RATE_CHANGED: + return "EVENT_DISPLAY_REFRESH_RATE_CHANGED"; + case EVENT_DISPLAY_STATE_CHANGED: + return "EVENT_DISPLAY_STATE_CHANGED"; } return "UNKNOWN"; } @@ -1630,6 +1655,17 @@ public final class DisplayManagerGlobal { baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; } + if (Flags.displayListenerPerformanceImprovements()) { + if ((eventFlags & DisplayManager.EVENT_FLAG_DISPLAY_REFRESH_RATE) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + } + + if ((eventFlags & DisplayManager.EVENT_FLAG_DISPLAY_STATE) != 0) { + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_STATE; + } + } + + return baseEventMask; } } diff --git a/core/java/android/hardware/flags/overlayproperties_flags.aconfig b/core/java/android/hardware/flags/flags.aconfig index 6c86108c4034..5ca6c6bed1f0 100644 --- a/core/java/android/hardware/flags/overlayproperties_flags.aconfig +++ b/core/java/android/hardware/flags/flags.aconfig @@ -2,6 +2,15 @@ package: "android.hardware.flags" container: "system" flag { + name: "luts_api" + is_exported: true + is_fixed_read_only: true + namespace: "core_graphics" + description: "public Luts related Apis" + bug: "349667978" +} + +flag { name: "overlayproperties_class_api" is_exported: true namespace: "core_graphics" diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 4b2f2c218e5a..fee074901c10 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -170,4 +170,11 @@ flag { namespace: "input" description: "Adds key gestures for talkback and magnifier" bug: "375277034" -}
\ No newline at end of file +} + +flag { + name: "can_window_override_power_gesture_api" + namespace: "wallet_integration" + description: "Adds new API in WindowManager class to check if the window can override the power key double tap behavior." + bug: "378736024" + }
\ No newline at end of file diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index 494bfc926384..e009c2f4a9e9 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -34,6 +34,8 @@ import android.chre.flags.Flags; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.contexthub.ErrorCode; +import android.hardware.contexthub.HubDiscoveryInfo; +import android.hardware.contexthub.HubEndpointInfo; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; @@ -42,6 +44,7 @@ import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; @@ -679,6 +682,29 @@ public final class ContextHubManager { } /** + * Find a list of endpoints that matches a specific ID. + * + * @param endpointId Statically generated ID for an endpoint. + * @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery. + */ + @FlaggedApi(Flags.FLAG_OFFLOAD_API) + @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + @NonNull + public List<HubDiscoveryInfo> findEndpoints(long endpointId) { + try { + List<HubEndpointInfo> endpointInfos = mService.findEndpoints(endpointId); + List<HubDiscoveryInfo> results = new ArrayList<>(endpointInfos.size()); + // Wrap with result type + for (HubEndpointInfo endpointInfo : endpointInfos) { + results.add(new HubDiscoveryInfo(endpointInfo)); + } + return results; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Set a callback to receive messages from the context hub * * @param callback Callback object diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl index b0cc763dc8fd..fc6a70a44ee3 100644 --- a/core/java/android/hardware/location/IContextHubService.aidl +++ b/core/java/android/hardware/location/IContextHubService.aidl @@ -18,17 +18,18 @@ package android.hardware.location; // Declare any non-default types here with import statements import android.app.PendingIntent; -import android.hardware.location.HubInfo; +import android.hardware.contexthub.HubEndpointInfo; import android.hardware.location.ContextHubInfo; import android.hardware.location.ContextHubMessage; -import android.hardware.location.NanoApp; -import android.hardware.location.NanoAppBinary; -import android.hardware.location.NanoAppFilter; -import android.hardware.location.NanoAppInstanceInfo; +import android.hardware.location.HubInfo; import android.hardware.location.IContextHubCallback; import android.hardware.location.IContextHubClient; import android.hardware.location.IContextHubClientCallback; import android.hardware.location.IContextHubTransactionCallback; +import android.hardware.location.NanoApp; +import android.hardware.location.NanoAppBinary; +import android.hardware.location.NanoAppFilter; +import android.hardware.location.NanoAppInstanceInfo; /** * @hide @@ -122,4 +123,8 @@ interface IContextHubService { // Enables or disables test mode @EnforcePermission("ACCESS_CONTEXT_HUB") boolean setTestMode(in boolean enable); + + // Finds all endpoints that havea specific ID + @EnforcePermission("ACCESS_CONTEXT_HUB") + List<HubEndpointInfo> findEndpoints(long endpointId); } diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index 4bc5bd2427ea..26308f69cfbe 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -16,6 +16,9 @@ package android.inputmethodservice; +import static android.view.inputmethod.Flags.FLAG_VERIFY_KEY_EVENT; + +import android.annotation.FlaggedApi; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -193,6 +196,12 @@ public abstract class AbstractInputMethodService extends WindowProviderService } } + @FlaggedApi(FLAG_VERIFY_KEY_EVENT) + @Override + public boolean onShouldVerifyKeyEvent(@NonNull KeyEvent event) { + return AbstractInputMethodService.this.onShouldVerifyKeyEvent(event); + } + /** * Take care of dispatching incoming trackball events to the appropriate * callbacks on the service, and tell the client when this is done. @@ -308,6 +317,14 @@ public abstract class AbstractInputMethodService extends WindowProviderService return false; } + /** + * @see InputMethodService#onShouldVerifyKeyEvent(KeyEvent) + */ + @FlaggedApi(FLAG_VERIFY_KEY_EVENT) + public boolean onShouldVerifyKeyEvent(@NonNull KeyEvent event) { + return false; + } + /** @hide */ @Override public final int getWindowType() { diff --git a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java index 62b131af74fe..9b37533f5b02 100644 --- a/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodSessionWrapper.java @@ -16,12 +16,16 @@ package android.inputmethodservice; +import static android.view.inputmethod.Flags.verifyKeyEvent; + import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.Rect; +import android.hardware.input.InputManager; import android.os.Bundle; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; import android.util.Log; import android.util.SparseArray; import android.view.InputChannel; @@ -41,6 +45,8 @@ import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; +import java.util.Objects; + class IInputMethodSessionWrapper extends IInputMethodSession.Stub implements HandlerCaller.Callback { private static final String TAG = "InputMethodWrapper"; @@ -56,6 +62,7 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub private static final int DO_REMOVE_IME_SURFACE = 130; private static final int DO_FINISH_INPUT = 140; private static final int DO_INVALIDATE_INPUT = 150; + private final Context mContext; @UnsupportedAppUsage @@ -66,6 +73,7 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub public IInputMethodSessionWrapper(Context context, InputMethodSession inputMethodSession, InputChannel channel) { + mContext = context; mCaller = new HandlerCaller(context, null, this, true /*asyncHandler*/); mInputMethodSession = inputMethodSession; @@ -233,6 +241,8 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub } private final class ImeInputEventReceiver extends InputEventReceiver implements InputMethodSession.EventCallback { + // Time after which a KeyEvent is invalid + private static final long KEY_EVENT_ALLOW_PERIOD_MS = 100L; private final SparseArray<InputEvent> mPendingEvents = new SparseArray<InputEvent>(); public ImeInputEventReceiver(InputChannel inputChannel, Looper looper) { @@ -247,10 +257,23 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub return; } + if (event instanceof KeyEvent keyEvent && needsVerification(keyEvent)) { + // any KeyEvent with modifiers (e.g. Ctrl/Alt/Fn) must be verified that + // they originated from system. + InputManager im = mContext.getSystemService(InputManager.class); + Objects.requireNonNull(im); + final long age = SystemClock.uptimeMillis() - keyEvent.getEventTime(); + if (age >= KEY_EVENT_ALLOW_PERIOD_MS && im.verifyInputEvent(keyEvent) == null) { + Log.w(TAG, "Unverified or Invalid KeyEvent injected into IME. Dropping " + + keyEvent); + finishInputEvent(event, false /* handled */); + return; + } + } + final int seq = event.getSequenceNumber(); mPendingEvents.put(seq, event); - if (event instanceof KeyEvent) { - KeyEvent keyEvent = (KeyEvent)event; + if (event instanceof KeyEvent keyEvent) { mInputMethodSession.dispatchKeyEvent(seq, keyEvent, this); } else { MotionEvent motionEvent = (MotionEvent)event; @@ -271,5 +294,21 @@ class IInputMethodSessionWrapper extends IInputMethodSession.Stub finishInputEvent(event, handled); } } + + private boolean hasKeyModifiers(KeyEvent event) { + if (event.hasNoModifiers()) { + return false; + } + return event.hasModifiers(KeyEvent.META_CTRL_ON) + || event.hasModifiers(KeyEvent.META_ALT_ON) + || event.hasModifiers(KeyEvent.KEYCODE_FUNCTION); + } + + private boolean needsVerification(KeyEvent event) { + //TODO(b/331730488): Handle a11y events as well. + return verifyKeyEvent() + && (hasKeyModifiers(event) + || mInputMethodSession.onShouldVerifyKeyEvent(event)); + } } } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index dadb5c386b76..a8fde4a3f7c6 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -56,6 +56,7 @@ import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECT import static android.view.inputmethod.ConnectionlessHandwritingCallback.CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED; import static android.view.inputmethod.Flags.FLAG_CONNECTIONLESS_HANDWRITING; import static android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API; +import static android.view.inputmethod.Flags.FLAG_VERIFY_KEY_EVENT; import static android.view.inputmethod.Flags.ctrlShiftShortcut; import static android.view.inputmethod.Flags.predictiveBackIme; @@ -3735,6 +3736,23 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Received by the IME before dispatch to {@link #onKeyDown(int, KeyEvent)} to let the system + * know if the {@link KeyEvent} needs to be verified that it originated from the system. + * {@link KeyEvent}s may originate from outside of the system and any sensitive keys should be + * marked for verification. One example of this could be using key shortcuts for switching to + * another IME. + * + * @param keyEvent the event that may need verification. + * @return {@code true} if {@link KeyEvent} should have its HMAC verified before dispatch, + * {@code false} otherwise. + */ + @FlaggedApi(FLAG_VERIFY_KEY_EVENT) + @Override + public boolean onShouldVerifyKeyEvent(@NonNull KeyEvent keyEvent) { + return false; + } + + /** * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent) * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle * the event). diff --git a/core/java/android/net/vcn/VcnFrameworkInitializer.java b/core/java/android/net/ConnectivityFrameworkInitializerBaklava.java index 8cb213b306be..1f0fa92d7976 100644 --- a/core/java/android/net/vcn/VcnFrameworkInitializer.java +++ b/core/java/android/net/ConnectivityFrameworkInitializerBaklava.java @@ -14,15 +14,21 @@ * limitations under the License. */ -package android.net.vcn; +package android.net; +import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API; + +import android.annotation.FlaggedApi; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.SystemServiceRegistry; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.Context; import android.content.pm.PackageManager; +import android.net.vcn.IVcnManagementService; +import android.net.vcn.VcnManager; import android.os.Build; import android.os.SystemProperties; @@ -31,8 +37,9 @@ import android.os.SystemProperties; * * @hide */ -// TODO: Expose it as @SystemApi(client = MODULE_LIBRARIES) -public final class VcnFrameworkInitializer { +@FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class ConnectivityFrameworkInitializerBaklava { /** * Starting with {@link VANILLA_ICE_CREAM}, Telephony feature flags (e.g. {@link * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}) are being checked before returning managers @@ -55,7 +62,7 @@ public final class VcnFrameworkInitializer { */ private static final int VENDOR_API_FOR_ANDROID_V = 202404; - private VcnFrameworkInitializer() {} + private ConnectivityFrameworkInitializerBaklava() {} // Suppressing AndroidFrameworkCompatChange because we're querying vendor // partition SDK level, not application's target SDK version (which BTW we @@ -86,7 +93,10 @@ public final class VcnFrameworkInitializer { * * @throws IllegalStateException if this is called anywhere besides {@link * SystemServiceRegistry}. + * @hide */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static void registerServiceWrappers() { SystemServiceRegistry.registerContextAwareService( VcnManager.VCN_MANAGEMENT_SERVICE_STRING, diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java index af93c964a8ba..3219ce81c256 100644 --- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java +++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java @@ -16,6 +16,7 @@ package android.net.vcn; import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE; +import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API; import static android.net.vcn.Flags.FLAG_SAFE_MODE_CONFIG; import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED; @@ -82,7 +83,15 @@ import java.util.concurrent.TimeUnit; * </ul> */ public final class VcnGatewayConnectionConfig { - /** @hide */ + /** + * Minimum NAT timeout not set. + * + * <p>When the timeout is not set, the device will automatically choose a keepalive interval and + * may reduce the keepalive frequency for power-optimization. + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + // This constant does not represent a minimum value. It indicates the value is not configured. + @SuppressLint("MinMaxConstant") public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET = -1; /** @hide */ @@ -773,7 +782,7 @@ public final class VcnGatewayConnectionConfig { * * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN * Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN - * Gateway. + * Gateway; or {@link MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET} to clear this value. * @return this {@link Builder} instance, for chaining */ @NonNull @@ -781,8 +790,10 @@ public final class VcnGatewayConnectionConfig { @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS) int minUdpPort4500NatTimeoutSeconds) { Preconditions.checkArgument( - minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, - "Timeout must be at least 120s"); + minUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET + || minUdpPort4500NatTimeoutSeconds + >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, + "Timeout must be at least 120s or MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET"); mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds; return this; diff --git a/core/java/android/net/vcn/VcnTransportInfo.java b/core/java/android/net/vcn/VcnTransportInfo.java index 1fc91eea3138..3638429f33fb 100644 --- a/core/java/android/net/vcn/VcnTransportInfo.java +++ b/core/java/android/net/vcn/VcnTransportInfo.java @@ -17,13 +17,16 @@ package android.net.vcn; import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API; import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS; import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; +import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.net.NetworkCapabilities; import android.net.TransportInfo; import android.net.wifi.WifiInfo; @@ -52,23 +55,29 @@ import java.util.Objects; * @hide */ // TODO: Do not store WifiInfo and subscription ID in VcnTransportInfo anymore -public class VcnTransportInfo implements TransportInfo, Parcelable { +@FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class VcnTransportInfo implements TransportInfo, Parcelable { @Nullable private final WifiInfo mWifiInfo; private final int mSubId; private final int mMinUdpPort4500NatTimeoutSeconds; + /** @hide */ public VcnTransportInfo(@NonNull WifiInfo wifiInfo) { this(wifiInfo, INVALID_SUBSCRIPTION_ID, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET); } + /** @hide */ public VcnTransportInfo(@NonNull WifiInfo wifiInfo, int minUdpPort4500NatTimeoutSeconds) { this(wifiInfo, INVALID_SUBSCRIPTION_ID, minUdpPort4500NatTimeoutSeconds); } + /** @hide */ public VcnTransportInfo(int subId) { this(null /* wifiInfo */, subId, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET); } + /** @hide */ public VcnTransportInfo(int subId, int minUdpPort4500NatTimeoutSeconds) { this(null /* wifiInfo */, subId, minUdpPort4500NatTimeoutSeconds); } @@ -86,6 +95,7 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { * <p>If the underlying Network for the associated VCN is Cellular, returns null. * * @return the WifiInfo if there is an underlying WiFi connection, else null. + * @hide */ @Nullable public WifiInfo getWifiInfo() { @@ -100,17 +110,27 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { * * @return the Subscription ID if a cellular underlying Network is present, else {@link * android.telephony.SubscriptionManager#INVALID_SUBSCRIPTION_ID}. + * @hide */ public int getSubId() { return mSubId; } /** - * Get the VCN provided UDP port 4500 NAT timeout + * Get the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping. * - * @return the UDP 4500 NAT timeout, or + * <p>To ensure uninterrupted connectivity, the device must send keepalive packets before the + * timeout. Failure to do so may result in the mapping being cleared and connection termination. + * This value is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs, + * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data. + * + * @return the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping, or * VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET if not set. + * @see VcnGatewayConnectionConfig.Builder#setMinUdpPort4500NatTimeoutSeconds(int) + * @hide */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public int getMinUdpPort4500NatTimeoutSeconds() { return mMinUdpPort4500NatTimeoutSeconds; } @@ -129,12 +149,21 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { && mMinUdpPort4500NatTimeoutSeconds == that.mMinUdpPort4500NatTimeoutSeconds; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @Override public int describeContents() { return 0; } + /** @hide */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @Override @NonNull public TransportInfo makeCopy(long redactions) { @@ -149,6 +178,9 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { mMinUdpPort4500NatTimeoutSeconds); } + /** @hide */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @Override public long getApplicableRedactions() { long redactions = REDACT_FOR_NETWORK_SETTINGS; @@ -161,7 +193,13 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { return redactions; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mSubId); @@ -174,7 +212,13 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { return "VcnTransportInfo { mWifiInfo = " + mWifiInfo + ", mSubId = " + mSubId + " }"; } - /** Implement the Parcelable interface */ + /** + * Implement the Parcelable interface + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final @NonNull Creator<VcnTransportInfo> CREATOR = new Creator<VcnTransportInfo>() { public VcnTransportInfo createFromParcel(Parcel in) { @@ -201,37 +245,63 @@ public class VcnTransportInfo implements TransportInfo, Parcelable { } }; - /** This class can be used to construct a {@link VcnTransportInfo}. */ + /** + * This class can be used to construct a {@link VcnTransportInfo}. + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final class Builder { private int mMinUdpPort4500NatTimeoutSeconds = MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET; - /** Construct Builder */ + /** + * Construct Builder + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public Builder() {} /** - * Sets the maximum supported IKEv2/IPsec NATT keepalive timeout. + * Set the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping. * * <p>This is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs, * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data. * - * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN - * Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN - * Gateway. + * @param minUdpPort4500NatTimeoutSeconds the minimum duration that the VCN Gateway + * guarantees to preserve a NAT mapping, or {@link MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET} + * to clear this value. To ensure uninterrupted connectivity, the device must send + * keepalive packets within this interval. Failure to do so may result in the mapping + * being cleared and connection termination. * @return this {@link Builder} instance, for chaining + * @see VcnGatewayConnectionConfig.Builder#setMinUdpPort4500NatTimeoutSeconds(int) + * @hide */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @NonNull public Builder setMinUdpPort4500NatTimeoutSeconds( @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS) int minUdpPort4500NatTimeoutSeconds) { Preconditions.checkArgument( - minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, - "Timeout must be at least 120s"); + minUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET + || minUdpPort4500NatTimeoutSeconds + >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, + "Timeout must be at least 120s or MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET"); mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds; return Builder.this; } - /** Build a VcnTransportInfo instance */ + /** + * Build a VcnTransportInfo instance + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @NonNull public VcnTransportInfo build() { return new VcnTransportInfo( diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 99e7d166446e..05bd10b053fe 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -18,10 +18,12 @@ package android.os; import static java.util.Objects.requireNonNull; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.util.ArrayMap; import android.util.Size; @@ -72,16 +74,18 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { /** * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain * Binder object(s). - * * @hide */ + @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final int STATUS_BINDERS_NOT_PRESENT = 0; /** * Status when the Bundle can <b>assert</b> that there are Binder object(s) in the Parcel. - * * @hide */ + @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final int STATUS_BINDERS_PRESENT = 1; /** @@ -94,9 +98,10 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * object to the Bundle but it is not possible to assert this fact unless the Bundle is written * to a Parcel. * </p> - * * @hide */ + @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final int STATUS_BINDERS_UNKNOWN = 2; /** @hide */ @@ -417,6 +422,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * Returns a status indicating whether the bundle contains any parcelled Binder objects. * @hide */ + @FlaggedApi(Flags.FLAG_ENABLE_HAS_BINDERS) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public @HasBinderStatus int hasBinders() { if ((mFlags & FLAG_HAS_BINDERS_KNOWN) != 0) { if ((mFlags & FLAG_HAS_BINDERS) != 0) { diff --git a/core/java/android/os/CpuHeadroomParams.java b/core/java/android/os/CpuHeadroomParams.java new file mode 100644 index 000000000000..f0d4f7d8737f --- /dev/null +++ b/core/java/android/os/CpuHeadroomParams.java @@ -0,0 +1,91 @@ +/* + * 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 android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.os.health.SystemHealthManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Headroom request params used by {@link SystemHealthManager#getCpuHeadroom(CpuHeadroomParams)}. + */ +@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS) +public final class CpuHeadroomParams { + final CpuHeadroomParamsInternal mInternal; + + public CpuHeadroomParams() { + mInternal = new CpuHeadroomParamsInternal(); + } + + /** @hide */ + @IntDef(flag = false, prefix = {"CPU_HEADROOM_CALCULATION_TYPE_"}, value = { + CPU_HEADROOM_CALCULATION_TYPE_MIN, // 0 + CPU_HEADROOM_CALCULATION_TYPE_AVERAGE, // 1 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CpuHeadroomCalculationType { + } + + /** + * Calculates the headroom based on minimum value over a device-defined window. + */ + public static final int CPU_HEADROOM_CALCULATION_TYPE_MIN = 0; + + /** + * Calculates the headroom based on average value over a device-defined window. + */ + public static final int CPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; + + /** + * Sets the headroom calculation type. + * <p> + * + * @throws IllegalArgumentException if the type is invalid. + */ + public void setCalculationType(@CpuHeadroomCalculationType int calculationType) { + switch (calculationType) { + case CPU_HEADROOM_CALCULATION_TYPE_MIN: + case CPU_HEADROOM_CALCULATION_TYPE_AVERAGE: + mInternal.calculationType = (byte) calculationType; + return; + } + throw new IllegalArgumentException("Invalid calculation type: " + calculationType); + } + + /** + * Gets the headroom calculation type. + * Default to {@link #CPU_HEADROOM_CALCULATION_TYPE_MIN} if not set. + */ + public @CpuHeadroomCalculationType int getCalculationType() { + @CpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) { + case CPU_HEADROOM_CALCULATION_TYPE_MIN, CPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> + mInternal.calculationType; + default -> CPU_HEADROOM_CALCULATION_TYPE_MIN; + }; + return validatedType; + } + + /** + * @hide + */ + public CpuHeadroomParamsInternal getInternal() { + return mInternal; + } +} diff --git a/core/java/android/os/CpuHeadroomParamsInternal.aidl b/core/java/android/os/CpuHeadroomParamsInternal.aidl new file mode 100644 index 000000000000..6cc4699a809e --- /dev/null +++ b/core/java/android/os/CpuHeadroomParamsInternal.aidl @@ -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 android.os; + +import android.hardware.power.CpuHeadroomParams; + +/** + * Changes should be synced with match function of HintManagerService#CpuHeadroomCacheItem. + * {@hide} + */ +@JavaDerive(equals = true, toString = true) +parcelable CpuHeadroomParamsInternal { + boolean usesDeviceHeadroom = false; + CpuHeadroomParams.CalculationType calculationType = CpuHeadroomParams.CalculationType.MIN; + CpuHeadroomParams.SelectionType selectionType = CpuHeadroomParams.SelectionType.ALL; +} + diff --git a/core/java/android/os/GpuHeadroomParams.java b/core/java/android/os/GpuHeadroomParams.java new file mode 100644 index 000000000000..efb2a28ad2b5 --- /dev/null +++ b/core/java/android/os/GpuHeadroomParams.java @@ -0,0 +1,91 @@ +/* + * 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 android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.os.health.SystemHealthManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Headroom request params used by {@link SystemHealthManager#getGpuHeadroom(GpuHeadroomParams)}. + */ +@FlaggedApi(Flags.FLAG_CPU_GPU_HEADROOMS) +public final class GpuHeadroomParams { + final GpuHeadroomParamsInternal mInternal; + + public GpuHeadroomParams() { + mInternal = new GpuHeadroomParamsInternal(); + } + + /** @hide */ + @IntDef(flag = false, prefix = {"GPU_HEADROOM_CALCULATION_TYPE_"}, value = { + GPU_HEADROOM_CALCULATION_TYPE_MIN, // 0 + GPU_HEADROOM_CALCULATION_TYPE_AVERAGE, // 1 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface GpuHeadroomCalculationType { + } + + /** + * Calculates the headroom based on minimum value over a device-defined window. + */ + public static final int GPU_HEADROOM_CALCULATION_TYPE_MIN = 0; + + /** + * Calculates the headroom based on average value over a device-defined window. + */ + public static final int GPU_HEADROOM_CALCULATION_TYPE_AVERAGE = 1; + + /** + * Sets the headroom calculation type. + * <p> + * + * @throws IllegalArgumentException if the type is invalid. + */ + public void setCalculationType(@GpuHeadroomCalculationType int calculationType) { + switch (calculationType) { + case GPU_HEADROOM_CALCULATION_TYPE_MIN: + case GPU_HEADROOM_CALCULATION_TYPE_AVERAGE: + mInternal.calculationType = (byte) calculationType; + return; + } + throw new IllegalArgumentException("Invalid calculation type: " + calculationType); + } + + /** + * Gets the headroom calculation type. + * Default to {@link #GPU_HEADROOM_CALCULATION_TYPE_MIN} if not set. + */ + public @GpuHeadroomCalculationType int getCalculationType() { + @GpuHeadroomCalculationType int validatedType = switch ((int) mInternal.calculationType) { + case GPU_HEADROOM_CALCULATION_TYPE_MIN, GPU_HEADROOM_CALCULATION_TYPE_AVERAGE -> + mInternal.calculationType; + default -> GPU_HEADROOM_CALCULATION_TYPE_MIN; + }; + return validatedType; + } + + /** + * @hide + */ + public GpuHeadroomParamsInternal getInternal() { + return mInternal; + } +} diff --git a/core/java/android/os/GpuHeadroomParamsInternal.aidl b/core/java/android/os/GpuHeadroomParamsInternal.aidl new file mode 100644 index 000000000000..20309e7673f2 --- /dev/null +++ b/core/java/android/os/GpuHeadroomParamsInternal.aidl @@ -0,0 +1,28 @@ +/* + * 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 android.hardware.power.GpuHeadroomParams; + +/** + * Changes should be synced with match function of HintManagerService#GpuHeadroomCacheItem. + * {@hide} + */ +@JavaDerive(equals = true, toString = true) +parcelable GpuHeadroomParamsInternal { + GpuHeadroomParams.CalculationType calculationType = GpuHeadroomParams.CalculationType.MIN; +} diff --git a/core/java/android/os/IHintManager.aidl b/core/java/android/os/IHintManager.aidl index 73cdd5682f31..33120556339f 100644 --- a/core/java/android/os/IHintManager.aidl +++ b/core/java/android/os/IHintManager.aidl @@ -17,6 +17,8 @@ package android.os; +import android.os.CpuHeadroomParamsInternal; +import android.os.GpuHeadroomParamsInternal; import android.os.IHintSession; import android.hardware.power.ChannelConfig; import android.hardware.power.SessionConfig; @@ -50,4 +52,8 @@ interface IHintManager { */ @nullable ChannelConfig getSessionChannel(in IBinder token); oneway void closeSessionChannel(); + float[] getCpuHeadroom(in CpuHeadroomParamsInternal params); + long getCpuHeadroomMinIntervalMillis(); + float getGpuHeadroom(in GpuHeadroomParamsInternal params); + long getGpuHeadroomMinIntervalMillis(); } diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS index 590ddb404b63..24e1d6666093 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -78,6 +78,9 @@ per-file PatternMatcher* = file:/PACKAGE_MANAGER_OWNERS # PermissionEnforcer per-file PermissionEnforcer.java = tweek@google.com, brufino@google.com +# RemoteCallbackList +per-file RemoteCallbackList.java = shayba@google.com + # ART per-file ArtModuleServiceManager.java = file:platform/art:/OWNERS diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 9ab92285c167..5a53bc1552b8 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -5308,7 +5308,13 @@ public class UserManager { Manifest.permission.MANAGE_USERS, Manifest.permission.CREATE_USERS, Manifest.permission.QUERY_USERS}, conditional = true) + @CachedProperty(api = "user_manager_user_data") public List<UserInfo> getProfiles(@UserIdInt int userId) { + if (android.multiuser.Flags.cacheProfilesReadOnly()) { + return UserManagerCache.getProfiles( + (Integer userIdentifier) -> mService.getProfiles(userIdentifier, false), + userId); + } try { return mService.getProfiles(userId, false /* enabledOnly */); } catch (RemoteException re) { @@ -6484,6 +6490,19 @@ public class UserManager { } /** + * This method is used to invalidate caches, when UserManagerService.mUsers + * {@link UserManagerService.UserData} is modified, including changes to {@link UserInfo}. + * In practice we determine modification by when that data is persisted, or scheduled to be + * presisted, to xml. + * @hide + */ + public static final void invalidateCacheOnUserDataChanged() { + if (android.multiuser.Flags.cacheProfilesReadOnly()) { + UserManagerCache.invalidateProfiles(); + } + } + + /** * Returns a serial number on this device for a given userId. User handles can be recycled * when deleting and creating users, but serial numbers are not reused until the device is * wiped. diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index d9db28e0b3c3..118167d02c48 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -66,6 +66,14 @@ flag { } flag { + name: "adpf_use_load_hints" + namespace: "game" + description: "Guards use of the ADPF public load hints behind a readonly flag" + is_fixed_read_only: true + bug: "367803904" +} + +flag { name: "allow_consentless_bugreport_delegated_consent" namespace: "crumpet" description: "Allow privileged apps to call bugreport generation without enforcing user consent and delegate it to the calling app instead" @@ -148,6 +156,13 @@ flag { } flag { + name: "cpu_gpu_headrooms" + namespace: "game" + description: "Feature flag for adding CPU/GPU headroom API" + bug: "346604998" +} + +flag { name: "disallow_cellular_null_ciphers_restriction" namespace: "cellular_security" description: "Guards a new UserManager user restriction that admins can use to require cellular encryption on their managed devices." @@ -253,6 +268,15 @@ flag { flag { namespace: "system_performance" + name: "enable_has_binders" + is_exported: true + description: "Add hasBinders to Public API under a flag." + is_fixed_read_only: true + bug: "330345513" +} + +flag { + namespace: "system_performance" name: "perfetto_sdk_tracing" description: "Tracing using Perfetto SDK." bug: "303199244" diff --git a/core/java/android/os/health/SystemHealthManager.java b/core/java/android/os/health/SystemHealthManager.java index deabfed365a6..4db9bc333e2b 100644 --- a/core/java/android/os/health/SystemHealthManager.java +++ b/core/java/android/os/health/SystemHealthManager.java @@ -17,6 +17,7 @@ package android.os.health; import android.annotation.FlaggedApi; +import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; @@ -25,6 +26,11 @@ import android.content.Context; import android.os.BatteryStats; import android.os.Build; import android.os.Bundle; +import android.os.CpuHeadroomParams; +import android.os.CpuHeadroomParamsInternal; +import android.os.GpuHeadroomParams; +import android.os.GpuHeadroomParamsInternal; +import android.os.IHintManager; import android.os.IPowerStatsService; import android.os.OutcomeReceiver; import android.os.PowerMonitor; @@ -68,6 +74,8 @@ public class SystemHealthManager { private final IBatteryStats mBatteryStats; @Nullable private final IPowerStatsService mPowerStats; + @Nullable + private final IHintManager mHintManager; private List<PowerMonitor> mPowerMonitorsInfo; private final Object mPowerMonitorsLock = new Object(); private static final long TAKE_UID_SNAPSHOT_TIMEOUT_MILLIS = 10_000; @@ -88,14 +96,111 @@ public class SystemHealthManager { public SystemHealthManager() { this(IBatteryStats.Stub.asInterface(ServiceManager.getService(BatteryStats.SERVICE_NAME)), IPowerStatsService.Stub.asInterface( - ServiceManager.getService(Context.POWER_STATS_SERVICE))); + ServiceManager.getService(Context.POWER_STATS_SERVICE)), + IHintManager.Stub.asInterface( + ServiceManager.getService(Context.PERFORMANCE_HINT_SERVICE))); } /** {@hide} */ public SystemHealthManager(@NonNull IBatteryStats batteryStats, - @Nullable IPowerStatsService powerStats) { + @Nullable IPowerStatsService powerStats, @Nullable IHintManager hintManager) { mBatteryStats = batteryStats; mPowerStats = powerStats; + mHintManager = hintManager; + } + + /** + * Provides an estimate of global available CPU headroom of the calling thread. + * <p> + * + * @param params params to customize the CPU headroom calculation, null to use default params. + * @return a single value a {@code Float.NaN} if it's temporarily unavailable. + * A valid value is ranged from [0, 100], where 0 indicates no more CPU resources can be + * granted. + * @throws UnsupportedOperationException if the API is unsupported or the request params can't + * be served. + */ + @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS) + public @FloatRange(from = 0f, to = 100f) float getCpuHeadroom( + @Nullable CpuHeadroomParams params) { + if (mHintManager == null) { + throw new UnsupportedOperationException(); + } + try { + return mHintManager.getCpuHeadroom( + params != null ? params.getInternal() : new CpuHeadroomParamsInternal())[0]; + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + + + /** + * Provides an estimate of global available GPU headroom of the device. + * <p> + * + * @param params params to customize the GPU headroom calculation, null to use default params. + * @return a single value headroom or a {@code Float.NaN} if it's temporarily unavailable. + * A valid value is ranged from [0, 100], where 0 indicates no more GPU resources can be + * granted. + * @throws UnsupportedOperationException if the API is unsupported or the request params can't + * be served. + */ + @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS) + public @FloatRange(from = 0f, to = 100f) float getGpuHeadroom( + @Nullable GpuHeadroomParams params) { + if (mHintManager == null) { + throw new UnsupportedOperationException(); + } + try { + return mHintManager.getGpuHeadroom( + params != null ? params.getInternal() : new GpuHeadroomParamsInternal()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Minimum polling interval for calling {@link #getCpuHeadroom(CpuHeadroomParams)} in + * milliseconds. + * <p> + * The {@link #getCpuHeadroom(CpuHeadroomParams)} API may return cached result if called more + * frequent than the interval. + * + * @throws UnsupportedOperationException if the API is unsupported. + */ + @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS) + public long getCpuHeadroomMinIntervalMillis() { + if (mHintManager == null) { + throw new UnsupportedOperationException(); + } + try { + return mHintManager.getCpuHeadroomMinIntervalMillis(); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Minimum polling interval for calling {@link #getGpuHeadroom(GpuHeadroomParams)} in + * milliseconds. + * <p> + * The {@link #getGpuHeadroom(GpuHeadroomParams)} API may return cached result if called more + * frequent than the interval. + * + * @throws UnsupportedOperationException if the API is unsupported. + */ + @FlaggedApi(android.os.Flags.FLAG_CPU_GPU_HEADROOMS) + public long getGpuHeadroomMinIntervalMillis() { + if (mHintManager == null) { + throw new UnsupportedOperationException(); + } + try { + return mHintManager.getGpuHeadroomMinIntervalMillis(); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } } /** @@ -261,7 +366,7 @@ public class SystemHealthManager { mPowerMonitorsInfo = result; } if (executor != null) { - executor.execute(()-> onResult.accept(result)); + executor.execute(() -> onResult.accept(result)); } else { onResult.accept(result); } diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index ce901217d700..09004b3dcf03 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -115,6 +115,14 @@ flag { } flag { + name: "protect_device_config_flags" + namespace: "psap_ai" + description: "Feature flag to limit adb shell to allowlisted flags" + bug: "364083026" + is_fixed_read_only: true +} + +flag { name: "keystore_grant_api" namespace: "hardware_backed_security" description: "Feature flag for exposing KeyStore grant APIs" diff --git a/core/java/android/security/forensic/ForensicManager.java b/core/java/android/security/forensic/ForensicManager.java new file mode 100644 index 000000000000..9126182eda7b --- /dev/null +++ b/core/java/android/security/forensic/ForensicManager.java @@ -0,0 +1,276 @@ +/* + * 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.security.forensic; + +import static android.Manifest.permission.MANAGE_FORENSIC_STATE; +import static android.Manifest.permission.READ_FORENSIC_STATE; + +import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; +import android.security.Flags; +import android.util.Log; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +/** + * ForensicManager manages the forensic logging on Android devices. + * Upon user consent, forensic logging collects various device events for + * off-device investigation of potential device compromise. + * <p> + * Forensic logging can either be enabled ({@link #STATE_ENABLED} + * or disabled ({@link #STATE_DISABLED}). + * <p> + * The Forensic logs will be transferred to + * {@link android.security.forensic.ForensicEventTransport}. + * + * @hide + */ +@SystemApi +@FlaggedApi(Flags.FLAG_AFL_API) +@SystemService(Context.FORENSIC_SERVICE) +public class ForensicManager { + private static final String TAG = "ForensicManager"; + + /** @hide */ + @Target(ElementType.TYPE_USE) + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "STATE_" }, value = { + STATE_UNKNOWN, + STATE_DISABLED, + STATE_ENABLED + }) + public @interface ForensicState {} + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "ERROR_" }, value = { + ERROR_UNKNOWN, + ERROR_PERMISSION_DENIED, + ERROR_TRANSPORT_UNAVAILABLE, + ERROR_DATA_SOURCE_UNAVAILABLE + }) + public @interface ForensicError {} + + /** + * Indicates an unknown state + */ + public static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN; + + /** + * Indicates an state that the forensic is turned off. + */ + public static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED; + + /** + * Indicates an state that the forensic is turned on. + */ + public static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED; + + /** + * Indicates an unknown error + */ + public static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN; + + /** + * Indicates an error due to insufficient access rights. + */ + public static final int ERROR_PERMISSION_DENIED = + IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED; + + /** + * Indicates an error due to unavailability of the forensic event transport. + */ + public static final int ERROR_TRANSPORT_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE; + + /** + * Indicates an error due to unavailability of the data source. + */ + public static final int ERROR_DATA_SOURCE_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; + + + private final IForensicService mService; + + private final ConcurrentHashMap<Consumer<Integer>, IForensicServiceStateCallback> + mStateCallbacks = new ConcurrentHashMap<>(); + + /** + * Constructor + * + * @param service A valid instance of IForensicService. + * @hide + */ + public ForensicManager(IForensicService service) { + mService = service; + } + + /** + * Add a callback to monitor the state of the ForensicService. + * + * @param executor The executor through which the callback should be invoked. + * @param callback The callback for state change. + * Once the callback is registered, the callback will be called + * to reflect the init state. + * The callback can be registered only once. + */ + @RequiresPermission(READ_FORENSIC_STATE) + public void addStateCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull @ForensicState Consumer<Integer> callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + if (mStateCallbacks.get(callback) != null) { + Log.d(TAG, "addStateCallback callback already present"); + return; + } + + final IForensicServiceStateCallback wrappedCallback = + new IForensicServiceStateCallback.Stub() { + @Override + public void onStateChange(int state) { + executor.execute(() -> callback.accept(state)); + } + }; + try { + mService.addStateCallback(wrappedCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + mStateCallbacks.put(callback, wrappedCallback); + } + + /** + * Remove a callback to monitor the state of the ForensicService. + * + * @param callback The callback to remove. + */ + @RequiresPermission(READ_FORENSIC_STATE) + public void removeStateCallback(@NonNull Consumer<@ForensicState Integer> callback) { + Objects.requireNonNull(callback); + if (!mStateCallbacks.containsKey(callback)) { + Log.d(TAG, "removeStateCallback callback not present"); + return; + } + + IForensicServiceStateCallback wrappedCallback = mStateCallbacks.get(callback); + + try { + mService.removeStateCallback(wrappedCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + mStateCallbacks.remove(callback); + } + + /** + * Enable forensic logging. + * If successful, ForensicService will transition to {@link #STATE_ENABLED} state. + * <p> + * When forensic logging is enabled, various device events will be collected and + * sent over to the registered {@link android.security.forensic.ForensicEventTransport}. + * + * @param executor The executor through which the callback should be invoked. + * @param callback The callback for the command result. + */ + @RequiresPermission(MANAGE_FORENSIC_STATE) + public void enable(@NonNull @CallbackExecutor Executor executor, + @NonNull CommandCallback callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mService.enable(new IForensicServiceCommandCallback.Stub() { + @Override + public void onSuccess() { + executor.execute(callback::onSuccess); + } + + @Override + public void onFailure(int error) { + executor.execute(() -> callback.onFailure(error)); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Disable forensic logging. + * If successful, ForensicService will transition to {@link #STATE_DISABLED}. + * <p> + * When forensic logging is disabled, device events will no longer be collected. + * Any events that have been collected but not yet sent to ForensicEventTransport + * will be transferred as a final batch. + * + * @param executor The executor through which the callback should be invoked. + * @param callback The callback for the command result. + */ + @RequiresPermission(MANAGE_FORENSIC_STATE) + public void disable(@NonNull @CallbackExecutor Executor executor, + @NonNull CommandCallback callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mService.disable(new IForensicServiceCommandCallback.Stub() { + @Override + public void onSuccess() { + executor.execute(callback::onSuccess); + } + + @Override + public void onFailure(int error) { + executor.execute(() -> callback.onFailure(error)); + } + }); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Callback used in {@link #enable} and {@link #disable} to indicate the result of the command. + */ + public interface CommandCallback { + /** + * Called when command succeeds. + */ + void onSuccess(); + + /** + * Called when command fails. + * @param error The error number. + */ + void onFailure(@ForensicError int error); + } +} diff --git a/core/java/android/security/forensic/IBackupTransport.aidl b/core/java/android/security/forensic/IForensicEventTransport.aidl index c2cbc83ba1b3..80e78eb9cf49 100644 --- a/core/java/android/security/forensic/IBackupTransport.aidl +++ b/core/java/android/security/forensic/IForensicEventTransport.aidl @@ -20,7 +20,7 @@ import android.security.forensic.ForensicEvent; import com.android.internal.infra.AndroidFuture; /** {@hide} */ -oneway interface IBackupTransport { +oneway interface IForensicEventTransport { /** * Initialize the server side. */ diff --git a/core/java/android/security/forensic/IForensicService.aidl b/core/java/android/security/forensic/IForensicService.aidl index a944b18cb26d..8039b264f0e5 100644 --- a/core/java/android/security/forensic/IForensicService.aidl +++ b/core/java/android/security/forensic/IForensicService.aidl @@ -24,9 +24,12 @@ import android.security.forensic.IForensicServiceStateCallback; * @hide */ interface IForensicService { - void monitorState(IForensicServiceStateCallback callback); - void makeVisible(IForensicServiceCommandCallback callback); - void makeInvisible(IForensicServiceCommandCallback callback); + @EnforcePermission("READ_FORENSIC_STATE") + void addStateCallback(IForensicServiceStateCallback callback); + @EnforcePermission("READ_FORENSIC_STATE") + void removeStateCallback(IForensicServiceStateCallback callback); + @EnforcePermission("MANAGE_FORENSIC_STATE") void enable(IForensicServiceCommandCallback callback); + @EnforcePermission("MANAGE_FORENSIC_STATE") void disable(IForensicServiceCommandCallback callback); } diff --git a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl index 7fa0c7f72690..6d1456ea0426 100644 --- a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl +++ b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl @@ -25,8 +25,8 @@ package android.security.forensic; UNKNOWN = 0, PERMISSION_DENIED = 1, INVALID_STATE_TRANSITION = 2, - BACKUP_TRANSPORT_UNAVAILABLE = 3, - DATA_SOURCE_UNAVAILABLE = 3, + TRANSPORT_UNAVAILABLE = 3, + DATA_SOURCE_UNAVAILABLE = 4, } void onSuccess(); void onFailure(ErrorCode error); diff --git a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl index 0cda35083ffd..1b68c7b14bca 100644 --- a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl +++ b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl @@ -23,9 +23,8 @@ package android.security.forensic; @Backing(type="int") enum State{ UNKNOWN = 0, - INVISIBLE = 1, - VISIBLE = 2, - ENABLED = 3, + DISABLED = 1, + ENABLED = 2, } void onStateChange(State state); } diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig index 66e1f38621ae..6c92991ceff6 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -103,3 +103,10 @@ flag { description: "Applies intentMatchingFlags while matching intents to application components" bug: "364354494" } + +flag { + name: "aapm_feature_disable_install_unknown_sources" + namespace: "responsible_apis" + description: "Android Advanced Protection Mode Feature: Disable Install Unknown Sources" + bug: "369361373" +} diff --git a/core/java/android/service/quickaccesswallet/flags.aconfig b/core/java/android/service/quickaccesswallet/flags.aconfig index 07311d5ffbe1..75a93091eec3 100644 --- a/core/java/android/service/quickaccesswallet/flags.aconfig +++ b/core/java/android/service/quickaccesswallet/flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "launch_wallet_option_on_power_double_tap" - namespace: "wallet_integrations" + namespace: "wallet_integration" description: "Option to launch the Wallet app on double-tap of the power button" bug: "378469025" }
\ No newline at end of file diff --git a/core/java/android/telephony/SubscriptionPlan.java b/core/java/android/telephony/SubscriptionPlan.java index 7b48a16c2227..4c59a8589df2 100644 --- a/core/java/android/telephony/SubscriptionPlan.java +++ b/core/java/android/telephony/SubscriptionPlan.java @@ -18,6 +18,7 @@ package android.telephony; import android.annotation.BytesLong; import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -28,6 +29,7 @@ import android.telephony.Annotation.NetworkType; import android.util.Range; import android.util.RecurrenceRule; +import com.android.internal.telephony.flags.Flags; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; @@ -83,6 +85,33 @@ public final class SubscriptionPlan implements Parcelable { /** Value indicating a timestamp is unknown. */ public static final long TIME_UNKNOWN = -1; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "SUBSCRIPTION_STATUS_" }, value = { + SUBSCRIPTION_STATUS_UNKNOWN, + SUBSCRIPTION_STATUS_ACTIVE, + SUBSCRIPTION_STATUS_INACTIVE, + SUBSCRIPTION_STATUS_TRIAL, + SUBSCRIPTION_STATUS_SUSPENDED + }) + public @interface SubscriptionStatus {} + + /** Subscription status is unknown. */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public static final int SUBSCRIPTION_STATUS_UNKNOWN = 0; + /** Subscription is active. */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public static final int SUBSCRIPTION_STATUS_ACTIVE = 1; + /** Subscription is inactive. */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public static final int SUBSCRIPTION_STATUS_INACTIVE = 2; + /** Subscription is in a trial period. */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public static final int SUBSCRIPTION_STATUS_TRIAL = 3; + /** Subscription is suspended. */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public static final int SUBSCRIPTION_STATUS_SUSPENDED = 4; + private final RecurrenceRule cycleRule; private CharSequence title; private CharSequence summary; @@ -91,6 +120,7 @@ public final class SubscriptionPlan implements Parcelable { private long dataUsageBytes = BYTES_UNKNOWN; private long dataUsageTime = TIME_UNKNOWN; private @NetworkType int[] networkTypes; + private int mSubscriptionStatus = SUBSCRIPTION_STATUS_UNKNOWN; private SubscriptionPlan(RecurrenceRule cycleRule) { this.cycleRule = Preconditions.checkNotNull(cycleRule); @@ -107,6 +137,7 @@ public final class SubscriptionPlan implements Parcelable { dataUsageBytes = source.readLong(); dataUsageTime = source.readLong(); networkTypes = source.createIntArray(); + mSubscriptionStatus = source.readInt(); } @Override @@ -124,6 +155,7 @@ public final class SubscriptionPlan implements Parcelable { dest.writeLong(dataUsageBytes); dest.writeLong(dataUsageTime); dest.writeIntArray(networkTypes); + dest.writeInt(mSubscriptionStatus); } @Override @@ -137,13 +169,14 @@ public final class SubscriptionPlan implements Parcelable { .append(" dataUsageBytes=").append(dataUsageBytes) .append(" dataUsageTime=").append(dataUsageTime) .append(" networkTypes=").append(Arrays.toString(networkTypes)) + .append(" subscriptionStatus=").append(mSubscriptionStatus) .append("}").toString(); } @Override public int hashCode() { return Objects.hash(cycleRule, title, summary, dataLimitBytes, dataLimitBehavior, - dataUsageBytes, dataUsageTime, Arrays.hashCode(networkTypes)); + dataUsageBytes, dataUsageTime, Arrays.hashCode(networkTypes), mSubscriptionStatus); } @Override @@ -157,7 +190,8 @@ public final class SubscriptionPlan implements Parcelable { && dataLimitBehavior == other.dataLimitBehavior && dataUsageBytes == other.dataUsageBytes && dataUsageTime == other.dataUsageTime - && Arrays.equals(networkTypes, other.networkTypes); + && Arrays.equals(networkTypes, other.networkTypes) + && mSubscriptionStatus == other.mSubscriptionStatus; } return false; } @@ -179,6 +213,13 @@ public final class SubscriptionPlan implements Parcelable { return cycleRule; } + /** Return the end date of this plan, or null if no end date exists. */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public @Nullable ZonedDateTime getPlanEndDate() { + // ZonedDateTime is immutable, so no need to create a defensive copy. + return cycleRule.end; + } + /** Return the short title of this plan. */ public @Nullable CharSequence getTitle() { return title; @@ -238,6 +279,16 @@ public final class SubscriptionPlan implements Parcelable { } /** + * Returns the status of the subscription plan. + * + * @return The subscription status, or {@link #SUBSCRIPTION_STATUS_UNKNOWN} if not available. + */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public @SubscriptionStatus int getSubscriptionStatus() { + return mSubscriptionStatus; + } + + /** * Builder for a {@link SubscriptionPlan}. */ public static class Builder { @@ -382,5 +433,21 @@ public final class SubscriptionPlan implements Parcelable { TelephonyManager.getAllNetworkTypes().length); return this; } + + /** + * Set the subscription status. + * + * @param subscriptionStatus the current subscription status + */ + @FlaggedApi(Flags.FLAG_SUBSCRIPTION_PLAN_ALLOW_STATUS_AND_END_DATE) + public @NonNull Builder setSubscriptionStatus(@SubscriptionStatus int subscriptionStatus) { + if (subscriptionStatus < SUBSCRIPTION_STATUS_UNKNOWN + || subscriptionStatus > SUBSCRIPTION_STATUS_SUSPENDED) { + throw new IllegalArgumentException( + "Subscription status must be defined with a valid value"); + } + plan.mSubscriptionStatus = subscriptionStatus; + return this; + } } } diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java deleted file mode 100644 index f69a333ff81f..000000000000 --- a/core/java/android/text/TextFlags.java +++ /dev/null @@ -1,81 +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 android.text; - -import android.annotation.NonNull; -import android.app.AppGlobals; - -/** - * Flags in the "text" namespace. - * - * TODO(nona): Remove this class. - * @hide - */ -public final class TextFlags { - - /** - * The name space of the "text" feature. - * - * This needs to move to DeviceConfig constant. - */ - public static final String NAMESPACE = "text"; - - /** - * Whether we use the new design of context menu. - */ - public static final String ENABLE_NEW_CONTEXT_MENU = - "TextEditing__enable_new_context_menu"; - - /** - * The key name used in app core settings for {@link #ENABLE_NEW_CONTEXT_MENU}. - */ - public static final String KEY_ENABLE_NEW_CONTEXT_MENU = "text__enable_new_context_menu"; - - /** - * Default value for the flag {@link #ENABLE_NEW_CONTEXT_MENU}. - */ - public static final boolean ENABLE_NEW_CONTEXT_MENU_DEFAULT = true; - - /** - * List of text flags to be transferred to the application process. - */ - public static final String[] TEXT_ACONFIGS_FLAGS = { - }; - - /** - * List of the default values of the text flags. - * - * The order must be the same to the TEXT_ACONFIG_FLAGS. - */ - public static final boolean[] TEXT_ACONFIG_DEFAULT_VALUE = { - }; - - /** - * Get a key for the feature flag. - */ - public static String getKeyForFlag(@NonNull String flag) { - return "text__" + flag; - } - - /** - * Return true if the feature flag is enabled. - */ - public static boolean isFeatureEnabled(@NonNull String flag) { - return AppGlobals.getIntCoreSetting( - getKeyForFlag(flag), 0 /* aconfig is false by default */) != 0; - } -} diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 68efa79715a3..d56768d2db03 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -4566,14 +4566,31 @@ public final class SurfaceControl implements Parcelable { return this; } - /** @hide */ + /** + * Sets the Luts for the layer. + * + * <p> The function also allows to clear previously applied lut(s). To do this, + * set the displayluts to be either {@code nullptr} or + * an empty {@link android.hardware.DisplayLuts} instance. + * + * @param sc The SurfaceControl to update + * + * @param displayLuts The selected Lut(s) + * + * @return this + * @see DisplayLuts + */ + @FlaggedApi(android.hardware.flags.Flags.FLAG_LUTS_API) public @NonNull Transaction setLuts(@NonNull SurfaceControl sc, - @NonNull DisplayLuts displayLuts) { + @Nullable DisplayLuts displayLuts) { checkPreconditions(sc); - - nativeSetLuts(mNativeObject, sc.mNativeObject, displayLuts.getLutBuffers(), - displayLuts.getOffsets(), displayLuts.getLutDimensions(), - displayLuts.getLutSizes(), displayLuts.getLutSamplingKeys()); + if (displayLuts != null && displayLuts.valid()) { + nativeSetLuts(mNativeObject, sc.mNativeObject, displayLuts.getLutBuffers(), + displayLuts.getOffsets(), displayLuts.getLutDimensions(), + displayLuts.getLutSizes(), displayLuts.getLutSamplingKeys()); + } else { + nativeSetLuts(mNativeObject, sc.mNativeObject, null, null, null, null, null); + } return this; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e50662adc3f1..19d3dc4df04e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2516,6 +2516,11 @@ public final class ViewRootImpl implements ViewParent, public void notifyInsetsAnimationRunningStateChanged(boolean running) { if (sToolkitSetFrameRateReadOnlyFlagValue) { mInsetsAnimationRunning = running; + if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + Trace.instant(Trace.TRACE_TAG_VIEW, + TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)", + Boolean.toString(running))); + } } } diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index a5603399142b..afe195c48b01 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -523,7 +523,6 @@ public class EditorInfo implements InputType, Parcelable { @Nullable public LocaleList hintLocales = null; - /** * List of acceptable MIME types for * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)}. @@ -758,6 +757,30 @@ public class EditorInfo implements InputType, Parcelable { return mIsStylusHandwritingEnabled; } + private boolean mWritingToolsEnabled = true; + + /** + * Returns {@code true} when an {@code Editor} has writing tools enabled. + * {@code true} by default for all editors. Toolkits can optionally disable them where not + * relevant e.g. passwords, number input, etc. + * @see #setWritingToolsEnabled(boolean) + */ + @FlaggedApi(Flags.FLAG_WRITING_TOOLS) + public boolean isWritingToolsEnabled() { + return mWritingToolsEnabled; + } + + /** + * Set {@code false} if {@code Editor} opts-out of writing tools, that enable IMEs to replace + * text with generative AI text. + * @param enabled set {@code true} to enabled or {@code false to disable} support. + * @see #isWritingToolsEnabled() + */ + @FlaggedApi(Flags.FLAG_WRITING_TOOLS) + public void setWritingToolsEnabled(boolean enabled) { + mWritingToolsEnabled = enabled; + } + /** * If not {@code null}, this editor needs to talk to IMEs that run for the specified user, no * matter what user ID the calling process has. @@ -1276,6 +1299,7 @@ public class EditorInfo implements InputType, Parcelable { + InputMethodDebug.handwritingGestureTypeFlagsToString( mSupportedHandwritingGesturePreviewTypes)); pw.println(prefix + "isStylusHandwritingEnabled=" + mIsStylusHandwritingEnabled); + pw.println(prefix + "writingToolsEnabled=" + mWritingToolsEnabled); pw.println(prefix + "contentMimeTypes=" + Arrays.toString(contentMimeTypes)); if (targetInputMethodUser != null) { pw.println(prefix + "targetInputMethodUserId=" + targetInputMethodUser.getIdentifier()); @@ -1356,6 +1380,7 @@ public class EditorInfo implements InputType, Parcelable { } dest.writeStringArray(contentMimeTypes); UserHandle.writeToParcel(targetInputMethodUser, dest); + dest.writeBoolean(mWritingToolsEnabled); } /** @@ -1396,6 +1421,7 @@ public class EditorInfo implements InputType, Parcelable { res.hintLocales = hintLocales.isEmpty() ? null : hintLocales; res.contentMimeTypes = source.readStringArray(); res.targetInputMethodUser = UserHandle.readFromParcel(source); + res.mWritingToolsEnabled = source.readBoolean(); return res; } diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java index 4f48cb684e8c..0f48f1246cf5 100644 --- a/core/java/android/view/inputmethod/InputMethodSession.java +++ b/core/java/android/view/inputmethod/InputMethodSession.java @@ -16,6 +16,7 @@ package android.view.inputmethod; +import android.annotation.NonNull; import android.graphics.Rect; import android.inputmethodservice.InputMethodService; import android.os.Bundle; @@ -125,6 +126,11 @@ public interface InputMethodSession { public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback); /** + * @hide + */ + boolean onShouldVerifyKeyEvent(@NonNull KeyEvent event); + + /** * This method is called when there is a track ball event. * * <p> diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index e619ab064005..63f8c8024e59 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -175,3 +175,11 @@ flag { bug: "342672560" is_fixed_read_only: true } + +flag { + name: "verify_key_event" + namespace: "input_method" + description: "Verify KeyEvents in IME" + bug: "331730488" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index d7750bd412a3..cb70466fcd81 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -106,6 +106,7 @@ import android.os.Parcelable; import android.os.ParcelableParcel; import android.os.Process; import android.os.SystemClock; +import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; import android.text.BoringLayout; @@ -9229,174 +9230,179 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected void onDraw(Canvas canvas) { - restartMarqueeIfNeeded(); + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "TextView.onDraw"); + try { + restartMarqueeIfNeeded(); - // Draw the background for this view - super.onDraw(canvas); - - final int compoundPaddingLeft = getCompoundPaddingLeft(); - final int compoundPaddingTop = getCompoundPaddingTop(); - final int compoundPaddingRight = getCompoundPaddingRight(); - final int compoundPaddingBottom = getCompoundPaddingBottom(); - final int scrollX = mScrollX; - final int scrollY = mScrollY; - final int right = mRight; - final int left = mLeft; - final int bottom = mBottom; - final int top = mTop; - final boolean isLayoutRtl = isLayoutRtl(); - final int offset = getHorizontalOffsetForDrawables(); - final int leftOffset = isLayoutRtl ? 0 : offset; - final int rightOffset = isLayoutRtl ? offset : 0; + // Draw the background for this view + super.onDraw(canvas); - final Drawables dr = mDrawables; - if (dr != null) { - /* - * Compound, not extended, because the icon is not clipped - * if the text height is smaller. - */ + final int compoundPaddingLeft = getCompoundPaddingLeft(); + final int compoundPaddingTop = getCompoundPaddingTop(); + final int compoundPaddingRight = getCompoundPaddingRight(); + final int compoundPaddingBottom = getCompoundPaddingBottom(); + final int scrollX = mScrollX; + final int scrollY = mScrollY; + final int right = mRight; + final int left = mLeft; + final int bottom = mBottom; + final int top = mTop; + final boolean isLayoutRtl = isLayoutRtl(); + final int offset = getHorizontalOffsetForDrawables(); + final int leftOffset = isLayoutRtl ? 0 : offset; + final int rightOffset = isLayoutRtl ? offset : 0; - int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; - int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; + final Drawables dr = mDrawables; + if (dr != null) { + /* + * Compound, not extended, because the icon is not clipped + * if the text height is smaller. + */ - // IMPORTANT: The coordinates computed are also used in invalidateDrawable() - // Make sure to update invalidateDrawable() when changing this code. - if (dr.mShowing[Drawables.LEFT] != null) { - canvas.save(); - canvas.translate(scrollX + mPaddingLeft + leftOffset, - scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2); - dr.mShowing[Drawables.LEFT].draw(canvas); - canvas.restore(); - } + int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; + int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; + + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. + if (dr.mShowing[Drawables.LEFT] != null) { + canvas.save(); + canvas.translate(scrollX + mPaddingLeft + leftOffset, + scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2); + dr.mShowing[Drawables.LEFT].draw(canvas); + canvas.restore(); + } - // IMPORTANT: The coordinates computed are also used in invalidateDrawable() - // Make sure to update invalidateDrawable() when changing this code. - if (dr.mShowing[Drawables.RIGHT] != null) { - canvas.save(); - canvas.translate(scrollX + right - left - mPaddingRight - - dr.mDrawableSizeRight - rightOffset, - scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); - dr.mShowing[Drawables.RIGHT].draw(canvas); - canvas.restore(); - } + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. + if (dr.mShowing[Drawables.RIGHT] != null) { + canvas.save(); + canvas.translate(scrollX + right - left - mPaddingRight + - dr.mDrawableSizeRight - rightOffset, + scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2); + dr.mShowing[Drawables.RIGHT].draw(canvas); + canvas.restore(); + } - // IMPORTANT: The coordinates computed are also used in invalidateDrawable() - // Make sure to update invalidateDrawable() when changing this code. - if (dr.mShowing[Drawables.TOP] != null) { - canvas.save(); - canvas.translate(scrollX + compoundPaddingLeft - + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop); - dr.mShowing[Drawables.TOP].draw(canvas); - canvas.restore(); - } + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. + if (dr.mShowing[Drawables.TOP] != null) { + canvas.save(); + canvas.translate(scrollX + compoundPaddingLeft + + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop); + dr.mShowing[Drawables.TOP].draw(canvas); + canvas.restore(); + } - // IMPORTANT: The coordinates computed are also used in invalidateDrawable() - // Make sure to update invalidateDrawable() when changing this code. - if (dr.mShowing[Drawables.BOTTOM] != null) { - canvas.save(); - canvas.translate(scrollX + compoundPaddingLeft - + (hspace - dr.mDrawableWidthBottom) / 2, - scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); - dr.mShowing[Drawables.BOTTOM].draw(canvas); - canvas.restore(); + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. + if (dr.mShowing[Drawables.BOTTOM] != null) { + canvas.save(); + canvas.translate(scrollX + compoundPaddingLeft + + (hspace - dr.mDrawableWidthBottom) / 2, + scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom); + dr.mShowing[Drawables.BOTTOM].draw(canvas); + canvas.restore(); + } } - } - int color = mCurTextColor; + int color = mCurTextColor; - if (mLayout == null) { - assumeLayout(); - } + if (mLayout == null) { + assumeLayout(); + } + + Layout layout = mLayout; - Layout layout = mLayout; + if (mHint != null && !mHideHint && mText.length() == 0) { + if (mHintTextColor != null) { + color = mCurHintTextColor; + } - if (mHint != null && !mHideHint && mText.length() == 0) { - if (mHintTextColor != null) { - color = mCurHintTextColor; + layout = mHintLayout; } - layout = mHintLayout; - } + mTextPaint.setColor(color); + mTextPaint.drawableState = getDrawableState(); - mTextPaint.setColor(color); - mTextPaint.drawableState = getDrawableState(); + canvas.save(); + /* Would be faster if we didn't have to do this. Can we chop the + (displayable) text so that we don't need to do this ever? + */ - canvas.save(); - /* Would be faster if we didn't have to do this. Can we chop the - (displayable) text so that we don't need to do this ever? - */ + int extendedPaddingTop = getExtendedPaddingTop(); + int extendedPaddingBottom = getExtendedPaddingBottom(); - int extendedPaddingTop = getExtendedPaddingTop(); - int extendedPaddingBottom = getExtendedPaddingBottom(); + final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; + final int maxScrollY = mLayout.getHeight() - vspace; - final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; - final int maxScrollY = mLayout.getHeight() - vspace; + float clipLeft = compoundPaddingLeft + scrollX; + float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY; + float clipRight = right - left - getCompoundPaddingRight() + scrollX; + float clipBottom = bottom - top + scrollY + - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom); - float clipLeft = compoundPaddingLeft + scrollX; - float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY; - float clipRight = right - left - getCompoundPaddingRight() + scrollX; - float clipBottom = bottom - top + scrollY - - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom); + if (mShadowRadius != 0) { + clipLeft += Math.min(0, mShadowDx - mShadowRadius); + clipRight += Math.max(0, mShadowDx + mShadowRadius); - if (mShadowRadius != 0) { - clipLeft += Math.min(0, mShadowDx - mShadowRadius); - clipRight += Math.max(0, mShadowDx + mShadowRadius); + clipTop += Math.min(0, mShadowDy - mShadowRadius); + clipBottom += Math.max(0, mShadowDy + mShadowRadius); + } - clipTop += Math.min(0, mShadowDy - mShadowRadius); - clipBottom += Math.max(0, mShadowDy + mShadowRadius); - } + canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); - canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); + int voffsetText = 0; + int voffsetCursor = 0; - int voffsetText = 0; - int voffsetCursor = 0; + // translate in by our padding + /* shortcircuit calling getVerticaOffset() */ + if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { + voffsetText = getVerticalOffset(false); + voffsetCursor = getVerticalOffset(true); + } + canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); + + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); + if (isMarqueeFadeEnabled()) { + if (!mSingleLine && getLineCount() == 1 && canMarquee() + && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { + final int width = mRight - mLeft; + final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight(); + final float dx = mLayout.getLineRight(0) - (width - padding); + canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); + } - // translate in by our padding - /* shortcircuit calling getVerticaOffset() */ - if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) { - voffsetText = getVerticalOffset(false); - voffsetCursor = getVerticalOffset(true); - } - canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); - - final int layoutDirection = getLayoutDirection(); - final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); - if (isMarqueeFadeEnabled()) { - if (!mSingleLine && getLineCount() == 1 && canMarquee() - && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { - final int width = mRight - mLeft; - final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight(); - final float dx = mLayout.getLineRight(0) - (width - padding); - canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); + if (mMarquee != null && mMarquee.isRunning()) { + final float dx = -mMarquee.getScroll(); + canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); + } } - if (mMarquee != null && mMarquee.isRunning()) { - final float dx = -mMarquee.getScroll(); - canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); - } - } + final int cursorOffsetVertical = voffsetCursor - voffsetText; - final int cursorOffsetVertical = voffsetCursor - voffsetText; + maybeUpdateHighlightPaths(); + // If there is a gesture preview highlight, then the selection or cursor is not drawn. + Path highlight = hasGesturePreviewHighlight() ? null : getUpdatedHighlightPath(); + if (mEditor != null) { + mEditor.onDraw(canvas, layout, mHighlightPaths, mHighlightPaints, highlight, + mHighlightPaint, cursorOffsetVertical); + } else { + layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint, + cursorOffsetVertical); + } - maybeUpdateHighlightPaths(); - // If there is a gesture preview highlight, then the selection or cursor is not drawn. - Path highlight = hasGesturePreviewHighlight() ? null : getUpdatedHighlightPath(); - if (mEditor != null) { - mEditor.onDraw(canvas, layout, mHighlightPaths, mHighlightPaints, highlight, - mHighlightPaint, cursorOffsetVertical); - } else { - layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint, - cursorOffsetVertical); - } + if (mMarquee != null && mMarquee.shouldDrawGhost()) { + final float dx = mMarquee.getGhostOffset(); + canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); + layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint, + cursorOffsetVertical); + } - if (mMarquee != null && mMarquee.shouldDrawGhost()) { - final float dx = mMarquee.getGhostOffset(); - canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f); - layout.draw(canvas, mHighlightPaths, mHighlightPaints, highlight, mHighlightPaint, - cursorOffsetVertical); + canvas.restore(); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); } - - canvas.restore(); } @Override @@ -11254,192 +11260,201 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - int widthSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSize = MeasureSpec.getSize(heightMeasureSpec); - - int width; - int height; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "TextView.onMeasure"); + try { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); - BoringLayout.Metrics boring = UNKNOWN_BORING; - BoringLayout.Metrics hintBoring = UNKNOWN_BORING; + int width; + int height; - if (mTextDir == null) { - mTextDir = getTextDirectionHeuristic(); - } + BoringLayout.Metrics boring = UNKNOWN_BORING; + BoringLayout.Metrics hintBoring = UNKNOWN_BORING; - int des = -1; - boolean fromexisting = false; - final float widthLimit = (widthMode == MeasureSpec.AT_MOST) - ? (float) widthSize : Float.MAX_VALUE; - - if (widthMode == MeasureSpec.EXACTLY) { - // Parent has told us how big to be. So be it. - width = widthSize; - } else { - if (mLayout != null && mEllipsize == null) { - des = desired(mLayout, mUseBoundsForWidth); + if (mTextDir == null) { + mTextDir = getTextDirectionHeuristic(); } - if (des < 0) { - boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(), - mBoring); - if (boring != null) { - mBoring = boring; - } - } else { - fromexisting = true; - } + int des = -1; + boolean fromexisting = false; + final float widthLimit = (widthMode == MeasureSpec.AT_MOST) + ? (float) widthSize : Float.MAX_VALUE; - if (boring == null || boring == UNKNOWN_BORING) { - if (des < 0) { - des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0, - mTransformed.length(), mTextPaint, mTextDir, widthLimit, - mUseBoundsForWidth)); - } - width = des; + if (widthMode == MeasureSpec.EXACTLY) { + // Parent has told us how big to be. So be it. + width = widthSize; } else { - if (mUseBoundsForWidth) { - RectF bbox = boring.getDrawingBoundingBox(); - float rightMax = Math.max(bbox.right, boring.width); - float leftMin = Math.min(bbox.left, 0); - width = Math.max(boring.width, (int) Math.ceil(rightMax - leftMin)); - } else { - width = boring.width; + if (mLayout != null && mEllipsize == null) { + des = desired(mLayout, mUseBoundsForWidth); } - } - - final Drawables dr = mDrawables; - if (dr != null) { - width = Math.max(width, dr.mDrawableWidthTop); - width = Math.max(width, dr.mDrawableWidthBottom); - } - - if (mHint != null) { - int hintDes = -1; - int hintWidth; - if (mHintLayout != null && mEllipsize == null) { - hintDes = desired(mHintLayout, mUseBoundsForWidth); - } - - if (hintDes < 0) { - hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, + if (des < 0) { + boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(), - mHintBoring); - if (hintBoring != null) { - mHintBoring = hintBoring; + mBoring); + if (boring != null) { + mBoring = boring; } + } else { + fromexisting = true; } - if (hintBoring == null || hintBoring == UNKNOWN_BORING) { - if (hintDes < 0) { - hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0, - mHint.length(), mTextPaint, mTextDir, widthLimit, + if (boring == null || boring == UNKNOWN_BORING) { + if (des < 0) { + des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0, + mTransformed.length(), mTextPaint, mTextDir, widthLimit, mUseBoundsForWidth)); } - hintWidth = hintDes; + width = des; } else { - hintWidth = hintBoring.width; + if (mUseBoundsForWidth) { + RectF bbox = boring.getDrawingBoundingBox(); + float rightMax = Math.max(bbox.right, boring.width); + float leftMin = Math.min(bbox.left, 0); + width = Math.max(boring.width, (int) Math.ceil(rightMax - leftMin)); + } else { + width = boring.width; + } } - if (hintWidth > width) { - width = hintWidth; + final Drawables dr = mDrawables; + if (dr != null) { + width = Math.max(width, dr.mDrawableWidthTop); + width = Math.max(width, dr.mDrawableWidthBottom); } - } - width += getCompoundPaddingLeft() + getCompoundPaddingRight(); + if (mHint != null) { + int hintDes = -1; + int hintWidth; - if (mMaxWidthMode == EMS) { - width = Math.min(width, mMaxWidth * getLineHeight()); - } else { - width = Math.min(width, mMaxWidth); - } + if (mHintLayout != null && mEllipsize == null) { + hintDes = desired(mHintLayout, mUseBoundsForWidth); + } - if (mMinWidthMode == EMS) { - width = Math.max(width, mMinWidth * getLineHeight()); - } else { - width = Math.max(width, mMinWidth); - } + if (hintDes < 0) { + hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, + isFallbackLineSpacingForBoringLayout(), + getResolvedMinimumFontMetrics(), + mHintBoring); + if (hintBoring != null) { + mHintBoring = hintBoring; + } + } - // Check against our minimum width - width = Math.max(width, getSuggestedMinimumWidth()); + if (hintBoring == null || hintBoring == UNKNOWN_BORING) { + if (hintDes < 0) { + hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0, + mHint.length(), mTextPaint, mTextDir, widthLimit, + mUseBoundsForWidth)); + } + hintWidth = hintDes; + } else { + hintWidth = hintBoring.width; + } - if (widthMode == MeasureSpec.AT_MOST) { - width = Math.min(widthSize, width); - } - } + if (hintWidth > width) { + width = hintWidth; + } + } - int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); - int unpaddedWidth = want; + width += getCompoundPaddingLeft() + getCompoundPaddingRight(); - if (mHorizontallyScrolling) want = VERY_WIDE; + if (mMaxWidthMode == EMS) { + width = Math.min(width, mMaxWidth * getLineHeight()); + } else { + width = Math.min(width, mMaxWidth); + } - int hintWant = want; - int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth(); + if (mMinWidthMode == EMS) { + width = Math.max(width, mMinWidth * getLineHeight()); + } else { + width = Math.max(width, mMinWidth); + } - if (mLayout == null) { - makeNewLayout(want, hintWant, boring, hintBoring, - width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); - } else { - final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant) - || (mLayout.getEllipsizedWidth() - != width - getCompoundPaddingLeft() - getCompoundPaddingRight()); + // Check against our minimum width + width = Math.max(width, getSuggestedMinimumWidth()); + + if (widthMode == MeasureSpec.AT_MOST) { + width = Math.min(widthSize, width); + } + } - final boolean widthChanged = (mHint == null) && (mEllipsize == null) - && (want > mLayout.getWidth()) - && (mLayout instanceof BoringLayout - || (fromexisting && des >= 0 && des <= want)); + int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight(); + int unpaddedWidth = want; - final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum); + if (mHorizontallyScrolling) want = VERY_WIDE; - if (layoutChanged || maximumChanged) { - if (!maximumChanged && widthChanged) { - mLayout.increaseWidthTo(want); + int hintWant = want; + int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth(); + + if (mLayout == null) { + makeNewLayout(want, hintWant, boring, hintBoring, + width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); + } else { + final boolean layoutChanged = + (mLayout.getWidth() != want) || (hintWidth != hintWant) + || (mLayout.getEllipsizedWidth() + != width - getCompoundPaddingLeft() - getCompoundPaddingRight()); + + final boolean widthChanged = (mHint == null) && (mEllipsize == null) + && (want > mLayout.getWidth()) + && (mLayout instanceof BoringLayout + || (fromexisting && des >= 0 && des <= want)); + + final boolean maximumChanged = + (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum); + + if (layoutChanged || maximumChanged) { + if (!maximumChanged && widthChanged) { + mLayout.increaseWidthTo(want); + } else { + makeNewLayout(want, hintWant, boring, hintBoring, + width - getCompoundPaddingLeft() - getCompoundPaddingRight(), + false); + } } else { - makeNewLayout(want, hintWant, boring, hintBoring, - width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); + // Nothing has changed } - } else { - // Nothing has changed } - } - if (heightMode == MeasureSpec.EXACTLY) { - // Parent has told us how big to be. So be it. - height = heightSize; - mDesiredHeightAtMeasure = -1; - } else { - int desired = getDesiredHeight(); + if (heightMode == MeasureSpec.EXACTLY) { + // Parent has told us how big to be. So be it. + height = heightSize; + mDesiredHeightAtMeasure = -1; + } else { + int desired = getDesiredHeight(); - height = desired; - mDesiredHeightAtMeasure = desired; + height = desired; + mDesiredHeightAtMeasure = desired; - if (heightMode == MeasureSpec.AT_MOST) { - height = Math.min(desired, heightSize); + if (heightMode == MeasureSpec.AT_MOST) { + height = Math.min(desired, heightSize); + } } - } - int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); - if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { - unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum)); - } + int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); + if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { + unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum)); + } - /* - * We didn't let makeNewLayout() register to bring the cursor into view, - * so do it here if there is any possibility that it is needed. - */ - if (mMovement != null - || mLayout.getWidth() > unpaddedWidth - || mLayout.getHeight() > unpaddedHeight) { - registerForPreDraw(); - } else { - scrollTo(0, 0); - } + /* + * We didn't let makeNewLayout() register to bring the cursor into view, + * so do it here if there is any possibility that it is needed. + */ + if (mMovement != null + || mLayout.getWidth() > unpaddedWidth + || mLayout.getHeight() > unpaddedHeight) { + registerForPreDraw(); + } else { + scrollTo(0, 0); + } - setMeasuredDimension(width, height); + setMeasuredDimension(width, height); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } } /** diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 0f2dd10d7f47..2c21417fb790 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -49,6 +49,7 @@ import android.content.ComponentName; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.os.BinderProxy; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -1089,8 +1090,13 @@ public final class TransitionInfo implements Parcelable { @Override public String toString() { final StringBuilder sb = new StringBuilder(); - sb.append('{'); sb.append(mContainer); - sb.append(" m="); sb.append(modeToString(mMode)); + sb.append('{'); + if (mContainer != null && !(mContainer.asBinder() instanceof BinderProxy)) { + // Only log the token if it is not a binder proxy and has additional container info + sb.append(mContainer); + sb.append(" "); + } + sb.append("m="); sb.append(modeToString(mMode)); sb.append(" f="); sb.append(flagsToString(mFlags)); if (mParent != null) { sb.append(" p="); sb.append(mParent); diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 3fe63ab17248..a88a17283482 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -1120,8 +1120,8 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public String toString() { return "WindowContainerTransaction {" - + " changes = " + mChanges - + " hops = " + mHierarchyOps + + " changes= " + mChanges + + " hops= " + mHierarchyOps + " errorCallbackToken=" + mErrorCallbackToken + " taskFragmentOrganizer=" + mTaskFragmentOrganizer + " }"; diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 4f924a82c9cc..ff69610dbf0e 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -418,6 +418,17 @@ flag { } flag { + name: "record_task_snapshots_before_shutdown" + namespace: "windowing_frontend" + description: "Record task snapshots before shutdown" + bug: "376821232" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "predictive_back_three_button_nav" namespace: "systemui" description: "Enable Predictive Back Animation for 3-button-nav" diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java index 3303d875c427..8df3f2abcafd 100644 --- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java +++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java @@ -88,15 +88,19 @@ public final class RavenwoodEnvironment { /** @hide */ public static class CompatIdsForTest { // Enabled by default + /** Used for testing */ @ChangeId public static final long TEST_COMPAT_ID_1 = 368131859L; + /** Used for testing */ @Disabled @ChangeId public static final long TEST_COMPAT_ID_2 = 368131701L; + /** Used for testing */ @EnabledAfter(targetSdkVersion = S) @ChangeId public static final long TEST_COMPAT_ID_3 = 368131659L; + /** Used for testing */ @EnabledAfter(targetSdkVersion = UPSIDE_DOWN_CAKE) @ChangeId public static final long TEST_COMPAT_ID_4 = 368132057L; } diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp index bb4084e8f39e..f64dec8eb215 100644 --- a/core/jni/android_hardware_OverlayProperties.cpp +++ b/core/jni/android_hardware_OverlayProperties.cpp @@ -106,7 +106,7 @@ static jobjectArray android_hardware_OverlayProperties_getLutProperties(JNIEnv* jlong nativeObject) { gui::OverlayProperties* overlayProperties = reinterpret_cast<gui::OverlayProperties*>(nativeObject); - if (overlayProperties->lutProperties.has_value()) { + if (!overlayProperties || !overlayProperties->lutProperties) { return NULL; } auto& lutProperties = overlayProperties->lutProperties.value(); diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index 49191ee02ad6..7ef7829c6ba5 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -33,6 +33,7 @@ #include <algorithm> #include <array> +#include <cctype> #include <cstring> #include <limits> #include <memory> @@ -1008,6 +1009,8 @@ jboolean android_os_Process_parseProcLineArray(JNIEnv* env, jobject clazz, } } if ((mode&PROC_OUT_STRING) != 0 && di < NS) { + std::replace_if(buffer+start, buffer+end, + [](unsigned char c){ return !std::isprint(c); }, '?'); jstring str = env->NewStringUTF(buffer+start); env->SetObjectArrayElement(outStrings, di, str); } diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 82b463ec9091..1925b3a41b5f 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -758,54 +758,64 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl*>(nativeObject); - ScopedIntArrayRW joffsets(env, joffsetArray); - if (joffsets.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from joffsetArray"); - return; - } - ScopedIntArrayRW jdimensions(env, jdimensionArray); - if (jdimensions.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jdimensionArray"); - return; - } - ScopedIntArrayRW jsizes(env, jsizeArray); - if (jsizes.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsizeArray"); - return; - } - ScopedIntArrayRW jsamplingKeys(env, jsamplingKeyArray); - if (jsamplingKeys.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsamplingKeyArray"); - return; - } + std::vector<int32_t> offsets; + std::vector<int32_t> dimensions; + std::vector<int32_t> sizes; + std::vector<int32_t> samplingKeys; + int32_t fd = -1; + + if (jdimensionArray) { + jsize numLuts = env->GetArrayLength(jdimensionArray); + ScopedIntArrayRW joffsets(env, joffsetArray); + if (joffsets.get() == nullptr) { + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from joffsetArray"); + return; + } + ScopedIntArrayRW jdimensions(env, jdimensionArray); + if (jdimensions.get() == nullptr) { + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jdimensionArray"); + return; + } + ScopedIntArrayRW jsizes(env, jsizeArray); + if (jsizes.get() == nullptr) { + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsizeArray"); + return; + } + ScopedIntArrayRW jsamplingKeys(env, jsamplingKeyArray); + if (jsamplingKeys.get() == nullptr) { + jniThrowRuntimeException(env, "Failed to get ScopedIntArrayRW from jsamplingKeyArray"); + return; + } - jsize numLuts = env->GetArrayLength(jdimensionArray); - std::vector<int32_t> offsets(joffsets.get(), joffsets.get() + numLuts); - std::vector<int32_t> dimensions(jdimensions.get(), jdimensions.get() + numLuts); - std::vector<int32_t> sizes(jsizes.get(), jsizes.get() + numLuts); - std::vector<int32_t> samplingKeys(jsamplingKeys.get(), jsamplingKeys.get() + numLuts); + if (numLuts > 0) { + offsets = std::vector<int32_t>(joffsets.get(), joffsets.get() + numLuts); + dimensions = std::vector<int32_t>(jdimensions.get(), jdimensions.get() + numLuts); + sizes = std::vector<int32_t>(jsizes.get(), jsizes.get() + numLuts); + samplingKeys = std::vector<int32_t>(jsamplingKeys.get(), jsamplingKeys.get() + numLuts); - ScopedFloatArrayRW jbuffers(env, jbufferArray); - if (jbuffers.get() == nullptr) { - jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRW from jbufferArray"); - return; - } + ScopedFloatArrayRW jbuffers(env, jbufferArray); + if (jbuffers.get() == nullptr) { + jniThrowRuntimeException(env, "Failed to get ScopedFloatArrayRW from jbufferArray"); + return; + } - // create the shared memory and copy jbuffers - size_t bufferSize = jbuffers.size() * sizeof(float); - int32_t fd = ashmem_create_region("lut_shread_mem", bufferSize); - if (fd < 0) { - jniThrowRuntimeException(env, "ashmem_create_region() failed"); - return; - } - void* ptr = mmap(nullptr, bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (ptr == MAP_FAILED) { - jniThrowRuntimeException(env, "Failed to map the shared memory"); - return; + // create the shared memory and copy jbuffers + size_t bufferSize = jbuffers.size() * sizeof(float); + fd = ashmem_create_region("lut_shared_mem", bufferSize); + if (fd < 0) { + jniThrowRuntimeException(env, "ashmem_create_region() failed"); + return; + } + void* ptr = mmap(nullptr, bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (ptr == MAP_FAILED) { + jniThrowRuntimeException(env, "Failed to map the shared memory"); + return; + } + memcpy(ptr, jbuffers.get(), bufferSize); + // unmap + munmap(ptr, bufferSize); + } } - memcpy(ptr, jbuffers.get(), bufferSize); - // unmap - munmap(ptr, bufferSize); transaction->setLuts(ctrl, base::unique_fd(fd), offsets, dimensions, sizes, samplingKeys); } @@ -1332,8 +1342,9 @@ static void nativeSetDisplaySize(JNIEnv* env, jclass clazz, } } -static jobject convertDeviceProductInfoToJavaObject( - JNIEnv* env, const std::optional<DeviceProductInfo>& info) { +static jobject convertDeviceProductInfoToJavaObject(JNIEnv* env, + const std::optional<DeviceProductInfo>& info, + bool isInternal) { using ModelYear = android::DeviceProductInfo::ModelYear; using ManufactureYear = android::DeviceProductInfo::ManufactureYear; using ManufactureWeekAndYear = android::DeviceProductInfo::ManufactureWeekAndYear; @@ -1368,7 +1379,8 @@ static jobject convertDeviceProductInfoToJavaObject( // Section 8.7 - Physical Address of HDMI Specification Version 1.3a using android::hardware::display::IDeviceProductInfoConstants; if (info->relativeAddress.size() != 4) { - connectionToSinkType = IDeviceProductInfoConstants::CONNECTION_TO_SINK_UNKNOWN; + connectionToSinkType = isInternal ? IDeviceProductInfoConstants::CONNECTION_TO_SINK_BUILT_IN + : IDeviceProductInfoConstants::CONNECTION_TO_SINK_UNKNOWN; } else if (info->relativeAddress[0] == 0) { connectionToSinkType = IDeviceProductInfoConstants::CONNECTION_TO_SINK_BUILT_IN; } else if (info->relativeAddress[1] == 0) { @@ -1390,12 +1402,14 @@ static jobject nativeGetStaticDisplayInfo(JNIEnv* env, jclass clazz, jlong id) { jobject object = env->NewObject(gStaticDisplayInfoClassInfo.clazz, gStaticDisplayInfoClassInfo.ctor); - env->SetBooleanField(object, gStaticDisplayInfoClassInfo.isInternal, - info.connectionType == ui::DisplayConnectionType::Internal); + + const bool isInternal = info.connectionType == ui::DisplayConnectionType::Internal; + env->SetBooleanField(object, gStaticDisplayInfoClassInfo.isInternal, isInternal); env->SetFloatField(object, gStaticDisplayInfoClassInfo.density, info.density); env->SetBooleanField(object, gStaticDisplayInfoClassInfo.secure, info.secure); env->SetObjectField(object, gStaticDisplayInfoClassInfo.deviceProductInfo, - convertDeviceProductInfoToJavaObject(env, info.deviceProductInfo)); + convertDeviceProductInfoToJavaObject(env, info.deviceProductInfo, + isInternal)); env->SetIntField(object, gStaticDisplayInfoClassInfo.installOrientation, static_cast<uint32_t>(info.installOrientation)); return object; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 3e0c1200749e..9514aaf4c3b5 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4144,6 +4144,37 @@ <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" android:featureFlag="android.security.aapm_api"/> + <!-- Allows an application to read the state of the ForensicService + @FlaggedApi(android.security.Flags.FLAG_AFL_API) + @SystemApi + @hide --> + <permission android:name="android.permission.READ_FORENSIC_STATE" + android:featureFlag="android.security.afl_api" + android:protectionLevel="signature|privileged" /> + <uses-permission android:name="android.permission.READ_FORENSIC_STATE" + android:featureFlag="android.security.afl_api"/> + + <!-- Allows an application to change the state of the ForensicService + @FlaggedApi(android.security.Flags.FLAG_AFL_API) + @SystemApi + @hide --> + <permission android:name="android.permission.MANAGE_FORENSIC_STATE" + android:featureFlag="android.security.afl_api" + android:protectionLevel="signature|privileged" /> + <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE" + android:featureFlag="android.security.afl_api"/> + + <!-- Must be required by any ForensicEventTransportService to ensure that + only the system can bind to it. + @FlaggedApi(android.security.Flags.FLAG_AFL_API) + @SystemApi + @hide --> + <permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE" + android:featureFlag="android.security.afl_api" + android:protectionLevel="signature" /> + <uses-permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE" + android:featureFlag="android.security.afl_api"/> + <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.--> <permission android:name="android.permission.PROVISION_DEMO_DEVICE" android:protectionLevel="signature|setup|knownSigner" @@ -4991,16 +5022,16 @@ android:protectionLevel="signature|privileged|role" android:featureFlag="com.android.settingslib.flags.settings_catalyst" /> - <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST) + <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_WRITE_SYSTEM_PREFERENCE_PERMISSION_ENABLED) Allows an application to access the Settings Preference services to write settings values exposed by the system Settings app and system apps that contribute settings surfaced in the Settings app. <p>This allows the calling application to write settings values through the host application, agnostic of underlying storage. - <p>Protection Level: signature|privileged|appop - appop to be added in followup --> + <p>Protection Level: signature|privileged|appop --> <permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" - android:protectionLevel="signature|privileged" - android:featureFlag="com.android.settingslib.flags.settings_catalyst" /> + android:protectionLevel="signature|privileged|appop" + android:featureFlag="com.android.settingslib.flags.write_system_preference_permission_enabled" /> <!-- ========================================= --> <!-- Permissions for special development tools --> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 41dec3776b5c..7ef539492aa4 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1855,13 +1855,23 @@ {@link android.R.styleable#AndroidManifestProcess process} tag, or to an {@link android.R.styleable#AndroidManifestApplication application} tag (to supply a default setting for all application components). --> - <attr name="memtagMode"> + <attr name="memtagMode"> <enum name="default" value="-1" /> <enum name="off" value="0" /> <enum name="async" value="1" /> <enum name="sync" value="2" /> </attr> + <!-- This attribute will be used to override app compatibility mode on 16 KB devices. + If set to enabled, Natives lib will be extracted from APK if they are not page aligned on + 16 KB device. 4 KB natives libs will be loaded app-compat mode if they are eligible. + @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> + <attr name="pageSizeCompat"> + <enum name="enabled" value="5" /> + <enum name="disabled" value="6" /> + </attr> + + <!-- Attribution tag to be used for permission sub-attribution if a permission is checked in {@link android.content.Context#sendBroadcast(Intent, String)}. Multiple tags can be specified separated by '|'. @@ -2212,6 +2222,9 @@ <attr name="memtagMode" /> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> + <attr name="pageSizeCompat" /> + <!-- If {@code true} enables automatic zero initialization of all native heap allocations. --> <attr name="nativeHeapZeroInitialized" format="boolean" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 969ee2e16deb..7799ff951997 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6567,7 +6567,7 @@ </string-array> <!-- the number of the max cached processes in the system. --> - <integer name="config_customizedMaxCachedProcesses">32</integer> + <integer name="config_customizedMaxCachedProcesses">1024</integer> <!-- Whether this device should support taking app snapshots on closure --> <bool name="config_disableTaskSnapshots">false</bool> @@ -7212,8 +7212,8 @@ <!-- Package for opening identity check settings page [CHAR LIMIT=NONE] [DO NOT TRANSLATE] --> <string name="identity_check_settings_package_name">com\u002eandroid\u002esettings</string> - <!-- The name of the service for forensic backup transport. --> - <string name="config_forensicBackupTransport" translatable="false"></string> + <!-- The name of the service for forensic event transport. --> + <string name="config_forensicEventTransport" translatable="false"></string> <!-- Whether to enable fp unlock when screen turns off on udfps devices --> <bool name="config_screen_off_udfps_enabled">false</bool> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index 31e9913dd988..4ec27a31df8c 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -318,6 +318,12 @@ <bool name="config_oem_enabled_satellite_access_allow">true</bool> <java-symbol type="bool" name="config_oem_enabled_satellite_access_allow" /> + <!-- Whether the satellite modem support concurrent TN scanning while device is in + NTN mode. + --> + <bool name="config_satellite_modem_support_concurrent_tn_scanning">true</bool> + <java-symbol type="bool" name="config_satellite_modem_support_concurrent_tn_scanning" /> + <!-- The time duration in seconds which is used to decide whether the Location returned from LocationManager#getLastKnownLocation is fresh. diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index b6436d0b30a5..a0bf89d66923 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -131,6 +131,8 @@ <public name="alternateLauncherIcons"/> <!-- @FlaggedApi(android.content.pm.Flags.FLAG_CHANGE_LAUNCHER_BADGING) --> <public name="alternateLauncherLabels"/> + <!-- @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> + <public name="pageSizeCompat" /> </staging-public-group> <staging-public-group type="id" first-id="0x01b60000"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 9dd302784c2c..7fe09128de05 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5679,8 +5679,8 @@ <java-symbol type="string" name="identity_check_settings_action" /> <java-symbol type="string" name="identity_check_settings_package_name" /> - <!-- Forensic backup transport --> - <java-symbol type="string" name="config_forensicBackupTransport" /> + <!-- Forensic event transport --> + <java-symbol type="string" name="config_forensicEventTransport" /> <!-- Fingerprint screen off unlock config --> <java-symbol type="bool" name="config_screen_off_udfps_enabled" /> diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java index 9dd196daf412..f62d420510c3 100644 --- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java @@ -507,7 +507,8 @@ public class EditorInfoTest { + "prefix: supportedHandwritingGestureTypes=(none)\n" + "prefix: supportedHandwritingGesturePreviewTypes=(none)\n" + "prefix: isStylusHandwritingEnabled=false\n" - + "prefix: contentMimeTypes=null\n"); + + "prefix: contentMimeTypes=null\n" + + "prefix: writingToolsEnabled=true\n"); } @Test @@ -539,6 +540,7 @@ public class EditorInfoTest { info.hintLocales = LocaleList.forLanguageTags("en,es,zh"); info.contentMimeTypes = new String[] {"image/png"}; info.targetInputMethodUser = UserHandle.of(10); + info.setWritingToolsEnabled(false); final StringBuilder sb = new StringBuilder(); info.dump(new StringBuilderPrinter(sb), "prefix2: "); assertThat(sb.toString()).isEqualTo( @@ -555,7 +557,8 @@ public class EditorInfoTest { + "prefix2: supportedHandwritingGesturePreviewTypes=SELECT\n" + "prefix2: isStylusHandwritingEnabled=" + isStylusHandwritingEnabled + "\n" + "prefix2: contentMimeTypes=[image/png]\n" - + "prefix2: targetInputMethodUserId=10\n"); + + "prefix2: targetInputMethodUserId=10\n" + + "prefix2: writingToolsEnabled=false\n"); } @Test @@ -576,7 +579,8 @@ public class EditorInfoTest { + "prefix: supportedHandwritingGestureTypes=(none)\n" + "prefix: supportedHandwritingGesturePreviewTypes=(none)\n" + "prefix: isStylusHandwritingEnabled=false\n" - + "prefix: contentMimeTypes=null\n"); + + "prefix: contentMimeTypes=null\n" + + "prefix: writingToolsEnabled=true\n"); } @Test @@ -621,4 +625,9 @@ public class EditorInfoTest { infoCopy.extras.putString("testKey2", "testValue"); assertFalse(TEST_EDITOR_INFO.kindofEquals(infoCopy)); } + + @Test + public void testWritingToolsEnabledbyDefault() { + assertTrue(TEST_EDITOR_INFO.isWritingToolsEnabled()); + } } diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 9552c887443b..6a5224d4524b 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -16,6 +16,11 @@ package android.hardware.display; +import static android.hardware.display.DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED; +import static android.hardware.display.DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; +import static android.hardware.display.DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_STATE; + +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -28,13 +33,19 @@ import android.content.Context; import android.os.Handler; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.DisplayInfo; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.server.display.feature.flags.Flags; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -55,6 +66,10 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class DisplayManagerGlobalTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final long ALL_DISPLAY_EVENTS = DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED @@ -117,6 +132,33 @@ public class DisplayManagerGlobalTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) + public void testDisplayListenerIsCalled_WhenDisplayPropertyChangeEventOccurs() + throws RemoteException { + mDisplayManagerGlobal.registerDisplayListener(mListener, mHandler, + INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE + | INTERNAL_EVENT_FLAG_DISPLAY_STATE, + null); + Mockito.verify(mDisplayManager) + .registerCallbackWithEventMask(mCallbackCaptor.capture(), anyLong()); + IDisplayManagerCallback callback = mCallbackCaptor.getValue(); + + int displayId = 1; + + Mockito.reset(mListener); + callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_REFRESH_RATE_CHANGED); + waitForHandler(); + Mockito.verify(mListener).onDisplayChanged(eq(displayId)); + Mockito.verifyNoMoreInteractions(mListener); + + Mockito.reset(mListener); + callback.onDisplayEvent(displayId, EVENT_DISPLAY_STATE_CHANGED); + waitForHandler(); + Mockito.verify(mListener).onDisplayChanged(eq(displayId)); + Mockito.verifyNoMoreInteractions(mListener); + } + + @Test public void testDisplayListenerIsNotCalled_WhenClientIsNotSubscribed() throws RemoteException { // First we subscribe to all events in order to test that the subsequent calls to // registerDisplayListener will update the event mask. @@ -231,6 +273,53 @@ public class DisplayManagerGlobalTest { verify(mListener2, never()).onDisplayChanged(anyInt()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS) + public void testMapFlagsToInternalEventFlag() { + // Test public flags mapping + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag(DisplayManager.EVENT_FLAG_DISPLAY_ADDED, 0)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED, 0)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag(DisplayManager.EVENT_FLAG_DISPLAY_REMOVED, 0)); + assertEquals(INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag( + DisplayManager.EVENT_FLAG_DISPLAY_REFRESH_RATE, + 0)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_STATE, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag( + DisplayManager.EVENT_FLAG_DISPLAY_STATE, + 0)); + + // test private flags mapping + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_FLAG_HDR_SDR_RATIO_CHANGED)); + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag(0, + DisplayManager.PRIVATE_EVENT_FLAG_DISPLAY_BRIGHTNESS)); + + // Test both public and private flags mapping + assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, + mDisplayManagerGlobal + .mapFlagsToInternalEventFlag( + DisplayManager.EVENT_FLAG_DISPLAY_REFRESH_RATE, + DisplayManager.PRIVATE_EVENT_FLAG_DISPLAY_BRIGHTNESS)); + } + private void waitForHandler() { mHandler.runWithScissors(() -> { }, 0); diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS index 6149382d0800..4620cb8d8148 100644 --- a/core/tests/coretests/src/android/os/OWNERS +++ b/core/tests/coretests/src/android/os/OWNERS @@ -12,3 +12,6 @@ per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS # Caching per-file IpcDataCache* = file:/PERFORMANCE_OWNERS + +# RemoteCallbackList +per-file RemoteCallbackListTest.java = shayba@google.com diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 541ca602a386..74235672ad3f 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -597,6 +597,9 @@ applications that come with the platform <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest --> <permission name="android.permission.READ_SYSTEM_PREFERENCES" /> <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" /> + <!-- Permission required for CTS test - ForensicManagerTest --> + <permission name="android.permission.READ_FORENSIC_STATE" /> + <permission name="android.permission.MANAGE_FORENSIC_STATE" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/graphics/java/android/graphics/BlendMode.java b/graphics/java/android/graphics/BlendMode.java index 5c294aa72ce8..c6ae680d01bf 100644 --- a/graphics/java/android/graphics/BlendMode.java +++ b/graphics/java/android/graphics/BlendMode.java @@ -571,10 +571,10 @@ public enum BlendMode { } @NonNull - private final Xfermode mXfermode; + private final PorterDuffXfermode mXfermode; BlendMode(int mode) { - mXfermode = new Xfermode(); + mXfermode = new PorterDuffXfermode(); mXfermode.porterDuffMode = mode; } @@ -582,7 +582,7 @@ public enum BlendMode { * @hide */ @NonNull - public Xfermode getXfermode() { + public PorterDuffXfermode getXfermode() { return mXfermode; } } diff --git a/graphics/java/android/graphics/ComposeShader.java b/graphics/java/android/graphics/ComposeShader.java index 977aeaa2f4d9..e7145686247e 100644 --- a/graphics/java/android/graphics/ComposeShader.java +++ b/graphics/java/android/graphics/ComposeShader.java @@ -40,9 +40,12 @@ public class ComposeShader extends Shader { * @param mode The mode that combines the colors from the two shaders. If mode * is null, then SRC_OVER is assumed. */ + //TODO(358126864): allow a ComposeShader to accept a RuntimeXfermode @Deprecated public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode) { - this(shaderA, shaderB, mode.porterDuffMode); + this(shaderA, shaderB, + mode instanceof PorterDuffXfermode ? ((PorterDuffXfermode) mode).porterDuffMode + : BlendMode.SRC_OVER.getXfermode().porterDuffMode); } /** diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 56bb0f0d12d5..2c166c32ba50 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -71,6 +71,7 @@ public class Paint { private long mNativePaint; private long mNativeShader; private long mNativeColorFilter; + private long mNativeXfermode; // Use a Holder to allow static initialization of Paint in the boot image. private static class NoImagePreloadHolder { @@ -735,6 +736,7 @@ public class Paint { mPathEffect = null; mShader = null; mNativeShader = 0; + mNativeXfermode = 0; mTypeface = null; mXfermode = null; @@ -780,6 +782,7 @@ public class Paint { mNativeShader = paint.mNativeShader; mTypeface = paint.mTypeface; mXfermode = paint.mXfermode; + mNativeXfermode = paint.mNativeXfermode; mHasCompatScaling = paint.mHasCompatScaling; mCompatScaling = paint.mCompatScaling; @@ -815,7 +818,7 @@ public class Paint { * * Note: Although this method is |synchronized|, this is simply so it * is not thread-hostile to multiple threads calling this method. It - * is still unsafe to attempt to change the Shader/ColorFilter while + * is still unsafe to attempt to change the Shader/ColorFilter/Xfermode while * another thread attempts to access the native object. * * @hide @@ -833,6 +836,13 @@ public class Paint { mNativeColorFilter = newNativeColorFilter; nSetColorFilter(mNativePaint, mNativeColorFilter); } + if (mXfermode instanceof RuntimeXfermode) { + long newNativeXfermode = ((RuntimeXfermode) mXfermode).createNativeInstance(); + if (newNativeXfermode != mNativeXfermode) { + mNativeXfermode = newNativeXfermode; + nSetXfermode(mNativePaint, mNativeXfermode); + } + } return mNativePaint; } @@ -1427,16 +1437,17 @@ public class Paint { } /** - * Get the paint's blend mode object. + * Get the paint's blend mode object. Will return null if there is a Xfermode applied that + * cannot be represented by a blend mode (i.e. a custom {@code RuntimeXfermode} * * @return the paint's blend mode (or null) */ @Nullable public BlendMode getBlendMode() { - if (mXfermode == null) { + if (mXfermode == null || !(mXfermode instanceof PorterDuffXfermode)) { return null; } else { - return BlendMode.fromValue(mXfermode.porterDuffMode); + return BlendMode.fromValue(((PorterDuffXfermode) mXfermode).porterDuffMode); } } @@ -1459,8 +1470,15 @@ public class Paint { @Nullable private Xfermode installXfermode(Xfermode xfermode) { - int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT; - int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT; + if (xfermode instanceof RuntimeXfermode) { + mXfermode = xfermode; + nSetXfermode(mNativePaint, ((RuntimeXfermode) xfermode).createNativeInstance()); + return xfermode; + } + int newMode = (xfermode instanceof PorterDuffXfermode) + ? ((PorterDuffXfermode) xfermode).porterDuffMode : PorterDuffXfermode.DEFAULT; + int curMode = (mXfermode instanceof PorterDuffXfermode) + ? ((PorterDuffXfermode) mXfermode).porterDuffMode : PorterDuffXfermode.DEFAULT; if (newMode != curMode) { nSetXfermode(mNativePaint, newMode); } @@ -3823,6 +3841,8 @@ public class Paint { @CriticalNative private static native void nSetXfermode(long paintPtr, int xfermode); @CriticalNative + private static native void nSetXfermode(long paintPtr, long xfermodePtr); + @CriticalNative private static native long nSetPathEffect(long paintPtr, long effect); @CriticalNative private static native long nSetMaskFilter(long paintPtr, long maskfilter); diff --git a/graphics/java/android/graphics/PorterDuffXfermode.java b/graphics/java/android/graphics/PorterDuffXfermode.java index ff9ff8b0069d..83d0507a5074 100644 --- a/graphics/java/android/graphics/PorterDuffXfermode.java +++ b/graphics/java/android/graphics/PorterDuffXfermode.java @@ -29,6 +29,9 @@ public class PorterDuffXfermode extends Xfermode { * * @param mode The porter-duff mode that is applied */ + static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt; + int porterDuffMode = DEFAULT; + PorterDuffXfermode() {} public PorterDuffXfermode(PorterDuff.Mode mode) { porterDuffMode = mode.nativeInt; } diff --git a/graphics/java/android/graphics/RuntimeXfermode.java b/graphics/java/android/graphics/RuntimeXfermode.java new file mode 100644 index 000000000000..f5a656862bf9 --- /dev/null +++ b/graphics/java/android/graphics/RuntimeXfermode.java @@ -0,0 +1,312 @@ +/* + * 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 android.graphics; + +import android.annotation.ColorInt; +import android.annotation.ColorLong; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; + +import com.android.graphics.hwui.flags.Flags; + +import libcore.util.NativeAllocationRegistry; + + +/** + * <p>A {@link RuntimeXfermode} calculates a per-pixel color based on the output of a user + * * defined Android Graphics Shading Language (AGSL) function.</p> + * + * <p>This AGSL function takes in two input colors to be operated on. These colors are in sRGB + * * and the output is also interpreted as sRGB. The AGSL function signature expects a single input + * * of color (packed as a half4 or float4 or vec4).</p> + * + * <pre class="prettyprint"> + * vec4 main(half4 src, half4 dst); + * </pre> + */ +@FlaggedApi(Flags.FLAG_RUNTIME_COLOR_FILTERS_BLENDERS) +public class RuntimeXfermode extends Xfermode { + + private static class NoImagePreloadHolder { + public static final NativeAllocationRegistry sRegistry = + NativeAllocationRegistry.createMalloced( + RuntimeXfermode.class.getClassLoader(), nativeGetFinalizer()); + } + + private long mBuilderNativeInstance; + + /** + * Creates a new RuntimeBlender. + * + * @param agsl The text of AGSL color filter program to run. + */ + public RuntimeXfermode(@NonNull String agsl) { + if (agsl == null) { + throw new NullPointerException("RuntimeShader requires a non-null AGSL string"); + } + mBuilderNativeInstance = nativeCreateBlenderBuilder(agsl); + RuntimeXfermode.NoImagePreloadHolder.sRegistry.registerNativeAllocation( + this, mBuilderNativeInstance); + } + /** + * Sets the uniform color value corresponding to this color filter. If the effect does not have + * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4 + * and corresponding layout(color) annotation then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the color uniform declared in the AGSL program + * @param color the provided sRGB color + */ + public void setColorUniform(@NonNull String uniformName, @ColorInt int color) { + setUniform(uniformName, Color.valueOf(color).getComponents(), true); + } + + /** + * Sets the uniform color value corresponding to this color filter. If the effect does not have + * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4 + * and corresponding layout(color) annotation then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the color uniform declared in the AGSL program + * @param color the provided sRGB color + */ + public void setColorUniform(@NonNull String uniformName, @ColorLong long color) { + Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); + setUniform(uniformName, exSRGB.getComponents(), true); + } + + /** + * Sets the uniform color value corresponding to this color filter. If the effect does not have + * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4 + * and corresponding layout(color) annotation then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the color uniform declared in the AGSL program + * @param color the provided sRGB color + */ + public void setColorUniform(@NonNull String uniformName, @NonNull Color color) { + if (color == null) { + throw new NullPointerException("The color parameter must not be null"); + } + Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); + setUniform(uniformName, exSRGB.getComponents(), true); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a float or + * float[1] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value) { + setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a vec2 or + * float[2] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2) { + setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a vec3 or + * float[3] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2, + float value3) { + setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3); + + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a vec4 or + * float[4] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2, + float value3, float value4) { + setFloatUniform(uniformName, value1, value2, value3, value4, 4); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a float + * (for N=1), vecN, or float[N] where N is the length of the values param then an + * IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) { + setUniform(uniformName, values, false); + } + + private void setFloatUniform(@NonNull String uniformName, float value1, float value2, + float value3, float value4, int count) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4, + count); + } + + private void setUniform(@NonNull String uniformName, @NonNull float[] values, boolean isColor) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + if (values == null) { + throw new NullPointerException("The uniform values parameter must not be null"); + } + nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values, isColor); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an int or int[1] + * then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value) { + setIntUniform(uniformName, value, 0, 0, 0, 1); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an ivec2 or + * int[2] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2) { + setIntUniform(uniformName, value1, value2, 0, 0, 2); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an ivec3 or + * int[3] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) { + setIntUniform(uniformName, value1, value2, value3, 0, 3); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an ivec4 or + * int[4] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2, + int value3, int value4) { + setIntUniform(uniformName, value1, value2, value3, value4, 4); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an int (for N=1), + * ivecN, or int[N] where N is the length of the values param then an IllegalArgumentException + * is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + if (values == null) { + throw new NullPointerException("The uniform values parameter must not be null"); + } + nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values); + } + + private void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3, + int value4, int count) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4, + count); + } + + /** + * Assigns the uniform shader to the provided shader parameter. If the shader program does not + * have a uniform shader with that name then an IllegalArgumentException is thrown. + * + * @param shaderName name matching the uniform declared in the AGSL program + * @param shader shader passed into the AGSL program for sampling + */ + public void setInputShader(@NonNull String shaderName, @NonNull Shader shader) { + if (shaderName == null) { + throw new NullPointerException("The shaderName parameter must not be null"); + } + if (shader == null) { + throw new NullPointerException("The shader parameter must not be null"); + } + nativeUpdateChild(mBuilderNativeInstance, shaderName, shader.getNativeInstance()); + } + + /** + * Assigns the uniform color filter to the provided color filter parameter. If the shader + * program does not have a uniform color filter with that name then an IllegalArgumentException + * is thrown. + * + * @param filterName name matching the uniform declared in the AGSL program + * @param colorFilter filter passed into the AGSL program for sampling + */ + public void setInputColorFilter(@NonNull String filterName, @NonNull ColorFilter colorFilter) { + if (filterName == null) { + throw new NullPointerException("The filterName parameter must not be null"); + } + if (colorFilter == null) { + throw new NullPointerException("The colorFilter parameter must not be null"); + } + nativeUpdateChild(mBuilderNativeInstance, filterName, colorFilter.getNativeInstance()); + } + + /** @hide */ + public long createNativeInstance() { + return nativeCreateNativeInstance(mBuilderNativeInstance); + } + + /** @hide */ + private static native long nativeGetFinalizer(); + private static native long nativeCreateBlenderBuilder(String agsl); + private static native long nativeCreateNativeInstance(long builder); + private static native void nativeUpdateUniforms( + long builder, String uniformName, float[] uniforms, boolean isColor); + private static native void nativeUpdateUniforms( + long builder, String uniformName, float value1, float value2, float value3, + float value4, int count); + private static native void nativeUpdateUniforms( + long builder, String uniformName, int[] uniforms); + private static native void nativeUpdateUniforms( + long builder, String uniformName, int value1, int value2, int value3, + int value4, int count); + private static native void nativeUpdateChild(long builder, String childName, long child); + +} diff --git a/graphics/java/android/graphics/Xfermode.java b/graphics/java/android/graphics/Xfermode.java index 6bb22a12280e..fb689e4cb9c2 100644 --- a/graphics/java/android/graphics/Xfermode.java +++ b/graphics/java/android/graphics/Xfermode.java @@ -21,9 +21,6 @@ package android.graphics; -import android.compat.annotation.UnsupportedAppUsage; -import android.os.Build; - /** * Xfermode is the base class for objects that are called to implement custom * "transfer-modes" in the drawing pipeline. The static function Create(Modes) @@ -31,8 +28,4 @@ import android.os.Build; * specified in the Modes enum. When an Xfermode is assigned to a Paint, then * objects drawn with that paint have the xfermode applied. */ -public class Xfermode { - static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt; - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - int porterDuffMode = DEFAULT; -} +public class Xfermode {} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java index 03e0ab0591a1..4300e84e8044 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java @@ -168,6 +168,16 @@ public class GroupedTaskInfo implements Parcelable { } /** + * @return The task info for the task in this group with the given {@code taskId}. + */ + @Nullable + public TaskInfo getTaskById(int taskId) { + return mTasks.stream() + .filter(task -> task.taskId == taskId) + .findFirst().orElse(null); + } + + /** * Get all {@link RecentTaskInfo}s grouped together. */ @NonNull @@ -176,6 +186,14 @@ public class GroupedTaskInfo implements Parcelable { } /** + * @return Whether this grouped task contains a task with the given {@code taskId}. + */ + public boolean containsTask(int taskId) { + return mTasks.stream() + .anyMatch((task -> task.taskId == taskId)); + } + + /** * Return {@link SplitBounds} if this is a split screen entry or {@code null} */ @Nullable @@ -249,9 +267,10 @@ public class GroupedTaskInfo implements Parcelable { return null; } return "id=" + taskInfo.taskId - + " baseIntent=" + (taskInfo.baseIntent != null - ? taskInfo.baseIntent.getComponent() - : "null") + + " baseIntent=" + + (taskInfo.baseIntent != null && taskInfo.baseIntent.getComponent() != null + ? taskInfo.baseIntent.getComponent().flattenToString() + : "null") + " winMode=" + WindowConfiguration.windowingModeToString( taskInfo.getWindowingMode()); } 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 77e041ee7cdb..e455985c87c3 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 @@ -1024,10 +1024,13 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static TaskStackTransitionObserver provideTaskStackTransitionObserver( - Lazy<Transitions> transitions, - ShellInit shellInit + ShellInit shellInit, + Lazy<ShellTaskOrganizer> shellTaskOrganizer, + ShellCommandHandler shellCommandHandler, + Lazy<Transitions> transitions ) { - return new TaskStackTransitionObserver(transitions, shellInit); + return new TaskStackTransitionObserver(shellInit, shellTaskOrganizer, shellCommandHandler, + transitions); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 2001f9743094..82c2ebc7ec77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -23,6 +23,7 @@ import android.os.Handler import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_OPEN import android.window.DesktopModeFlags import android.window.TransitionInfo import android.window.TransitionInfo.Change @@ -95,7 +96,7 @@ class DesktopMixedTransitionHandler( fun startLaunchTransition( @WindowManager.TransitionType transitionType: Int, wct: WindowContainerTransaction, - taskId: Int, + taskId: Int?, minimizingTaskId: Int? = null, exitingImmersiveTask: Int? = null, ): IBinder { @@ -216,12 +217,12 @@ class DesktopMixedTransitionHandler( ): Boolean { // Check if there's also an immersive change during this launch. val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask -> - findDesktopTaskChange(info, exitingTask) + findTaskChange(info, exitingTask) } val minimizeChange = pending.minimizingTask?.let { minimizingTask -> - findDesktopTaskChange(info, minimizingTask) + findTaskChange(info, minimizingTask) } - val launchChange = findDesktopTaskChange(info, pending.launchingTask) + val launchChange = findDesktopTaskLaunchChange(info, pending.launchingTask) if (launchChange == null) { check(minimizeChange == null) check(immersiveExitChange == null) @@ -291,7 +292,7 @@ class DesktopMixedTransitionHandler( ): Boolean { if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false - val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask) + val minimizeChange = findTaskChange(info, pending.minimizingTask) if (minimizeChange == null) { logW("Should have minimizing desktop task") return false @@ -417,8 +418,24 @@ class DesktopMixedTransitionHandler( } } - private fun findDesktopTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? { - return info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId } + private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? = + info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId } + + private fun findDesktopTaskLaunchChange( + info: TransitionInfo, + launchTaskId: Int? + ): TransitionInfo.Change? { + return if (launchTaskId != null) { + // Launching a known task (probably from background or moving to front), so + // specifically look for it. + findTaskChange(info, launchTaskId) + } else { + // Launching a new task, so the first opening freeform task. + info.changes.firstOrNull { change -> + change.mode == TRANSIT_OPEN + && change.taskInfo != null && change.taskInfo!!.isFreeform + } + } } private fun WindowContainerTransaction?.merge( @@ -441,7 +458,7 @@ class DesktopMixedTransitionHandler( /** A task is opening or moving to front. */ data class Launch( override val transition: IBinder, - val launchingTask: Int, + val launchingTask: Int?, val minimizingTask: Int?, val exitingImmersiveTask: Int?, ) : PendingMixedTransition() 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 08ca55f93e3f..7fcb7678f6af 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 @@ -411,6 +411,12 @@ class DesktopRepository ( desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString()) // Remove task from unminimized task if it is minimized. unminimizeTask(displayId, taskId) + // Mark task as not in immersive if it was immersive. + setTaskInFullImmersiveState( + displayId = displayId, + taskId = taskId, + immersive = false + ) removeActiveTask(taskId) removeVisibleTask(taskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { @@ -487,6 +493,9 @@ class DesktopRepository ( mainCoroutineScope.launch { try { persistentRepository.addOrUpdateDesktop( + // Use display id as desktop id for now since only once desktop per display + // is supported. + desktopId = displayId, visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks, minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks, freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder 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 6928c255edde..4db0be5c9025 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 @@ -89,7 +89,6 @@ import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing -import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener @@ -618,25 +617,18 @@ class DesktopTasksController( private fun moveBackgroundTaskToFront(taskId: Int, remoteTransition: RemoteTransition?) { logV("moveBackgroundTaskToFront taskId=%s", taskId) val wct = WindowContainerTransaction() - // TODO: b/342378842 - Instead of using default display, support multiple displays - val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( - wct = wct, - displayId = DEFAULT_DISPLAY, - excludeTaskId = taskId, - ) wct.startTask( taskId, ActivityOptions.makeBasic().apply { launchWindowingMode = WINDOWING_MODE_FREEFORM }.toBundle(), ) - val transition = startLaunchTransition( + startLaunchTransition( TRANSIT_OPEN, wct, taskId, remoteTransition = remoteTransition ) - exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } /** @@ -655,47 +647,53 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */) - val result = desktopImmersiveController.exitImmersiveIfApplicable( - wct = wct, - displayId = taskInfo.displayId, - excludeTaskId = taskInfo.taskId, - ) - val exitResult = if (result is ExitResult.Exit) { result } else { null } - val transition = startLaunchTransition( + startLaunchTransition( transitionType = TRANSIT_TO_FRONT, wct = wct, - taskId = taskInfo.taskId, - exitingImmersiveTask = exitResult?.exitingTask, + launchingTaskId = taskInfo.taskId, remoteTransition = remoteTransition, displayId = taskInfo.displayId, ) - exitResult?.runOnTransitionStart?.invoke(transition) } private fun startLaunchTransition( transitionType: Int, wct: WindowContainerTransaction, - taskId: Int, - exitingImmersiveTask: Int? = null, + launchingTaskId: Int?, remoteTransition: RemoteTransition? = null, displayId: Int = DEFAULT_DISPLAY, ): IBinder { - val taskIdToMinimize = addAndGetMinimizeChanges(displayId, wct, taskId) + val taskIdToMinimize = if (launchingTaskId != null) { + addAndGetMinimizeChanges(displayId, wct, newTaskId = launchingTaskId) + } else { + logW("Starting desktop task launch without checking the task-limit") + // TODO(b/378920066): This currently does not respect the desktop window limit. + // It's possible that |launchingTaskId| is null when launching using an intent, and + // the task-limit should be respected then too. + null + } + val exitImmersiveResult = desktopImmersiveController.exitImmersiveIfApplicable( + wct = wct, + displayId = displayId, + excludeTaskId = launchingTaskId, + ) if (remoteTransition == null) { val t = desktopMixedTransitionHandler.startLaunchTransition( transitionType = transitionType, wct = wct, - taskId = taskId, + taskId = launchingTaskId, minimizingTaskId = taskIdToMinimize, - exitingImmersiveTask = exitingImmersiveTask, + exitingImmersiveTask = exitImmersiveResult.asExit()?.exitingTask, ) taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) } + exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t) return t } if (taskIdToMinimize == null) { val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) + exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t) return t } val remoteTransitionHandler = @@ -704,6 +702,7 @@ class DesktopTasksController( val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) taskIdToMinimize.let { addPendingMinimizeTransition(t, it) } + exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t) return t } @@ -1472,10 +1471,14 @@ class DesktopTasksController( ) } WINDOWING_MODE_FREEFORM -> { - // TODO(b/336289597): This currently does not respect the desktop window limit. val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, fillIn, options.toBundle()) - transitions.startTransition(TRANSIT_OPEN, wct, null) + startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + launchingTaskId = null, + displayId = callingTaskInfo.displayId + ) } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index 49cf8ae81aa8..35e6c8dd6580 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -46,6 +46,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { "ShellBackPreview"), WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_TASK_OBSERVER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + Consts.TAG_WM_SHELL), // TODO(b/282232877): turn logToLogcat to false. WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM_SHELL), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index b58f0681c571..68dc0f27bca1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -45,9 +45,15 @@ oneway interface IRecentTasksListener { */ void onRunningTaskChanged(in RunningTaskInfo taskInfo); - /** A task has moved to front. */ - void onTaskMovedToFront(in GroupedTaskInfo[] visibleTasks); + /** A task has moved to front. Only used if enableShellTopTaskTracking() is disabled. */ + void onTaskMovedToFront(in GroupedTaskInfo taskToFront); - /** A task info has changed. */ + /** A task info has changed. Only used if enableShellTopTaskTracking() is disabled. */ void onTaskInfoChanged(in RunningTaskInfo taskInfo); + + /** + * If enableShellTopTaskTracking() is enabled, this reports the set of all visible tasks. + * Otherwise, this reports only the new top most visible task. + */ + void onVisibleTasksChanged(in GroupedTaskInfo[] visibleTasks); }
\ No newline at end of file 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 9911669d2cb8..6da4f510ab77 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 @@ -17,14 +17,18 @@ package com.android.wm.shell.recents; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PC; +import static com.android.wm.shell.Flags.enableShellTopTaskTracking; +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; import android.Manifest; import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.ActivityManager.RecentTaskInfo; +import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.KeyguardManager; @@ -65,7 +69,6 @@ import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; -import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; @@ -111,6 +114,11 @@ public class RecentTasksController implements TaskStackListenerCallback, private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>(); /** + * Cached list of the visible tasks, sorted from top most to bottom most. + */ + private final List<RunningTaskInfo> mVisibleTasks = new ArrayList<>(); + + /** * Creates {@link RecentTasksController}, returns {@code null} if the feature is not * supported. */ @@ -170,10 +178,8 @@ public class RecentTasksController implements TaskStackListenerCallback, mShellCommandHandler.addDumpCallback(this::dump, this); mTaskStackListener.addListener(this); mDesktopRepository.ifPresent(it -> it.addActiveTaskListener(this)); - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this, - mMainExecutor); - } + mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this, + mMainExecutor); mContext.getSystemService(KeyguardManager.class).addKeyguardLockedStateListener( mMainExecutor, isKeyguardLocked -> notifyRecentTasksChanged()); } @@ -205,7 +211,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mTaskSplitBoundsMap.put(taskId1, splitBounds); mTaskSplitBoundsMap.put(taskId2, splitBounds); notifyRecentTasksChanged(); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Add split pair: %d, %d, %s", + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Add split pair: %d, %d, %s", taskId1, taskId2, splitBounds); return true; } @@ -221,7 +227,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mTaskSplitBoundsMap.remove(taskId); mTaskSplitBoundsMap.remove(pairedTaskId); notifyRecentTasksChanged(); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Remove split pair: %d, %d", + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Remove split pair: %d, %d", taskId, pairedTaskId); } } @@ -234,7 +240,17 @@ public class RecentTasksController implements TaskStackListenerCallback, // We could do extra verification of requiring both taskIds of a pair and verifying that // the same split bounds object is returned... but meh. Seems unnecessary. - return mTaskSplitBoundsMap.get(taskId); + SplitBounds splitBounds = mTaskSplitBoundsMap.get(taskId); + if (splitBounds != null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "getSplitBoundsForTaskId: taskId=%d splitBoundsTasks=[%d, %d]", taskId, + splitBounds.leftTopTaskId, splitBounds.rightBottomTaskId); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "getSplitBoundsForTaskId: expected split bounds for taskId=%d but not found", + taskId); + } + return splitBounds; } @Override @@ -249,7 +265,10 @@ public class RecentTasksController implements TaskStackListenerCallback, @Override public void onTaskStackChanged() { - notifyRecentTasksChanged(); + if (!enableShellTopTaskTracking()) { + // Skip notifying recent tasks changed whenever task stack changes + notifyRecentTasksChanged(); + } } @Override @@ -263,15 +282,18 @@ public class RecentTasksController implements TaskStackListenerCallback, notifyRecentTasksChanged(); } - public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) { + public void onTaskAdded(RunningTaskInfo taskInfo) { notifyRunningTaskAppeared(taskInfo); } - public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) { + public void onTaskRemoved(RunningTaskInfo taskInfo) { // Remove any split pairs associated with this task removeSplitPair(taskInfo.taskId); - notifyRecentTasksChanged(); notifyRunningTaskVanished(taskInfo); + if (!enableShellTopTaskTracking()) { + // Only notify recent tasks changed if we aren't already notifying the visible tasks + notifyRecentTasksChanged(); + } } /** @@ -279,7 +301,7 @@ public class RecentTasksController implements TaskStackListenerCallback, * * This currently includes windowing mode and visibility. */ - public void onTaskRunningInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + public void onTaskRunningInfoChanged(RunningTaskInfo taskInfo) { notifyRecentTasksChanged(); notifyRunningTaskChanged(taskInfo); } @@ -290,14 +312,21 @@ public class RecentTasksController implements TaskStackListenerCallback, } @Override + public void onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo) { + notifyTaskMovedToFront(runningTaskInfo); + } + + @Override public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) { notifyTaskInfoChanged(taskInfo); } @Override - public void onTaskMovedToFrontThroughTransition( - ActivityManager.RunningTaskInfo runningTaskInfo) { - notifyTaskMovedToFront(runningTaskInfo); + public void onVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) { + mVisibleTasks.clear(); + mVisibleTasks.addAll(visibleTasks); + // Notify with all the info and not just the running task info + notifyVisibleTasksChanged(visibleTasks); } @VisibleForTesting @@ -316,7 +345,7 @@ public class RecentTasksController implements TaskStackListenerCallback, /** * Notify the running task listener that a task appeared on desktop environment. */ - private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { + private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) { if (mListener == null || !shouldEnableRunningTasksForDesktopMode() || taskInfo.realActivity == null) { @@ -330,9 +359,25 @@ public class RecentTasksController implements TaskStackListenerCallback, } /** + * Notify the running task listener that a task was changed on desktop environment. + */ + private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) { + if (mListener == null + || !shouldEnableRunningTasksForDesktopMode() + || taskInfo.realActivity == null) { + return; + } + try { + mListener.onRunningTaskChanged(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onRunningTaskChanged", e); + } + } + + /** * Notify the running task listener that a task was removed on desktop environment. */ - private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) { if (mListener == null || !shouldEnableRunningTasksForDesktopMode() || taskInfo.realActivity == null) { @@ -346,25 +391,30 @@ public class RecentTasksController implements TaskStackListenerCallback, } /** - * Notify the running task listener that a task was changed on desktop environment. + * Notify the recents task listener that a task moved to front via a transition. */ - private void notifyRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null - || !shouldEnableRunningTasksForDesktopMode() - || taskInfo.realActivity == null) { + || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() + || taskInfo.realActivity == null + || enableShellTopTaskTracking()) { return; } try { - mListener.onRunningTaskChanged(taskInfo); + mListener.onTaskMovedToFront(GroupedTaskInfo.forFullscreenTasks(taskInfo)); } catch (RemoteException e) { - Slog.w(TAG, "Failed call onRunningTaskChanged", e); + Slog.w(TAG, "Failed call onTaskMovedToFront", e); } } + /** + * Notify the recents task listener that a task changed via a transition. + */ private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() - || taskInfo.realActivity == null) { + || taskInfo.realActivity == null + || enableShellTopTaskTracking()) { return; } try { @@ -374,17 +424,21 @@ public class RecentTasksController implements TaskStackListenerCallback, } } - private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { + /** + * Notifies that the test of visible tasks have changed. + */ + private void notifyVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) { if (mListener == null || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() - || taskInfo.realActivity == null) { + || !enableShellTopTaskTracking()) { return; } try { - GroupedTaskInfo runningTask = GroupedTaskInfo.forFullscreenTasks(taskInfo); - mListener.onTaskMovedToFront(new GroupedTaskInfo[]{ runningTask }); + // Compute the visible recent tasks in order, and move the task to the top + mListener.onVisibleTasksChanged(generateList(visibleTasks) + .toArray(new GroupedTaskInfo[0])); } catch (RemoteException e) { - Slog.w(TAG, "Failed call onTaskMovedToFront", e); + Slog.w(TAG, "Failed call onVisibleTasksChanged", e); } } @@ -397,6 +451,11 @@ public class RecentTasksController implements TaskStackListenerCallback, @VisibleForTesting void registerRecentTasksListener(IRecentTasksListener listener) { mListener = listener; + if (enableShellTopTaskTracking()) { + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "registerRecentTasksListener"); + // Post a notification for the current set of visible tasks + mMainExecutor.executeDelayed(() -> notifyVisibleTasksChanged(mVisibleTasks), 0); + } } @VisibleForTesting @@ -411,14 +470,18 @@ public class RecentTasksController implements TaskStackListenerCallback, @VisibleForTesting ArrayList<GroupedTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { - // Note: the returned task list is from the most-recent to least-recent order - final List<RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks( - maxNum, flags, userId); + // Note: the returned task list is ordered from the most-recent to least-recent order + return generateList(mActivityTaskManager.getRecentTasks(maxNum, flags, userId)); + } + /** + * Generates a list of GroupedTaskInfos for the given list of tasks. + */ + private <T extends TaskInfo> ArrayList<GroupedTaskInfo> generateList(@NonNull List<T> tasks) { // Make a mapping of task id -> task info final SparseArray<TaskInfo> rawMapping = new SparseArray<>(); - for (int i = 0; i < rawList.size(); i++) { - final TaskInfo taskInfo = rawList.get(i); + for (int i = 0; i < tasks.size(); i++) { + final TaskInfo taskInfo = tasks.get(i); rawMapping.put(taskInfo.taskId, taskInfo); } @@ -427,10 +490,10 @@ public class RecentTasksController implements TaskStackListenerCallback, int mostRecentFreeformTaskIndex = Integer.MAX_VALUE; + ArrayList<GroupedTaskInfo> groupedTasks = new ArrayList<>(); // Pull out the pairs as we iterate back in the list - ArrayList<GroupedTaskInfo> recentTasks = new ArrayList<>(); - for (int i = 0; i < rawList.size(); i++) { - final RecentTaskInfo taskInfo = rawList.get(i); + for (int i = 0; i < tasks.size(); i++) { + final TaskInfo taskInfo = tasks.get(i); if (!rawMapping.contains(taskInfo.taskId)) { // If it's not in the mapping, then it was already paired with another task continue; @@ -441,7 +504,7 @@ public class RecentTasksController implements TaskStackListenerCallback, && mDesktopRepository.get().isActiveTask(taskInfo.taskId)) { // Freeform tasks will be added as a separate entry if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) { - mostRecentFreeformTaskIndex = recentTasks.size(); + mostRecentFreeformTaskIndex = groupedTasks.size(); } // If task has their app bounds set to null which happens after reboot, set the // app bounds to persisted lastFullscreenBounds. Also set the position in parent @@ -461,36 +524,34 @@ public class RecentTasksController implements TaskStackListenerCallback, } final int pairedTaskId = mSplitTasks.get(taskInfo.taskId, INVALID_TASK_ID); - if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains( - pairedTaskId)) { + if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) { final TaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId); rawMapping.remove(pairedTaskId); - recentTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, + groupedTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, mTaskSplitBoundsMap.get(pairedTaskId))); } else { - recentTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo)); + // TODO(346588978): Consolidate multiple visible fullscreen tasks into the same + // grouped task + groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo)); } } // Add a special entry for freeform tasks if (!freeformTasks.isEmpty()) { - recentTasks.add(mostRecentFreeformTaskIndex, + groupedTasks.add(mostRecentFreeformTaskIndex, GroupedTaskInfo.forFreeformTasks( freeformTasks, minimizedFreeformTasks)); } - return recentTasks; - } + if (enableShellTopTaskTracking()) { + // We don't current send pinned tasks as a part of recent or running tasks, so remove + // them from the list here + groupedTasks.removeIf( + gti -> gti.getTaskInfo1().getWindowingMode() == WINDOWING_MODE_PINNED); + } - /** - * Returns the top running leaf task. - */ - @Nullable - public ActivityManager.RunningTaskInfo getTopRunningTask() { - List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1, - false /* filterOnlyVisibleRecents */); - return tasks.isEmpty() ? null : tasks.get(0); + return groupedTasks; } /** @@ -498,12 +559,13 @@ public class RecentTasksController implements TaskStackListenerCallback, * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task. */ @Nullable - public ActivityManager.RunningTaskInfo getTopRunningTask( + public RunningTaskInfo getTopRunningTask( @Nullable WindowContainerToken ignoreTaskToken) { - List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2, - false /* filterOnlyVisibleRecents */); + final List<RunningTaskInfo> tasks = enableShellTopTaskTracking() + ? mVisibleTasks + : mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */); for (int i = 0; i < tasks.size(); i++) { - final ActivityManager.RunningTaskInfo task = tasks.get(i); + final RunningTaskInfo task = tasks.get(i); if (task.token.equals(ignoreTaskToken)) { continue; } @@ -541,7 +603,7 @@ public class RecentTasksController implements TaskStackListenerCallback, } /** - * Find the background task that match the given taskId. + * Find the background task (in the recent tasks list) that matches the given taskId. */ @Nullable public RecentTaskInfo findTaskInBackground(int taskId) { @@ -638,29 +700,34 @@ public class RecentTasksController implements TaskStackListenerCallback, } @Override - public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) { + public void onRunningTaskAppeared(RunningTaskInfo taskInfo) { mListener.call(l -> l.onRunningTaskAppeared(taskInfo)); } @Override - public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + public void onRunningTaskVanished(RunningTaskInfo taskInfo) { mListener.call(l -> l.onRunningTaskVanished(taskInfo)); } @Override - public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + public void onRunningTaskChanged(RunningTaskInfo taskInfo) { mListener.call(l -> l.onRunningTaskChanged(taskInfo)); } @Override - public void onTaskMovedToFront(GroupedTaskInfo[] taskInfo) { - mListener.call(l -> l.onTaskMovedToFront(taskInfo)); + public void onTaskMovedToFront(GroupedTaskInfo taskToFront) { + mListener.call(l -> l.onTaskMovedToFront(taskToFront)); } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { mListener.call(l -> l.onTaskInfoChanged(taskInfo)); } + + @Override + public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) { + mListener.call(l -> l.onVisibleTasksChanged(visibleTasks)); + } }; public IRecentTasksImpl(RecentTasksController controller) { @@ -714,12 +781,12 @@ public class RecentTasksController implements TaskStackListenerCallback, } @Override - public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) { - final ActivityManager.RunningTaskInfo[][] tasks = - new ActivityManager.RunningTaskInfo[][]{null}; + public RunningTaskInfo[] getRunningTasks(int maxNum) { + final RunningTaskInfo[][] tasks = + new RunningTaskInfo[][]{null}; executeRemoteCallWithTaskPermission(mController, "getRunningTasks", (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum) - .toArray(new ActivityManager.RunningTaskInfo[0]), + .toArray(new RunningTaskInfo[0]), true /* blocking */); return tasks[0]; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt index d28a462546f9..93f2e4cf0e45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -17,37 +17,162 @@ package com.android.wm.shell.recents import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.os.IBinder import android.util.ArrayMap import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopModeFlags import android.window.TransitionInfo +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.Flags.enableShellTopTaskTracking +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER import com.android.wm.shell.shared.TransitionUtil +import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import dagger.Lazy +import java.io.PrintWriter +import java.util.StringJoiner import java.util.concurrent.Executor /** - * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners - * about task stack changes. + * A [Transitions.TransitionObserver] that observes shell transitions, tracks the visible tasks + * and notifies listeners whenever the visible tasks change (at the start and end of a transition). * - * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it + * This can be replaced once we have a generalized task repository tracking visible tasks. */ class TaskStackTransitionObserver( + shellInit: ShellInit, + private val shellTaskOrganizer: Lazy<ShellTaskOrganizer>, + private val shellCommandHandler: ShellCommandHandler, private val transitions: Lazy<Transitions>, - shellInit: ShellInit -) : Transitions.TransitionObserver { +) : Transitions.TransitionObserver, ShellTaskOrganizer.TaskVanishedListener { + + // List of currently visible tasks sorted in z-order from top-most to bottom-most, only used + // when Flags.enableShellTopTaskTracking() is enabled. + private var visibleTasks: MutableList<RunningTaskInfo> = mutableListOf() + private val pendingCloseTasks: MutableList<RunningTaskInfo> = mutableListOf() + // Set of listeners to notify when the visible tasks change private val taskStackTransitionObserverListeners = ArrayMap<TaskStackTransitionObserverListener, Executor>() + // Used to filter out leaf-tasks + private val leafTaskFilter: TransitionUtil.LeafTaskFilter = TransitionUtil.LeafTaskFilter() init { shellInit.addInitCallback(::onInit, this) } fun onInit() { + shellTaskOrganizer.get().addTaskVanishedListener(this) + shellCommandHandler.addDumpCallback(::dump, this) transitions.get().registerObserver(this) + + // TODO(346588978): We need to update the running tasks once the ShellTaskOrganizer is + // registered since there is no existing transition (yet) corresponding for the already + // visible tasks + } + + /** + * This method handles transition ready when only + * DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL is set. + */ + private fun onDesktopOnlyFlagTransitionReady(info: TransitionInfo) { + for (change in info.changes) { + if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { + continue + } + + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + continue + } + + // Find the first task that is opening, this should be the one at the front after + // the transition + if (TransitionUtil.isOpeningType(change.mode)) { + notifyOnTaskMovedToFront(taskInfo) + break + } else if (change.mode == TRANSIT_CHANGE) { + notifyOnTaskChanged(taskInfo) + } + } + } + + /** + * This method handles transition ready when Flags.enableShellTopTaskTracking() is set. + */ + private fun onShellTopTaskTrackerFlagTransitionReady(info: TransitionInfo) { + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Transition ready: %d", info.debugId) + + // Filter out non-leaf tasks (we will likely need them later, but visible task tracking + // is currently used only for visible leaf tasks) + val changesReversed = mutableListOf<TransitionInfo.Change>() + for (change in info.changes) { + if (!leafTaskFilter.test(change)) { + // Not a leaf task + continue + } + changesReversed.add(0, change) + } + + // We iterate the change list in reverse order because changes are sorted top to bottom and + // we want to update the lists such that the top most tasks are inserted at the front last + var notifyChanges = false + for (change in changesReversed) { + val taskInfo = change.taskInfo + if (taskInfo == null || taskInfo.taskId == -1) { + // Not a valid task + continue + } + + if (TransitionUtil.isClosingMode(change.mode)) { + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tClosing task=%d", taskInfo.taskId) + + // Closing task's visibilities are not committed until after the transition + // completes, so track such tasks so that we can notify on finish + if (!pendingCloseTasks.any { it.taskId == taskInfo.taskId }) { + pendingCloseTasks.add(taskInfo) + } + } else if (TransitionUtil.isOpeningMode(change.mode) + || TransitionUtil.isOrderOnly(change)) { + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tOpening task=%d", taskInfo.taskId) + + // Remove from pending close tasks list if it's being opened again + pendingCloseTasks.removeIf { it.taskId == taskInfo.taskId } + // Move the task to the front of the visible tasks list + visibleTasks.removeIf { it.taskId == taskInfo.taskId } + visibleTasks.add(0, taskInfo) + notifyChanges = true + } + } + + // TODO(346588978): We should verify the task list has actually changed before notifying + // (ie. starting an activity that's already top-most would result in no visible change) + if (notifyChanges) { + updateVisibleTasksList("transition-start") + } + } + + private fun updateVisibleTasksList(reason: String) { + // This simply constructs a list of visible tasks, where the always-on-top tasks are moved + // to the front of the list in-order, to ensure that they match the visible z order + val orderedVisibleTasks = mutableListOf<RunningTaskInfo>() + var numAlwaysOnTop = 0 + for (info in visibleTasks) { + if (info.windowingMode == WINDOWING_MODE_PINNED + || info.configuration.windowConfiguration.isAlwaysOnTop) { + orderedVisibleTasks.add(numAlwaysOnTop, info) + numAlwaysOnTop++ + } else { + orderedVisibleTasks.add(info) + } + } + visibleTasks = orderedVisibleTasks + + dumpVisibleTasks(reason) + notifyVisibleTasksChanged(visibleTasks) } override fun onTransitionReady( @@ -56,26 +181,10 @@ class TaskStackTransitionObserver( startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction ) { - if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) { - for (change in info.changes) { - if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) { - continue - } - - val taskInfo = change.taskInfo - if (taskInfo == null || taskInfo.taskId == -1) { - continue - } - - // Find the first task that is opening, this should be the one at the front after - // the transition - if (TransitionUtil.isOpeningType(change.mode)) { - notifyOnTaskMovedToFront(taskInfo) - break - } else if (change.mode == TRANSIT_CHANGE) { - notifyOnTaskChanged(taskInfo) - } - } + if (enableShellTopTaskTracking()) { + onShellTopTaskTrackerFlagTransitionReady(info) + } else if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) { + onDesktopOnlyFlagTransitionReady(info) } } @@ -83,8 +192,35 @@ class TaskStackTransitionObserver( override fun onTransitionMerged(merged: IBinder, playing: IBinder) {} - override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {} + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + if (enableShellTopTaskTracking()) { + if (pendingCloseTasks.isNotEmpty()) { + // Update the visible task list based on the pending close tasks + for (change in pendingCloseTasks) { + visibleTasks.removeIf { + it.taskId == change.taskId + } + } + updateVisibleTasksList("transition-finished") + } + } + } + override fun onTaskVanished(taskInfo: RunningTaskInfo?) { + if (!enableShellTopTaskTracking()) { + return + } + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Task vanished: task=%d", taskInfo?.taskId) + pendingCloseTasks.removeIf { it.taskId == taskInfo?.taskId } + if (visibleTasks.any { it.taskId == taskInfo?.taskId }) { + visibleTasks.removeIf { it.taskId == taskInfo?.taskId } + updateVisibleTasksList("task-vanished") + } + } + + /** + * Adds a new task stack observer. + */ fun addTaskStackTransitionObserverListener( taskStackTransitionObserverListener: TaskStackTransitionObserverListener, executor: Executor @@ -92,6 +228,9 @@ class TaskStackTransitionObserver( taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor } + /** + * Removes an existing task stack observer. + */ fun removeTaskStackTransitionObserverListener( taskStackTransitionObserverListener: TaskStackTransitionObserverListener ) { @@ -99,22 +238,66 @@ class TaskStackTransitionObserver( } private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) { + if (enableShellTopTaskTracking()) { + return + } taskStackTransitionObserverListeners.forEach { (listener, executor) -> executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) } } } private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) { + if (enableShellTopTaskTracking()) { + return + } taskStackTransitionObserverListeners.forEach { (listener, executor) -> executor.execute { listener.onTaskChangedThroughTransition(taskInfo) } } } + private fun notifyVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) { + taskStackTransitionObserverListeners.forEach { (listener, executor) -> + executor.execute { listener.onVisibleTasksChanged(visibleTasks) } + } + } + + fun dump(pw: PrintWriter, prefix: String) { + pw.println("${prefix}$TAG:") + + if (visibleTasks.isEmpty()) { + pw.println("$prefix visibleTasks=[]") + } else { + val stringJoiner = StringJoiner(",\n\t", "[\n\t", "\n]") + visibleTasks.forEach { + stringJoiner.add("id=${it.taskId} cmp=${it.baseIntent.component}") + } + pw.println("$prefix visibleTasks=$stringJoiner") + } + } + + /** Dumps the set of visible tasks to protolog */ + private fun dumpVisibleTasks(reason: String) { + if (!WM_SHELL_TASK_OBSERVER.isEnabled) { + return + } + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tVisible tasks (%s)", reason) + for (task in visibleTasks) { + ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\t\ttaskId=%d package=%s", task.taskId, + task.baseIntent.component?.packageName) + } + } + /** Listener to use to get updates regarding task stack from this observer */ interface TaskStackTransitionObserverListener { /** Called when a task is moved to front. */ fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {} + /** Called when the set of visible tasks have changed. */ + fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {} /** Called when a task info has changed. */ fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {} } + + companion object { + const val TAG = "TaskStackTransitionObserver" + } } 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 cc0e1df115c2..7d1ffb80735e 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 @@ -1362,7 +1362,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void clearSplitPairedInRecents(@ExitReason int exitReason) { - if (!shouldBreakPairedTaskInRecents(exitReason) || !mShouldUpdateRecents) return; + if (!shouldBreakPairedTaskInRecents(exitReason) || !mShouldUpdateRecents) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearSplitPairedInRecents: skipping reason=%s", + !mShouldUpdateRecents ? "shouldn't update" : exitReasonToString(exitReason)); + return; + } ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearSplitPairedInRecents: reason=%s", exitReasonToString(exitReason)); @@ -1608,6 +1612,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void updateRecentTasksSplitPair() { // Preventing from single task update while processing recents. if (!mShouldUpdateRecents || !mPausingTasks.isEmpty()) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "updateRecentTasksSplitPair: skipping reason=%s", + !mShouldUpdateRecents ? "shouldn't update" : "no pausing tasks"); return; } mRecentTasks.ifPresent(recentTasks -> { diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt new file mode 100644 index 000000000000..e1120bdda194 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.Rotation.ROTATION_90 +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindowWithDragToTopDragZone +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by dragging it to the top drag zone. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppWindowWithDragToTopDragZoneLandscape : MaximizeAppWindowWithDragToTopDragZone( + rotation = ROTATION_90 +) { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWithDragToTopDragZone() = super.maximizeAppWithDragToTopDragZone() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt new file mode 100644 index 000000000000..fb910c7b907d --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt @@ -0,0 +1,48 @@ +/* + * 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP +import com.android.wm.shell.scenarios.MaximizeAppWindowWithDragToTopDragZone +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Maximize app window by dragging it to the top drag zone. + * + * Assert that the app window keeps the same increases in size, filling the vertical and horizontal + * stable display bounds. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MaximizeAppWindowWithDragToTopDragZonePortrait : MaximizeAppWindowWithDragToTopDragZone() { + @ExpectedScenarios(["MAXIMIZE_APP"]) + @Test + override fun maximizeAppWithDragToTopDragZone() = super.maximizeAppWithDragToTopDragZone() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt index 351a70094654..f9bf49ecae1c 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithAppHeaderExit.kt @@ -57,7 +57,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) mailApp.launchViaIntent(wmHelper) nonResizeableApp.launchViaIntent(wmHelper) } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt index 3f9927f1fab6..16e537361b66 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindow.kt @@ -40,7 +40,7 @@ abstract class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase() @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) mailApp.launchViaIntent(wmHelper) newTasksApp.launchViaIntent(wmHelper) imeApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt index 6d52a11153d9..c43a57594fb3 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowMultiWindowAndPip.kt @@ -47,7 +47,7 @@ open class DragAppWindowMultiWindowAndPip : DragAppWindowScenarioTestBase() Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) // Set string extra to ensure the app is on PiP mode at launch pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true")) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) mailApp.launchViaIntent(wmHelper) newTasksApp.launchViaIntent(wmHelper) imeApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt index 91cfd17340fc..786a8b710434 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/DragAppWindowSingleWindow.kt @@ -36,7 +36,7 @@ open class DragAppWindowSingleWindow : DragAppWindowScenarioTestBase() @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt index 967bd29958c2..0f546cdf97c5 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt @@ -49,7 +49,7 @@ constructor( @Test open fun enterDesktopWithDrag() { - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopModeWithDrag(wmHelper, device) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt index f442fdb31592..28008393da84 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt @@ -46,7 +46,7 @@ constructor( instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode)) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt index 6637b01f9d9c..5cf51e3be18c 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximiseAppWithCornerResize.kt @@ -66,7 +66,7 @@ abstract class MaximiseAppWithCornerResize( tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) ChangeDisplayOrientationRule.setRotation(rotation) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) testApp.cornerResize( wmHelper, device, diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt index a54d497bf511..d2be4944d365 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt @@ -58,7 +58,7 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: B tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) ChangeDisplayOrientationRule.setRotation(rotation) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt index a2b88f278ff2..60a0fb547909 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt @@ -59,7 +59,7 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) ChangeDisplayOrientationRule.setRotation(rotation) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt index b5483634b057..971637b62604 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt @@ -61,7 +61,7 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) ChangeDisplayOrientationRule.setRotation(rotation) - testApp1.enterDesktopWithDrag(wmHelper, device) + testApp1.enterDesktopMode(wmHelper, device) testApp2.launchViaIntent(wmHelper) testApp3.launchViaIntent(wmHelper) } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt index b86765e23422..7987f7ec59fa 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt @@ -58,7 +58,7 @@ open class MinimizeWindowOnAppOpen() @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) mailApp.launchViaIntent(wmHelper) newTasksApp.launchViaIntent(wmHelper) imeApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt index aad266fb8374..6ce36f53f0d1 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppsInDesktopMode.kt @@ -61,7 +61,7 @@ abstract class OpenAppsInDesktopMode(val rotation: Rotation = Rotation.ROTATION_ tapl.setExpectedRotation(rotation.value) tapl.enableTransientTaskbar(false) ChangeDisplayOrientationRule.setRotation(rotation) - firstApp.enterDesktopWithDrag(wmHelper, device) + firstApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt index bfee3181cbc0..eefa0bb3c00a 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindow.kt @@ -61,7 +61,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0, Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) mailApp.launchViaIntent(wmHelper) newTasksApp.launchViaIntent(wmHelper) imeApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt index 5b1b64e7c562..0226eb35de14 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppCornerMultiWindowAndPip.kt @@ -65,7 +65,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0, tapl.setExpectedRotation(rotation.value) // Set string extra to ensure the app is on PiP mode at launch pipApp.launchViaIntentAndWaitForPip(wmHelper, stringExtras = mapOf("enter_pip" to "true")) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) mailApp.launchViaIntent(wmHelper) newTasksApp.launchViaIntent(wmHelper) imeApp.launchViaIntent(wmHelper) diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt index a7cebf402d8e..64636230e5e0 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithCornerResize.kt @@ -67,7 +67,7 @@ abstract class ResizeAppWithCornerResize( tapl.setEnableRotation(true) ChangeDisplayOrientationRule.setRotation(rotation) tapl.setExpectedRotation(rotation.value) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt index 67802387b267..f198cfed7c50 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ResizeAppWithEdgeResize.kt @@ -60,7 +60,7 @@ constructor( ) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt index 2b40497844ef..fd4c2434589d 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithButton.kt @@ -56,7 +56,7 @@ constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt index b4bd7e1c5211..62e860ed24a7 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SnapResizeAppWindowWithDrag.kt @@ -56,7 +56,7 @@ constructor(private val toLeft: Boolean = true, isResizable: Boolean = true) { @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt index f08e50e0d4ee..de330e072ad1 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionResizeAndDrag.kt @@ -59,7 +59,7 @@ open class StartAppMediaProjectionResizeAndDrag { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) tapl.setEnableRotation(true) tapl.setExpectedRotation(0) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt index ce235d445fe5..4b3f15f1db86 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithMaxDesktopWindows.kt @@ -67,7 +67,7 @@ open class StartAppMediaProjectionWithMaxDesktopWindows { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) tapl.setEnableRotation(true) tapl.setExpectedRotation(0) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt index 005195296c62..a1083671f687 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/StartScreenMediaProjectionWithMaxDesktopWindows.kt @@ -62,7 +62,7 @@ open class StartScreenMediaProjectionWithMaxDesktopWindows { @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt index dad2eb633c72..1455bd1888e2 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/SwitchToOverviewFromDesktop.kt @@ -54,7 +54,7 @@ constructor(val navigationMode: NavBar = NavBar.MODE_GESTURAL) { @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) - testApp.enterDesktopWithDrag(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt new file mode 100644 index 000000000000..528ca7e94ee3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell + +import com.android.wm.shell.common.ShellExecutor + +/** + * Test ShellExecutor that runs everything synchronously. + */ +class TestSyncExecutor : ShellExecutor { + override fun execute(runnable: Runnable) { + runnable.run() + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + runnable.run() + } + + override fun removeCallbacks(runnable: Runnable) { + } + + override fun hasCallback(runnable: Runnable): Boolean { + return false + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index f21f26443748..62717a32d99f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -31,6 +31,8 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TransitionType @@ -47,6 +49,7 @@ import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler.PendingMix import com.android.wm.shell.freeform.FreeformTaskTransitionHandler import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -491,6 +494,72 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startLaunchTransition_unknownLaunchingTask_animates() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any())) + .thenReturn(mock()) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = null, + ) + + val started = mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(createChange(task, mode = TRANSIT_OPEN)) + ), + StubTransaction(), + StubTransaction(), + ) { } + + assertThat(started).isEqualTo(true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startLaunchTransition_unknownLaunchingTaskOverImmersive_animatesImmersiveChange() { + val wct = WindowContainerTransaction() + val immersiveTask = createTask(WINDOWING_MODE_FREEFORM) + val openingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any())) + .thenReturn(mock()) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = null, + exitingImmersiveTask = immersiveTask.taskId, + ) + + val immersiveChange = createChange(immersiveTask, mode = TRANSIT_CHANGE) + val openingChange = createChange(openingTask, mode = TRANSIT_OPEN) + val started = mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(immersiveChange, openingChange) + ), + StubTransaction(), + StubTransaction(), + ) { } + + assertThat(started).isEqualTo(true) + verify(desktopImmersiveController) + .animateResizeChange(eq(immersiveChange), any(), any(), any()) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { val wct = WindowContainerTransaction() @@ -712,9 +781,13 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { changes.forEach { change -> addChange(change) } } - private fun createChange(task: RunningTaskInfo): TransitionInfo.Change = + private fun createChange( + task: RunningTaskInfo, + @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE + ): TransitionInfo.Change = TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task + setMode(mode) } private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo = 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 ad266ead774e..2319716617bf 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 @@ -100,6 +100,7 @@ import com.android.wm.shell.common.LaunchAdjacentController import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener @@ -167,6 +168,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.times import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.kotlin.eq @@ -294,10 +296,10 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>())) - .thenReturn(DesktopImmersiveController.ExitResult.NoExit) + .thenReturn(ExitResult.NoExit) whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull())) - .thenReturn(DesktopImmersiveController.ExitResult.NoExit) + .thenReturn(ExitResult.NoExit) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -1833,7 +1835,8 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) .thenReturn(transition) whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task))) - .thenReturn(DesktopImmersiveController.ExitResult.Exit( + .thenReturn( + ExitResult.Exit( exitingTask = task.taskId, runOnTransitionStart = runOnTransit, )) @@ -3214,13 +3217,43 @@ class DesktopTasksControllerTest : ShellTestCase() { fun newWindow_fromFreeformAddsNewWindow() { setUpLandscapeDisplay() val task = setUpFreeformTask() - val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctCaptor = argumentCaptor<WindowContainerTransaction>() + val transition = Binder() + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull())) + .thenReturn(ExitResult.NoExit) + whenever(desktopMixedTransitionHandler + .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn(transition) + runOpenNewWindow(task) - verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions) + + verify(desktopMixedTransitionHandler) + .startLaunchTransition(anyInt(), wctCaptor.capture(), anyOrNull(), anyOrNull(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions) .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFreeform_exitsImmersiveIfNeeded() { + setUpLandscapeDisplay() + val immersiveTask = setUpFreeformTask() + val task = setUpFreeformTask() + val runOnStart = RunOnStartTransitionCallback() + val transition = Binder() + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull())) + .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart)) + whenever(desktopMixedTransitionHandler + .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn(transition) + + runOpenNewWindow(task) + + runOnStart.assertOnlyInvocation(transition) + } + private fun runOpenNewWindow(task: RunningTaskInfo) { markTaskVisible(task) task.baseActivity = mock(ComponentName::class.java) @@ -3314,7 +3347,8 @@ class DesktopTasksControllerTest : ShellTestCase() { .thenReturn(transition) whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId))) - .thenReturn(DesktopImmersiveController.ExitResult.Exit( + .thenReturn( + ExitResult.Exit( exitingTask = immersiveTask.taskId, runOnTransitionStart = runOnStartTransit, )) @@ -3719,7 +3753,8 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)) - .thenReturn(DesktopImmersiveController.ExitResult.Exit( + .thenReturn( + ExitResult.Exit( exitingTask = 5, runOnTransitionStart = runOnStartTransit, )) @@ -3740,7 +3775,8 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)) - .thenReturn(DesktopImmersiveController.ExitResult.Exit( + .thenReturn( + ExitResult.Exit( exitingTask = 5, runOnTransitionStart = runOnStartTransit, )) @@ -3760,7 +3796,8 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))) - .thenReturn(DesktopImmersiveController.ExitResult.Exit( + .thenReturn( + ExitResult.Exit( exitingTask = 5, runOnTransitionStart = runOnStartTransit, )) @@ -3782,7 +3819,8 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))) - .thenReturn(DesktopImmersiveController.ExitResult.Exit( + .thenReturn( + ExitResult.Exit( exitingTask = 5, runOnTransitionStart = runOnStartTransit, )) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt index 2b30bc360d06..fd3adabfd44b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt @@ -176,6 +176,30 @@ class GroupedTaskInfoTest : ShellTestCase() { assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray()) } + @Test + fun testGetTaskById_singleTasks() { + val task1 = createTaskInfo(id = 1234) + + val taskInfo = GroupedTaskInfo.forFullscreenTasks(task1) + + assertThat(taskInfo.getTaskById(1234)).isEqualTo(task1) + assertThat(taskInfo.containsTask(1234)).isTrue() + } + + @Test + fun testGetTaskById_multipleTasks() { + val task1 = createTaskInfo(id = 1) + val task2 = createTaskInfo(id = 2) + val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50) + + val taskInfo = GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds) + + assertThat(taskInfo.getTaskById(1)).isEqualTo(task1) + assertThat(taskInfo.getTaskById(2)).isEqualTo(task2) + assertThat(taskInfo.containsTask(1)).isTrue() + assertThat(taskInfo.containsTask(2)).isTrue() + } + private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply { taskId = id token = WindowContainerToken(mock(IWindowContainerToken::class.java)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index dede583ca970..12c397868f5a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -610,7 +610,7 @@ public class RecentTasksControllerTest extends ShellTestCase { mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo); GroupedTaskInfo runningTask = GroupedTaskInfo.forFullscreenTasks(taskInfo); - verify(mRecentTasksListener).onTaskMovedToFront(eq(new GroupedTaskInfo[] { runningTask })); + verify(mRecentTasksListener).onTaskMovedToFront(eq(runningTask)); } @Test @@ -656,6 +656,35 @@ public class RecentTasksControllerTest extends ShellTestCase { assertEquals(splitBounds4, pair2Bounds); } + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + public void shellTopTaskTracker_onTaskStackChanged_expectNoRecentsChanged() throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + mRecentTasksControllerReal.onTaskStackChanged(); + verify(mRecentTasksListener, never()).onRecentTasksChanged(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + public void shellTopTaskTracker_onTaskRemoved_expectNoRecentsChanged() throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + mRecentTasksControllerReal.onTaskRemoved(taskInfo); + verify(mRecentTasksListener, never()).onRecentTasksChanged(); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + public void shellTopTaskTracker_onVisibleTasksChanged() throws Exception { + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10); + mRecentTasksControllerReal.onVisibleTasksChanged(List.of(taskInfo)); + verify(mRecentTasksListener, never()).onVisibleTasksChanged(any()); + } + /** * Helper to create a task with a given task id. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt index efe4fb18f273..99194620c313 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -16,21 +16,36 @@ package com.android.wm.shell.recents -import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.app.TaskInfo import android.app.WindowConfiguration +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED +import android.content.ComponentName +import android.content.Intent import android.os.IBinder +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_FIRST_CUSTOM +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IWindowContainerToken import android.window.TransitionInfo +import android.window.TransitionInfo.FLAG_MOVED_TO_TOP import android.window.WindowContainerToken import androidx.test.filters.SmallTest import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.TestSyncExecutor import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions @@ -61,7 +76,10 @@ class TaskStackTransitionObserverTest { @JvmField @Rule val setFlagsRule = SetFlagsRule() @Mock private lateinit var shellInit: ShellInit - @Mock lateinit var testExecutor: ShellExecutor + @Mock private lateinit var shellTaskOrganizerLazy: Lazy<ShellTaskOrganizer> + @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock private lateinit var shellCommandHandler: ShellCommandHandler + @Mock private lateinit var testExecutor: ShellExecutor @Mock private lateinit var transitionsLazy: Lazy<Transitions> @Mock private lateinit var transitions: Transitions @Mock private lateinit var mockTransitionBinder: IBinder @@ -73,24 +91,23 @@ class TaskStackTransitionObserverTest { MockitoAnnotations.initMocks(this) shellInit = Mockito.spy(ShellInit(testExecutor)) whenever(transitionsLazy.get()).thenReturn(transitions) - transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit) - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(shellInit) - .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) - initRunnableCaptor.value.run() - } else { - transitionObserver.onInit() - } + whenever(shellTaskOrganizerLazy.get()).thenReturn(shellTaskOrganizer) + transitionObserver = TaskStackTransitionObserver(shellInit, shellTaskOrganizerLazy, + shellCommandHandler, transitionsLazy) + + val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) + verify(shellInit) + .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) + initRunnableCaptor.value.run() } @Test - @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun testRegistersObserverAtInit() { verify(transitions).registerObserver(same(transitionObserver)) } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun taskCreated_freeformWindow_listenerNotified() { val listener = TestListener() @@ -98,11 +115,11 @@ class TaskStackTransitionObserverTest { transitionObserver.addTaskStackTransitionObserverListener(listener, executor) val change = createChange( - WindowManager.TRANSIT_OPEN, + TRANSIT_OPEN, createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val transitionInfo = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) callOnTransitionFinished() @@ -114,6 +131,7 @@ class TaskStackTransitionObserverTest { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun taskCreated_fullscreenWindow_listenerNotified() { val listener = TestListener() @@ -121,11 +139,11 @@ class TaskStackTransitionObserverTest { transitionObserver.addTaskStackTransitionObserverListener(listener, executor) val change = createChange( - WindowManager.TRANSIT_OPEN, - createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + TRANSIT_OPEN, + createTaskInfo(1, WINDOWING_MODE_FULLSCREEN) ) val transitionInfo = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() callOnTransitionReady(transitionInfo) callOnTransitionFinished() @@ -133,10 +151,11 @@ class TaskStackTransitionObserverTest { assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(1) assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) - .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() { val listener = TestListener() @@ -144,7 +163,7 @@ class TaskStackTransitionObserverTest { transitionObserver.addTaskStackTransitionObserverListener(listener, executor) val freeformOpenChange = createChange( - WindowManager.TRANSIT_OPEN, + TRANSIT_OPEN, createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val freeformReorderChange = @@ -153,7 +172,7 @@ class TaskStackTransitionObserverTest { createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val transitionInfo = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0) + TransitionInfoBuilder(TRANSIT_OPEN, 0) .addChange(freeformOpenChange) .addChange(freeformReorderChange) .build() @@ -169,6 +188,7 @@ class TaskStackTransitionObserverTest { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun transitionMerged_withChange_onlyOpenChangeIsNotified() { val listener = TestListener() @@ -178,11 +198,11 @@ class TaskStackTransitionObserverTest { // Create open transition val change = createChange( - WindowManager.TRANSIT_OPEN, + TRANSIT_OPEN, createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val transitionInfo = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() // create change transition to be merged to above transition val mergedChange = @@ -212,6 +232,7 @@ class TaskStackTransitionObserverTest { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun transitionMerged_withOpen_lastOpenChangeIsNotified() { val listener = TestListener() @@ -221,20 +242,20 @@ class TaskStackTransitionObserverTest { // Create open transition val change = createChange( - WindowManager.TRANSIT_OPEN, + TRANSIT_OPEN, createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val transitionInfo = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build() + TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() // create change transition to be merged to above transition val mergedChange = createChange( - WindowManager.TRANSIT_OPEN, + TRANSIT_OPEN, createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val mergedTransitionInfo = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(mergedChange).build() + TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(mergedChange).build() val mergedTransition = Mockito.mock(IBinder::class.java) callOnTransitionReady(transitionInfo) @@ -250,6 +271,7 @@ class TaskStackTransitionObserverTest { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun taskChange_freeformWindowToFullscreenWindow_listenerNotified() { val listener = TestListener() @@ -257,11 +279,11 @@ class TaskStackTransitionObserverTest { transitionObserver.addTaskStackTransitionObserverListener(listener, executor) val freeformState = createChange( - WindowManager.TRANSIT_OPEN, + TRANSIT_OPEN, createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val transitionInfoOpen = - TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(freeformState).build() + TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(freeformState).build() callOnTransitionReady(transitionInfoOpen) callOnTransitionFinished() executor.flushAll() @@ -276,7 +298,7 @@ class TaskStackTransitionObserverTest { val fullscreenState = createChange( WindowManager.TRANSIT_CHANGE, - createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + createTaskInfo(1, WINDOWING_MODE_FULLSCREEN) ) val transitionInfoChange = TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) @@ -301,6 +323,7 @@ class TaskStackTransitionObserverTest { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun singleTransition_withOpenAndChange_onlyOpenIsNotified() { val listener = TestListener() @@ -310,13 +333,13 @@ class TaskStackTransitionObserverTest { // Creating multiple changes to be fired in a single transition val freeformState = createChange( - mode = WindowManager.TRANSIT_OPEN, + mode = TRANSIT_OPEN, taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) ) val fullscreenState = createChange( mode = WindowManager.TRANSIT_CHANGE, - taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + taskInfo = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN) ) val transitionInfoWithChanges = @@ -336,6 +359,7 @@ class TaskStackTransitionObserverTest { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) fun singleTransition_withMultipleChanges_listenerNotified_forEachChange() { val listener = TestListener() @@ -349,7 +373,7 @@ class TaskStackTransitionObserverTest { listOf( WindowConfiguration.WINDOWING_MODE_FREEFORM, WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION, - WindowConfiguration.WINDOWING_MODE_FULLSCREEN + WINDOWING_MODE_FULLSCREEN ) .map { change -> createChange( @@ -376,19 +400,259 @@ class TaskStackTransitionObserverTest { } } - class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { - var taskInfoOnTaskMovedToFront = ActivityManager.RunningTaskInfo() - var taskInfoOnTaskChanged = mutableListOf<ActivityManager.RunningTaskInfo>() + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun openTransition_visibleTasksChanged() { + val listener = TestListener() + val executor = TestSyncExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Model an opening task + val firstOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(firstOpeningTransition) + callOnTransitionFinished() + // Assert that the task is reported visible + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + assertVisibleTasks(listener, listOf(1)) + + // Model opening another task + val nextOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN), + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(nextOpeningTransition) + // Assert that the visible list from top to bottom is valid (opening, closing) + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2) + assertVisibleTasks(listener, listOf(2, 1)) + + callOnTransitionFinished() + // Assert that after the transition finishes, there is only the opening task remaining + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(3) + assertVisibleTasks(listener, listOf(2)) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun toFrontTransition_visibleTasksChanged() { + val listener = TestListener() + val executor = TestSyncExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Model an opening task + val firstOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(firstOpeningTransition) + callOnTransitionFinished() + // Assert that the task is reported visible + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + assertVisibleTasks(listener, listOf(1)) + + // Model opening another task + val nextOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(nextOpeningTransition) + callOnTransitionFinished() + // Assert that the visible list from top to bottom is valid + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2) + assertVisibleTasks(listener, listOf(2, 1)) + + // Model the first task moving to front + val toFrontTransition = + createTransitionInfo(TRANSIT_TO_FRONT, + listOf( + createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN, + FLAG_MOVED_TO_TOP), + ) + ) + + callOnTransitionReady(toFrontTransition) + callOnTransitionFinished() + // Assert that the visible list from top to bottom is valid + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(3) + assertVisibleTasks(listener, listOf(1, 2)) + } - override fun onTaskMovedToFrontThroughTransition( - taskInfo: ActivityManager.RunningTaskInfo - ) { + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun closeTransition_visibleTasksChanged() { + val listener = TestListener() + val executor = TestSyncExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Model an opening task + val firstOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(firstOpeningTransition) + callOnTransitionFinished() + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + + // Model a closing task + val nextOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(nextOpeningTransition) + // Assert that the visible list hasn't changed (the close is pending) + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + + callOnTransitionFinished() + // Assert that after the transition finishes, there is only the opening task remaining + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2) + assertVisibleTasks(listener, listOf()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun changeTransition_visibleTasksUnchanged() { + val listener = TestListener() + val executor = TestSyncExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Model an opening task + val firstOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(firstOpeningTransition) + callOnTransitionFinished() + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + + // Model a closing task + val nextOpeningTransition = + createTransitionInfo( + TRANSIT_FIRST_CUSTOM, + listOf( + createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(nextOpeningTransition) + // Assert that the visible list hasn't changed + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun taskVanished_visibleTasksChanged() { + val listener = TestListener() + val executor = TestSyncExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Model an opening task + val firstOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(firstOpeningTransition) + callOnTransitionFinished() + // Assert that the task is reported visible + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + assertVisibleTasks(listener, listOf(1)) + + // Trigger task vanished + val removedTaskInfo = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN) + transitionObserver.onTaskVanished(removedTaskInfo) + + // Assert that the visible list is now empty + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2) + assertVisibleTasks(listener, listOf()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING) + fun alwaysOnTop_taskIsTopMostVisible() { + val listener = TestListener() + val executor = TestSyncExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Model an opening PIP task + val pipOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_PINNED), + ) + ) + + callOnTransitionReady(pipOpeningTransition) + callOnTransitionFinished() + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1) + assertVisibleTasks(listener, listOf(1)) + + // Model an opening fullscreen task + val firstOpeningTransition = + createTransitionInfo(TRANSIT_OPEN, + listOf( + createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN), + ) + ) + + callOnTransitionReady(firstOpeningTransition) + callOnTransitionFinished() + assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2) + assertVisibleTasks(listener, listOf(1, 2)) + } + + class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { + // Only used if FLAG_ENABLE_SHELL_TOP_TASK_TRACKING is disabled + var taskInfoOnTaskMovedToFront = RunningTaskInfo() + var taskInfoOnTaskChanged = mutableListOf<RunningTaskInfo>() + // Only used if FLAG_ENABLE_SHELL_TOP_TASK_TRACKING is enabled + var visibleTasks = mutableListOf<TaskInfo>() + var visibleTasksUpdatedCount = 0 + + override fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) { taskInfoOnTaskMovedToFront = taskInfo } - override fun onTaskChangedThroughTransition(taskInfo: ActivityManager.RunningTaskInfo) { + override fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) { taskInfoOnTaskChanged += taskInfo } + + override fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) { + this.visibleTasks.clear() + this.visibleTasks.addAll(visibleTasks) + visibleTasksUpdatedCount++ + } } /** Simulate calling the onTransitionReady() method */ @@ -412,27 +676,64 @@ class TaskStackTransitionObserverTest { transitionObserver.onTransitionMerged(merged, playing) } + /** + * Asserts that the listener has the given expected task ids (in order). + */ + private fun assertVisibleTasks( + listener: TestListener, + expectedVisibleTaskIds: List<Int> + ) { + assertThat(listener.visibleTasks.size).isEqualTo(expectedVisibleTaskIds.size) + expectedVisibleTaskIds.forEachIndexed { index, taskId -> + assertThat(listener.visibleTasks[index].taskId).isEqualTo(taskId) + } + } + companion object { - fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo { - val taskInfo = ActivityManager.RunningTaskInfo() + fun createTaskInfo(taskId: Int, windowingMode: Int): RunningTaskInfo { + val taskInfo = RunningTaskInfo() + taskInfo.baseIntent = Intent().setComponent( + ComponentName(javaClass.packageName, "Test")) taskInfo.taskId = taskId taskInfo.configuration.windowConfiguration.windowingMode = windowingMode - + if (windowingMode == WINDOWING_MODE_PINNED) { + taskInfo.configuration.windowConfiguration.isAlwaysOnTop = true + } return taskInfo } fun createChange( mode: Int, - taskInfo: ActivityManager.RunningTaskInfo + taskInfo: RunningTaskInfo, + flags: Int = 0, ): TransitionInfo.Change { val change = TransitionInfo.Change( WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)), Mockito.mock(SurfaceControl::class.java) ) + change.flags = flags change.mode = mode change.taskInfo = taskInfo return change } + + fun createChange( + mode: Int, + taskId: Int, + windowingMode: Int, + flags: Int = 0, + ): TransitionInfo.Change { + return createChange(mode, createTaskInfo(taskId, windowingMode), flags) + } + + fun createTransitionInfo( + transitionType: Int, + changes: List<TransitionInfo.Change> + ): TransitionInfo { + return TransitionInfoBuilder(transitionType, 0) + .apply { changes.forEach { c -> this@apply.addChange(c) } } + .build() + } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index c36b88e34835..71af97e5add3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -43,6 +43,7 @@ import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import com.android.wm.shell.TestSyncExecutor; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; @@ -475,27 +476,6 @@ public class UnfoldTransitionHandlerTest { } } - private static class TestSyncExecutor implements ShellExecutor { - @Override - public void execute(Runnable runnable) { - runnable.run(); - } - - @Override - public void executeDelayed(Runnable runnable, long delayMillis) { - runnable.run(); - } - - @Override - public void removeCallbacks(Runnable runnable) { - } - - @Override - public boolean hasCallback(Runnable runnable) { - return false; - } - } - private TransitionInfo createUnfoldTransitionInfo() { TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0); TransitionInfo.Change change = new TransitionInfo.Change(null, mock(SurfaceControl.class)); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index fcb7efc35c94..e2db2c9e3827 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -355,6 +355,7 @@ cc_defaults { "jni/AnimatedImageDrawable.cpp", "jni/Bitmap.cpp", "jni/BitmapRegionDecoder.cpp", + "jni/RuntimeXfermode.cpp", "jni/BufferUtils.cpp", "jni/HardwareBufferHelpers.cpp", "jni/BitmapFactory.cpp", diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index 15b2bac50c79..56de56805be4 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -28,6 +28,7 @@ extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*); +extern int register_android_graphics_RuntimeXfermode(JNIEnv*); extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); @@ -107,6 +108,7 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); REG_JNI(register_android_graphics_Bitmap), REG_JNI(register_android_graphics_BitmapFactory), REG_JNI(register_android_graphics_BitmapRegionDecoder), + REG_JNI(register_android_graphics_RuntimeXfermode), REG_JNI(register_android_graphics_ByteBufferStreamAdaptor), REG_JNI(register_android_graphics_Camera), REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor), diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index da237928e5e1..a7d855d7e8ca 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -906,6 +906,13 @@ namespace PaintGlue { paint->setBlendMode(mode); } + static void setRuntimeXfermode(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, + jlong xfermodeHandle) { + Paint* paint = reinterpret_cast<Paint*>(paintHandle); + SkBlender* blender = reinterpret_cast<SkBlender*>(xfermodeHandle); + paint->setBlender(sk_ref_sp(blender)); + } + static jlong setPathEffect(CRITICAL_JNI_PARAMS_COMMA jlong objHandle, jlong effectHandle) { Paint* obj = reinterpret_cast<Paint*>(objHandle); SkPathEffect* effect = reinterpret_cast<SkPathEffect*>(effectHandle); @@ -1233,6 +1240,7 @@ static const JNINativeMethod methods[] = { {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader}, {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter}, {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode}, + {"nSetXfermode", "(JJ)V", (void*)PaintGlue::setRuntimeXfermode}, {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect}, {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter}, {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface}, diff --git a/libs/hwui/jni/RuntimeXfermode.cpp b/libs/hwui/jni/RuntimeXfermode.cpp new file mode 100644 index 000000000000..c1c8964bf5eb --- /dev/null +++ b/libs/hwui/jni/RuntimeXfermode.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "GraphicsJNI.h" +#include "RuntimeEffectUtils.h" +#include "SkBlender.h" + +using namespace android::uirenderer; + +static void SkRuntimeEffectBuilder_delete(SkRuntimeEffectBuilder* builder) { + delete builder; +} + +static jlong RuntimeXfermode_getNativeFinalizer(JNIEnv*, jobject) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeEffectBuilder_delete)); +} + +static jlong RuntimeXfermode_createBuilder(JNIEnv* env, jobject, jstring sksl) { + ScopedUtfChars strSksl(env, sksl); + auto result = + SkRuntimeEffect::MakeForBlender(SkString(strSksl.c_str()), SkRuntimeEffect::Options{}); + if (result.effect.get() == nullptr) { + doThrowIAE(env, result.errorText.c_str()); + return 0; + } + return reinterpret_cast<jlong>(new SkRuntimeEffectBuilder(std::move(result.effect))); +} + +static jlong RuntimeXfermode_create(JNIEnv* env, jobject, jlong builderPtr) { + auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr); + sk_sp<SkBlender> blender = builder->makeBlender(); + if (!blender) { + doThrowIAE(env); + } + return reinterpret_cast<jlong>(blender.release()); +} + +static void RuntimeXfermode_updateFloatArrayUniforms(JNIEnv* env, jobject, jlong builderPtr, + jstring uniformName, jfloatArray uniforms, + jboolean isColor) { + auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr); + ScopedUtfChars name(env, uniformName); + AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess); + UpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(), isColor); +} + +static void RuntimeXfermode_updateFloatUniforms(JNIEnv* env, jobject, jlong builderPtr, + jstring uniformName, jfloat value1, jfloat value2, + jfloat value3, jfloat value4, jint count) { + auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr); + ScopedUtfChars name(env, uniformName); + const float values[4] = {value1, value2, value3, value4}; + UpdateFloatUniforms(env, builder, name.c_str(), values, count, false); +} + +static void RuntimeXfermode_updateIntArrayUniforms(JNIEnv* env, jobject, jlong builderPtr, + jstring uniformName, jintArray uniforms) { + auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr); + ScopedUtfChars name(env, uniformName); + AutoJavaIntArray autoValues(env, uniforms, 0); + UpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length()); +} + +static void RuntimeXfermode_updateIntUniforms(JNIEnv* env, jobject, jlong builderPtr, + jstring uniformName, jint value1, jint value2, + jint value3, jint value4, jint count) { + auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr); + ScopedUtfChars name(env, uniformName); + const int values[4] = {value1, value2, value3, value4}; + UpdateIntUniforms(env, builder, name.c_str(), values, count); +} + +static void RuntimeXfermode_updateChild(JNIEnv* env, jobject, jlong builderPtr, jstring childName, + jlong childPtr) { + auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr); + ScopedUtfChars name(env, childName); + auto* child = reinterpret_cast<SkFlattenable*>(childPtr); + if (child) { + UpdateChild(env, builder, name.c_str(), child); + } +} + +static const JNINativeMethod gRuntimeXfermodeMethods[] = { + {"nativeGetFinalizer", "()J", (void*)RuntimeXfermode_getNativeFinalizer}, + {"nativeCreateBlenderBuilder", "(Ljava/lang/String;)J", + (void*)RuntimeXfermode_createBuilder}, + {"nativeCreateNativeInstance", "(J)J", (void*)RuntimeXfermode_create}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", + (void*)RuntimeXfermode_updateFloatArrayUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V", + (void*)RuntimeXfermode_updateFloatUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V", + (void*)RuntimeXfermode_updateIntArrayUniforms}, + {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V", + (void*)RuntimeXfermode_updateIntUniforms}, + {"nativeUpdateChild", "(JLjava/lang/String;J)V", (void*)RuntimeXfermode_updateChild}, +}; + +int register_android_graphics_RuntimeXfermode(JNIEnv* env) { + android::RegisterMethodsOrDie(env, "android/graphics/RuntimeXfermode", gRuntimeXfermodeMethods, + NELEM(gRuntimeXfermodeMethods)); + + return 0; +} diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java index 3cd5f5266ef2..da50f2cd86c4 100644 --- a/media/java/android/media/AudioPlaybackConfiguration.java +++ b/media/java/android/media/AudioPlaybackConfiguration.java @@ -19,6 +19,7 @@ package android.media; import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL; import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE; import static android.media.audio.Flags.FLAG_MUTED_BY_PORT_VOLUME_API; +import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -39,6 +40,8 @@ import com.android.internal.annotations.GuardedBy; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** @@ -461,8 +464,12 @@ public final class AudioPlaybackConfiguration implements Parcelable { /** * Returns information about the {@link AudioDeviceInfo} used for this playback. - * @return the audio playback device or null if the device is not available at the time of query + * @return the audio playback device or null if the device is not available at the time of + * query. + * @deprecated this information was never populated */ + @Deprecated + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) public @Nullable AudioDeviceInfo getAudioDeviceInfo() { final int deviceId; synchronized (mUpdateablePropLock) { @@ -476,6 +483,23 @@ public final class AudioPlaybackConfiguration implements Parcelable { /** * @hide + * Returns information about the List of {@link AudioDeviceInfo} used for this playback. + * @return the audio playback devices + */ + @SystemApi + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public @NonNull List<AudioDeviceInfo> getAudioDeviceInfos() { + List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>(); + AudioDeviceInfo audioDeviceInfo = getAudioDeviceInfo(); + if (audioDeviceInfo != null) { + audioDeviceInfos.add(audioDeviceInfo); + } + return audioDeviceInfos; + } + + /** + * @hide * Return the audio session ID associated with this player. * See {@link AudioManager#generateAudioSessionId()}. * @return an audio session ID diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 80e57193d0dc..939494152116 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -20,8 +20,10 @@ import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAUL import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE; +import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; @@ -1920,6 +1922,23 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, } /** + * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this + * AudioRecord. + * Note: The query is only valid if the AudioRecord is currently playing. If it is not, + * <code>getRoutedDevices()</code> will return an empty list. + */ + @Override + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) + public @NonNull List<AudioDeviceInfo> getRoutedDevices() { + List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>(); + AudioDeviceInfo audioDeviceInfo = getRoutedDevice(); + if (audioDeviceInfo != null) { + audioDeviceInfos.add(audioDeviceInfo); + } + return audioDeviceInfos; + } + + /** * Must match the native definition in frameworks/av/service/audioflinger/Audioflinger.h. */ private static final long MAX_SHARED_AUDIO_HISTORY_MS = 5000; diff --git a/media/java/android/media/AudioRouting.java b/media/java/android/media/AudioRouting.java index 26fa631ac6ac..22aa9a09d560 100644 --- a/media/java/android/media/AudioRouting.java +++ b/media/java/android/media/AudioRouting.java @@ -16,9 +16,16 @@ package android.media; +import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.os.Handler; import android.os.Looper; +import java.util.ArrayList; +import java.util.List; + /** * AudioRouting defines an interface for controlling routing and routing notifications in * AudioTrack and AudioRecord objects. @@ -49,6 +56,22 @@ public interface AudioRouting { public AudioDeviceInfo getRoutedDevice(); /** + * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this + * AudioTrack/AudioRecord. + * Note: The query is only valid if the AudioTrack/AudioRecord is currently playing. + * If it is not, <code>getRoutedDevices()</code> will return an empty List. + */ + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) + default @NonNull List<AudioDeviceInfo> getRoutedDevices() { + List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>(); + AudioDeviceInfo audioDeviceInfo = getRoutedDevice(); + if (audioDeviceInfo != null) { + audioDeviceInfos.add(audioDeviceInfo); + } + return new ArrayList<AudioDeviceInfo>(); + } + + /** * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing * changes on this AudioTrack/AudioRecord. * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 03cd53580b1b..93a183188793 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -17,8 +17,10 @@ package android.media; import static android.media.AudioManager.AUDIO_SESSION_ID_GENERATE; +import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; @@ -54,7 +56,9 @@ import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.NioUtils; +import java.util.ArrayList; import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; @@ -3783,6 +3787,8 @@ public class AudioTrack extends PlayerBase * Returns an {@link AudioDeviceInfo} identifying the current routing of this AudioTrack. * Note: The query is only valid if the AudioTrack is currently playing. If it is not, * <code>getRoutedDevice()</code> will return null. + * Audio may play on multiple devices simultaneously (e.g. an alarm playing on headphones and + * speaker on a phone), so prefer using {@link #getRoutedDevices}. */ @Override public AudioDeviceInfo getRoutedDevice() { @@ -3793,6 +3799,23 @@ public class AudioTrack extends PlayerBase return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS); } + /** + * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this + * AudioTrack. + * Note: The query is only valid if the AudioTrack is currently playing. If it is not, + * <code>getRoutedDevices()</code> will return an empty list. + */ + @Override + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) + public @NonNull List<AudioDeviceInfo> getRoutedDevices() { + List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>(); + AudioDeviceInfo audioDeviceInfo = getRoutedDevice(); + if (audioDeviceInfo != null) { + audioDeviceInfos.add(audioDeviceInfo); + } + return audioDeviceInfos; + } + private void tryToDisableNativeRoutingCallback() { synchronized (mRoutingChangeListeners) { if (mEnableSelfRoutingMonitor) { diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 3f9126aa9456..1ecba31ce07f 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -16,10 +16,9 @@ package android.media; +import static android.media.tv.flags.Flags.FLAG_MEDIACAS_UPDATE_CLIENT_PROFILE_PRIORITY; import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN; -import static com.android.media.flags.Flags.FLAG_UPDATE_CLIENT_PROFILE_PRIORITY; - import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -996,7 +995,7 @@ public final class MediaCas implements AutoCloseable { * @param niceValue the nice value. * @hide */ - @FlaggedApi(FLAG_UPDATE_CLIENT_PROFILE_PRIORITY) + @FlaggedApi(FLAG_MEDIACAS_UPDATE_CLIENT_PROFILE_PRIORITY) @SystemApi @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) public boolean updateResourcePriority(int priority, int niceValue) { diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index a0f8ae5defeb..158bc7fcd482 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -18,7 +18,9 @@ package android.media; import static android.Manifest.permission.BIND_IMS_SERVICE; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -84,6 +86,7 @@ import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.URL; import java.nio.ByteOrder; +import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.HashMap; @@ -1542,6 +1545,8 @@ public class MediaPlayer extends PlayerBase * Note: The query is only valid if the MediaPlayer is currently playing. * If the player is not playing, the returned device can be null or correspond to previously * selected device when the player was last active. + * Audio may play on multiple devices simultaneously (e.g. an alarm playing on headphones and + * speaker on a phone), so prefer using {@link #getRoutedDevices}. */ @Override public AudioDeviceInfo getRoutedDevice() { @@ -1552,6 +1557,23 @@ public class MediaPlayer extends PlayerBase return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS); } + /** + * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this + * MediaPlayer. + * Note: The query is only valid if the MediaPlayer is currently playing. + * If the player is not playing, the returned devices can be empty or correspond to previously + * selected devices when the player was last active. + */ + @Override + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) + public @NonNull List<AudioDeviceInfo> getRoutedDevices() { + List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>(); + AudioDeviceInfo audioDeviceInfo = getRoutedDevice(); + if (audioDeviceInfo != null) { + audioDeviceInfos.add(audioDeviceInfo); + } + return audioDeviceInfos; + } /** * Sends device list change notification to all listeners. diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index 2d17bf500f12..f75bcf3c437d 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -16,7 +16,10 @@ package android.media; +import static android.media.audio.Flags.FLAG_ROUTED_DEVICE_IDS; + import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; @@ -1695,6 +1698,24 @@ public class MediaRecorder implements AudioRouting, return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_INPUTS); } + /** + * Returns a List of {@link AudioDeviceInfo} identifying the current routing of this + * MediaRecorder. + * Note: The query is only valid if the MediaRecorder is currently recording. + * If the recorder is not recording, the returned devices can be empty or correspond to + * previously selected devices when the recorder was last active. + */ + @Override + @FlaggedApi(FLAG_ROUTED_DEVICE_IDS) + public @NonNull List<AudioDeviceInfo> getRoutedDevices() { + List<AudioDeviceInfo> audioDeviceInfos = new ArrayList<AudioDeviceInfo>(); + AudioDeviceInfo audioDeviceInfo = getRoutedDevice(); + if (audioDeviceInfo != null) { + audioDeviceInfos.add(audioDeviceInfo); + } + return audioDeviceInfos; + } + /* * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler. */ diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 7895eb27b372..5b1ea8b81c80 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -78,13 +78,6 @@ flag { } flag { - name: "update_client_profile_priority" - namespace: "media_solutions" - description : "Feature flag to add updateResourcePriority api to MediaCas" - bug: "300565729" -} - -flag { name: "enable_built_in_speaker_route_suitability_statuses" is_exported: true namespace: "media_solutions" diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index dd2a534676d8..ff0279fef99e 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -240,6 +240,38 @@ public class TvAdView extends ViewGroup { } } + /** + * Controls whether the TvAdView's surface is placed on top of other regular surface views in + * the window (but still behind the window itself). + * + * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. + * + * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false} + * otherwise. + */ + @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW) + public void setZOrderMediaOverlay(boolean isMediaOverlay) { + if (mSurfaceView != null) { + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.setZOrderMediaOverlay(isMediaOverlay); + } + } + + /** + * Controls whether the TvAdView's surface is placed on top of its window. + * + * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + * + * @param onTop {@code true} to be on top of its window, {@code false} otherwise. + */ + @FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW) + public void setZOrderOnTop(boolean onTop) { + if (mSurfaceView != null) { + mSurfaceView.setZOrderMediaOverlay(false); + mSurfaceView.setZOrderOnTop(onTop); + } + } + private void resetSurfaceView() { if (mSurfaceView != null) { mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); diff --git a/media/java/android/media/tv/extension/oad/IOadUpdateInterface.aidl b/media/java/android/media/tv/extension/oad/IOadUpdateInterface.aidl new file mode 100644 index 000000000000..2a2e71a53993 --- /dev/null +++ b/media/java/android/media/tv/extension/oad/IOadUpdateInterface.aidl @@ -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 android.media.tv.extension.oad; + +/** + * @hide + */ +interface IOadUpdateInterface { + // Enable or disable the OAD function. + void setOadStatus(boolean enable); + // Get status of OAD function. + boolean getOadStatus(); + // Start OAD scan of all frequency in the program list. + void startScan(); + // Stop OAD scan of all frequency in the program list. + void stopScan(); + // Start OAD detect for the current channel. + void startDetect(); + // Stop OAD detect for the current channel. + void stopDetect(); + // Start OAD download after it has been detected or scanned. + void startDownload(); + // Stop OAD download. + void stopDownload(); + // Retrieves current OAD software version. + int getSoftwareVersion(); +} diff --git a/core/java/android/text/ClientFlags.java b/media/java/android/media/tv/extension/rating/IDownloadableRatingTableMonitor.aidl index ca887646b3aa..bf1a385f05ae 100644 --- a/core/java/android/text/ClientFlags.java +++ b/media/java/android/media/tv/extension/rating/IDownloadableRatingTableMonitor.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,14 @@ * limitations under the License. */ -package android.text; +package android.media.tv.extension.rating; + +import android.os.Bundle; /** - * An aconfig feature flags that can be accessible from application process without - * ContentProvider IPCs. - * - * When you add new flags, you have to add flag string to {@link TextFlags#TEXT_ACONFIGS_FLAGS}. - * - * TODO(nona): Remove this class. * @hide */ -public class ClientFlags { +interface IDownloadableRatingTableMonitor { + // Get RRT rating info on downloadable rating data + Bundle[] getTable(); } diff --git a/media/java/android/media/tv/extension/rating/IPmtRatingInterface.aidl b/media/java/android/media/tv/extension/rating/IPmtRatingInterface.aidl new file mode 100644 index 000000000000..06cac3d0d411 --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IPmtRatingInterface.aidl @@ -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 android.media.tv.extension.rating; + +import android.media.tv.extension.rating.IPmtRatingListener; + +/** + * @hide + */ +interface IPmtRatingInterface { + // Get Pmt rating information. + String getPmtRating(String sessionToken); + // Register a listener for pmt rating updates. + void addPmtRatingListener(String clientToken, in IPmtRatingListener listener); + // Remove the previously added IPmtRatingListener. + void removePmtRatingListener(in IPmtRatingListener listener); +} diff --git a/media/java/android/media/tv/extension/rating/IPmtRatingListener.aidl b/media/java/android/media/tv/extension/rating/IPmtRatingListener.aidl new file mode 100644 index 000000000000..d88ae9425f8c --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IPmtRatingListener.aidl @@ -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 android.media.tv.extension.rating; + +/** + * @hide + */ +oneway interface IPmtRatingListener { + void onPmtRatingChanged(String sessionToken, String newTvContentRating); +} diff --git a/media/java/android/media/tv/extension/rating/IProgramRatingInfo.aidl b/media/java/android/media/tv/extension/rating/IProgramRatingInfo.aidl new file mode 100644 index 000000000000..a490491d7acc --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IProgramRatingInfo.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.extension.rating; + +import android.media.tv.extension.rating.IProgramRatingInfoListener; +import android.os.Bundle; + +/** + * @hide + */ +interface IProgramRatingInfo { + // Register a listener to receive notifications when ProgramRatingInfo is updated. + void addProgramRatingInfoListener(String clientToken, in IProgramRatingInfoListener listener); + // Remove a listener for ProgramRatingInfo update notifications. + void removeProgramRatingInfoListener(in IProgramRatingInfoListener listener); + // Get ProgramRatingInfo that may only be obtained when viewing. + Bundle getProgramRatingInfo(String sessionToken); +} diff --git a/media/java/android/media/tv/extension/rating/IProgramRatingInfoListener.aidl b/media/java/android/media/tv/extension/rating/IProgramRatingInfoListener.aidl new file mode 100644 index 000000000000..6777cd3035d8 --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IProgramRatingInfoListener.aidl @@ -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 android.media.tv.extension.rating; + +import android.os.Bundle; + +/** + * @hide + */ +interface IProgramRatingInfoListener { + void onProgramInfoChanged(String sessionToken,in Bundle changedProgramInfo); +} diff --git a/media/java/android/media/tv/extension/rating/IRatingInterface.aidl b/media/java/android/media/tv/extension/rating/IRatingInterface.aidl new file mode 100644 index 000000000000..d68fe763ef28 --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IRatingInterface.aidl @@ -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 android.media.tv.extension.rating; + +import android.os.Bundle; + +/** + * @hide + */ +interface IRatingInterface { + // Get RRT rating information + Bundle getRRTRatingInfo(); + // Set RRT rating information when user select + boolean setRRTRatingInfo(in Bundle param); + // Reset RRT5 to clear information + boolean setResetRrt5(); +} diff --git a/media/java/android/media/tv/extension/rating/IVbiRatingInterface.aidl b/media/java/android/media/tv/extension/rating/IVbiRatingInterface.aidl new file mode 100644 index 000000000000..bad40676e8aa --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IVbiRatingInterface.aidl @@ -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 android.media.tv.extension.rating; + +import android.media.tv.extension.rating.IVbiRatingListener; + +/** + * @hide + */ +interface IVbiRatingInterface { + // Get Vbi rating. + String getVbiRating(String sessionToken); + // Register a listener for Vbi rating updates. + void addVbiRatingListener(String clientToken, in IVbiRatingListener listener); + // Remove the previously added VbiRatingListener. + void removeVbiRatingListener(in IVbiRatingListener listener); +} diff --git a/media/java/android/media/tv/extension/rating/IVbiRatingListener.aidl b/media/java/android/media/tv/extension/rating/IVbiRatingListener.aidl new file mode 100644 index 000000000000..36d523f97613 --- /dev/null +++ b/media/java/android/media/tv/extension/rating/IVbiRatingListener.aidl @@ -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 android.media.tv.extension.rating; + +/** + * @hide + */ +oneway interface IVbiRatingListener { + void onVbiRatingChanged(String sessionToken, String newTvContentRating); +} diff --git a/media/java/android/media/tv/extension/time/IBroadcastTime.aidl b/media/java/android/media/tv/extension/time/IBroadcastTime.aidl new file mode 100644 index 000000000000..123d00f9faf4 --- /dev/null +++ b/media/java/android/media/tv/extension/time/IBroadcastTime.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv.extension.time; + +import android.os.Bundle; + +/** + * @hide + */ +interface IBroadcastTime { + long getUtcTime(); + long getLocalTime(); + Bundle getTimeZoneInfo(); + long getUtcTimePerStream(String SessionToken); + long getLocalTimePerStream(String SessionToken); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig index 64416525441b..4b832aee49c5 100644 --- a/media/java/android/media/tv/flags/media_tv.aconfig +++ b/media/java/android/media/tv/flags/media_tv.aconfig @@ -2,6 +2,22 @@ package: "android.media.tv.flags" container: "system" flag { + name: "enable_le_audio_broadcast_ui" + is_exported: true + namespace: "media_tv" + description: "Enable Broadcast UI for LE Audio on TV." + bug: "378732734" +} + +flag { + name: "enable_le_audio_unicast_ui" + is_exported: true + namespace: "media_tv" + description: "Enable Unicast UI for LE Audio on TV." + bug: "378732734" +} + +flag { name: "broadcast_visibility_types" is_exported: true namespace: "media_tv" @@ -77,11 +93,19 @@ flag { name: "set_resource_holder_retain" is_exported: true namespace: "media_tv" - description : "Feature flag to add setResourceHolderRetain api to MediaCas and Tuner JAVA." + description: "Feature flag to add setResourceHolderRetain api to MediaCas and Tuner JAVA." bug: "372973197" } flag { + name: "mediacas_update_client_profile_priority" + is_exported: true + namespace: "media_tv" + description: "Feature flag to add updateResourcePriority api to MediaCas" + bug: "372971241" +} + +flag { name: "apply_picture_profiles" is_exported: true namespace: "media_tv" diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 635572d12cc5..9e9699ff6bc6 100644 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -271,6 +271,38 @@ public class TvInteractiveAppView extends ViewGroup { } } + /** + * Controls whether the TvInteractiveAppView's surface is placed on top of other regular surface + * views in the window (but still behind the window itself). + * + * <p>Calling this overrides any previous call to {@link #setZOrderOnTop}. + * + * @param isMediaOverlay {@code true} to be on top of another regular surface, {@code false} + * otherwise. + */ + @FlaggedApi(Flags.FLAG_TIAF_V_APIS) + public void setZOrderMediaOverlay(boolean isMediaOverlay) { + if (mSurfaceView != null) { + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.setZOrderMediaOverlay(isMediaOverlay); + } + } + + /** + * Controls whether the TvInterActiveAppView's surface is placed on top of its window. + * + * <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}. + * + * @param onTop {@code true} to be on top of its window, {@code false} otherwise. + */ + @FlaggedApi(Flags.FLAG_TIAF_V_APIS) + public void setZOrderOnTop(boolean onTop) { + if (mSurfaceView != null) { + mSurfaceView.setZOrderMediaOverlay(false); + mSurfaceView.setZOrderOnTop(onTop); + } + } + private void resetSurfaceView() { if (mSurfaceView != null) { mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java index ac85ab7f6a6e..88c1c434cc0d 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java @@ -257,6 +257,16 @@ public class CameraBinderTest extends AndroidTestCase { public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) { // TODO Auto-generated method stub } + + /* + * (non-Javadoc) + * @see android.hardware.camera2.ICameraDeviceCallbacks#onClientSharedAccessPriorityChanged + */ + @Override + public void onClientSharedAccessPriorityChanged(boolean primaryClient) { + // TODO Auto-generated method stub + } + } @SmallTest @@ -276,7 +286,7 @@ public class CameraBinderTest extends AndroidTestCase { 0 /*oomScoreOffset*/, getContext().getApplicationInfo().targetSdkVersion, ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, - DEVICE_POLICY_DEFAULT); + DEVICE_POLICY_DEFAULT, false/*sharedMode*/); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); @@ -320,6 +330,13 @@ public class CameraBinderTest extends AndroidTestCase { Log.v(TAG, String.format("Camera " + cameraId + " torch strength level changed to " + torchStrength )); } + @Override + public void onCameraOpenedInSharedMode(String cameraId, String clientPackageName, + int deviceId, boolean primaryClient) { + Log.v(TAG, "Camera " + cameraId + " is opened in shared mode by " + + "client package " + clientPackageName + " as primary client=" + + primaryClient); + } } /** diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java index 35ad924cee74..3758c515c1a5 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java @@ -175,6 +175,15 @@ public class CameraDeviceBinderTest extends AndroidTestCase { public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) { // TODO Auto-generated method stub } + + /** + * (non-Javadoc) + * @see android.hardware.camera2.ICameraDeviceCallbacks#onClientSharedAccessPriorityChanged + */ + @Override + public void onClientSharedAccessPriorityChanged(boolean primaryClient) { + // TODO Auto-generated method stub + } } class IsMetadataNotEmpty implements ArgumentMatcher<CameraMetadataNative> { @@ -250,7 +259,8 @@ public class CameraDeviceBinderTest extends AndroidTestCase { mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId, /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion, - ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, DEVICE_POLICY_DEFAULT); + ICameraService.ROTATION_OVERRIDE_NONE, clientAttribution, DEVICE_POLICY_DEFAULT, + /*sharedMode*/false); assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser); mHandlerThread = new HandlerThread(TAG); mHandlerThread.start(); diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index a0460572abfc..2d1fbf9e7f66 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -361,6 +361,8 @@ LIBANDROID { APerformanceHint_setThreads; # introduced=UpsideDownCake APerformanceHint_setPreferPowerEfficiency; # introduced=VanillaIceCream APerformanceHint_reportActualWorkDuration2; # introduced=VanillaIceCream + APerformanceHint_notifyWorkloadIncrease; # introduced=36 + APerformanceHint_notifyWorkloadReset; # introduced=36 AWorkDuration_create; # introduced=VanillaIceCream AWorkDuration_release; # introduced=VanillaIceCream AWorkDuration_setWorkPeriodStartTimestampNanos; # introduced=VanillaIceCream @@ -379,6 +381,8 @@ LIBANDROID_PLATFORM { APerformanceHint_getThreadIds; APerformanceHint_createSessionInternal; APerformanceHint_setUseFMQForTesting; + APerformanceHint_getRateLimiterPropertiesForTesting; + APerformanceHint_setUseNewLoadHintBehaviorForTesting; extern "C++" { ASurfaceControl_registerSurfaceStatsListener*; ASurfaceControl_unregisterSurfaceStatsListener*; diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 15f77cebf3ba..e2fa94dd39bb 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -33,12 +33,14 @@ #include <android/performance_hint.h> #include <android/trace.h> #include <android_os.h> +#include <cutils/trace.h> #include <fmq/AidlMessageQueue.h> #include <inttypes.h> #include <performance_hint_private.h> #include <utils/SystemClock.h> #include <chrono> +#include <format> #include <future> #include <set> #include <utility> @@ -63,6 +65,22 @@ struct APerformanceHintSession; constexpr int64_t SEND_HINT_TIMEOUT = std::chrono::nanoseconds(100ms).count(); struct AWorkDuration : public hal::WorkDuration {}; +// A pair of values that determine the behavior of the +// load hint rate limiter, to only allow "X hints every Y seconds" +constexpr double kLoadHintInterval = std::chrono::nanoseconds(2s).count(); +constexpr double kMaxLoadHintsPerInterval = 20; +constexpr double kReplenishRate = kMaxLoadHintsPerInterval / kLoadHintInterval; +bool kForceNewHintBehavior = false; + +template <class T> +constexpr int32_t enum_size() { + return static_cast<int32_t>(*(ndk::enum_range<T>().end() - 1)) + 1; +} + +bool useNewLoadHintBehavior() { + return android::os::adpf_use_load_hints() || kForceNewHintBehavior; +} + // Shared lock for the whole PerformanceHintManager and sessions static std::mutex sHintMutex = std::mutex{}; class FMQWrapper { @@ -76,7 +94,8 @@ public: hal::WorkDuration* durations, size_t count) REQUIRES(sHintMutex); bool updateTargetWorkDuration(std::optional<hal::SessionConfig>& config, int64_t targetDurationNanos) REQUIRES(sHintMutex); - bool sendHint(std::optional<hal::SessionConfig>& config, SessionHint hint) REQUIRES(sHintMutex); + bool sendHints(std::optional<hal::SessionConfig>& config, std::vector<hal::SessionHint>& hint, + int64_t now) REQUIRES(sHintMutex); bool setMode(std::optional<hal::SessionConfig>& config, hal::SessionMode, bool enabled) REQUIRES(sHintMutex); void setToken(ndk::SpAIBinder& token); @@ -86,10 +105,11 @@ public: private: template <HalChannelMessageContents::Tag T, bool urgent = false, class C = HalChannelMessageContents::_at<T>> - bool sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count = 1) - REQUIRES(sHintMutex); + bool sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count = 1, + int64_t now = ::android::uptimeNanos()) REQUIRES(sHintMutex); template <HalChannelMessageContents::Tag T, class C = HalChannelMessageContents::_at<T>> - void writeBuffer(C* message, hal::SessionConfig& config, size_t count) REQUIRES(sHintMutex); + void writeBuffer(C* message, hal::SessionConfig& config, size_t count, int64_t now) + REQUIRES(sHintMutex); bool isActiveLocked() REQUIRES(sHintMutex); bool updatePersistentTransaction() REQUIRES(sHintMutex); @@ -120,6 +140,7 @@ public: hal::SessionTag tag = hal::SessionTag::APP); int64_t getPreferredRateNanos() const; FMQWrapper& getFMQWrapper(); + bool canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) REQUIRES(sHintMutex); private: // Necessary to create an empty binder object @@ -138,6 +159,8 @@ private: ndk::SpAIBinder mToken; const int64_t mPreferredRateNanos; FMQWrapper mFMQWrapper; + double mHintBudget = kMaxLoadHintsPerInterval; + int64_t mLastBudgetReplenish = 0; }; struct APerformanceHintSession { @@ -151,7 +174,9 @@ public: int updateTargetWorkDuration(int64_t targetDurationNanos); int reportActualWorkDuration(int64_t actualDurationNanos); - int sendHint(SessionHint hint); + int sendHints(std::vector<hal::SessionHint>& hints, int64_t now, const char* debugName); + int notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName); + int notifyWorkloadReset(bool cpu, bool gpu, const char* debugName); int setThreads(const int32_t* threadIds, size_t size); int getThreadIds(int32_t* const threadIds, size_t* size); int setPreferPowerEfficiency(bool enabled); @@ -173,6 +198,8 @@ private: // Last target hit timestamp int64_t mLastTargetMetTimestamp GUARDED_BY(sHintMutex); // Last hint reported from sendHint indexed by hint value + // This is only used by the old rate limiter impl and is replaced + // with the new rate limiter under a flag std::vector<int64_t> mLastHintSentTimestamp GUARDED_BY(sHintMutex); // Cached samples std::vector<hal::WorkDuration> mActualWorkDurations GUARDED_BY(sHintMutex); @@ -255,6 +282,21 @@ APerformanceHintManager* APerformanceHintManager::create(std::shared_ptr<IHintMa return new APerformanceHintManager(manager, preferredRateNanos); } +bool APerformanceHintManager::canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) { + mHintBudget = + std::max(kMaxLoadHintsPerInterval, + mHintBudget + + static_cast<double>(now - mLastBudgetReplenish) * kReplenishRate); + mLastBudgetReplenish = now; + + // If this youngest timestamp isn't older than the timeout time, we can't send + if (hints.size() > mHintBudget) { + return false; + } + mHintBudget -= hints.size(); + return true; +} + APerformanceHintSession* APerformanceHintManager::createSession( const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos, hal::SessionTag tag) { @@ -292,9 +334,7 @@ FMQWrapper& APerformanceHintManager::getFMQWrapper() { // ===================================== APerformanceHintSession implementation -constexpr int kNumEnums = - ndk::enum_range<hal::SessionHint>().end() - ndk::enum_range<hal::SessionHint>().begin(); - +constexpr int kNumEnums = enum_size<hal::SessionHint>(); APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> hintManager, std::shared_ptr<IHintSession> session, int64_t preferredRateNanos, @@ -361,31 +401,83 @@ int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNano return reportActualWorkDurationInternal(static_cast<AWorkDuration*>(&workDuration)); } -int APerformanceHintSession::sendHint(SessionHint hint) { +int APerformanceHintSession::sendHints(std::vector<hal::SessionHint>& hints, int64_t now, + const char*) { std::scoped_lock lock(sHintMutex); - if (hint < 0 || hint >= static_cast<int32_t>(mLastHintSentTimestamp.size())) { - ALOGE("%s: invalid session hint %d", __FUNCTION__, hint); + if (hints.empty()) { return EINVAL; } - int64_t now = uptimeNanos(); + for (auto&& hint : hints) { + if (static_cast<int32_t>(hint) < 0 || static_cast<int32_t>(hint) >= kNumEnums) { + ALOGE("%s: invalid session hint %d", __FUNCTION__, hint); + return EINVAL; + } + } - // Limit sendHint to a pre-detemined rate for safety - if (now < (mLastHintSentTimestamp[hint] + SEND_HINT_TIMEOUT)) { - return 0; + if (useNewLoadHintBehavior()) { + if (!APerformanceHintManager::getInstance()->canSendLoadHints(hints, now)) { + return EBUSY; + } + } + // keep old rate limiter behavior for legacy flag + else { + for (auto&& hint : hints) { + if (now < (mLastHintSentTimestamp[static_cast<int32_t>(hint)] + SEND_HINT_TIMEOUT)) { + return EBUSY; + } + } } - if (!getFMQ().sendHint(mSessionConfig, hint)) { - ndk::ScopedAStatus ret = mHintSession->sendHint(hint); + if (!getFMQ().sendHints(mSessionConfig, hints, now)) { + for (auto&& hint : hints) { + ndk::ScopedAStatus ret = mHintSession->sendHint(static_cast<int32_t>(hint)); - if (!ret.isOk()) { - ALOGE("%s: HintSession sendHint failed: %s", __FUNCTION__, ret.getMessage()); - return EPIPE; + if (!ret.isOk()) { + ALOGE("%s: HintSession sendHint failed: %s", __FUNCTION__, ret.getMessage()); + return EPIPE; + } + } + } + + if (!useNewLoadHintBehavior()) { + for (auto&& hint : hints) { + mLastHintSentTimestamp[static_cast<int32_t>(hint)] = now; } } - mLastHintSentTimestamp[hint] = now; + + if (ATrace_isEnabled()) { + ATRACE_INSTANT("Sending load hint"); + } + return 0; } +int APerformanceHintSession::notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName) { + std::vector<hal::SessionHint> hints(2); + hints.clear(); + if (cpu) { + hints.push_back(hal::SessionHint::CPU_LOAD_UP); + } + if (gpu) { + hints.push_back(hal::SessionHint::GPU_LOAD_UP); + } + int64_t now = ::android::uptimeNanos(); + return sendHints(hints, now, debugName); +} + +int APerformanceHintSession::notifyWorkloadReset(bool cpu, bool gpu, const char* debugName) { + std::vector<hal::SessionHint> hints(2); + hints.clear(); + if (cpu) { + hints.push_back(hal::SessionHint::CPU_LOAD_RESET); + } + if (gpu) { + hints.push_back(hal::SessionHint::GPU_LOAD_RESET); + } + int64_t now = ::android::uptimeNanos(); + return sendHints(hints, now, debugName); +} + int APerformanceHintSession::setThreads(const int32_t* threadIds, size_t size) { if (size == 0) { ALOGE("%s: the list of thread ids must not be empty.", __FUNCTION__); @@ -565,24 +657,25 @@ void FMQWrapper::stopChannel(IHintManager* manager) { } template <HalChannelMessageContents::Tag T, class C> -void FMQWrapper::writeBuffer(C* message, hal::SessionConfig& config, size_t) { - new (mFmqTransaction.getSlot(0)) hal::ChannelMessage{ - .sessionID = static_cast<int32_t>(config.id), - .timeStampNanos = ::android::uptimeNanos(), - .data = HalChannelMessageContents::make<T, C>(std::move(*message)), - }; +void FMQWrapper::writeBuffer(C* message, hal::SessionConfig& config, size_t count, int64_t now) { + for (size_t i = 0; i < count; ++i) { + new (mFmqTransaction.getSlot(i)) hal::ChannelMessage{ + .sessionID = static_cast<int32_t>(config.id), + .timeStampNanos = now, + .data = HalChannelMessageContents::make<T, C>(std::move(*(message + i))), + }; + } } template <> void FMQWrapper::writeBuffer<HalChannelMessageContents::workDuration>(hal::WorkDuration* messages, hal::SessionConfig& config, - size_t count) { + size_t count, int64_t now) { for (size_t i = 0; i < count; ++i) { hal::WorkDuration& message = messages[i]; new (mFmqTransaction.getSlot(i)) hal::ChannelMessage{ .sessionID = static_cast<int32_t>(config.id), - .timeStampNanos = - (i == count - 1) ? ::android::uptimeNanos() : message.timeStampNanos, + .timeStampNanos = (i == count - 1) ? now : message.timeStampNanos, .data = HalChannelMessageContents::make<HalChannelMessageContents::workDuration, hal::WorkDurationFixedV1>({ .durationNanos = message.cpuDurationNanos, @@ -595,7 +688,8 @@ void FMQWrapper::writeBuffer<HalChannelMessageContents::workDuration>(hal::WorkD } template <HalChannelMessageContents::Tag T, bool urgent, class C> -bool FMQWrapper::sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count) { +bool FMQWrapper::sendMessages(std::optional<hal::SessionConfig>& config, C* message, size_t count, + int64_t now) { if (!isActiveLocked() || !config.has_value() || mCorrupted) { return false; } @@ -609,7 +703,7 @@ bool FMQWrapper::sendMessages(std::optional<hal::SessionConfig>& config, C* mess return false; } } - writeBuffer<T, C>(message, *config, count); + writeBuffer<T, C>(message, *config, count, now); mQueue->commitWrite(count); mEventFlag->wake(mWriteMask); // Re-create the persistent transaction after writing @@ -641,10 +735,9 @@ bool FMQWrapper::updateTargetWorkDuration(std::optional<hal::SessionConfig>& con return sendMessages<HalChannelMessageContents::targetDuration>(config, &targetDurationNanos); } -bool FMQWrapper::sendHint(std::optional<hal::SessionConfig>& config, SessionHint hint) { - return sendMessages<HalChannelMessageContents::hint>(config, - reinterpret_cast<hal::SessionHint*>( - &hint)); +bool FMQWrapper::sendHints(std::optional<hal::SessionConfig>& config, + std::vector<hal::SessionHint>& hints, int64_t now) { + return sendMessages<HalChannelMessageContents::hint>(config, hints.data(), hints.size(), now); } bool FMQWrapper::setMode(std::optional<hal::SessionConfig>& config, hal::SessionMode mode, @@ -758,7 +851,9 @@ void APerformanceHint_closeSession(APerformanceHintSession* session) { int APerformanceHint_sendHint(APerformanceHintSession* session, SessionHint hint) { VALIDATE_PTR(session) - return session->sendHint(hint); + std::vector<hal::SessionHint> hints{static_cast<hal::SessionHint>(hint)}; + int64_t now = ::android::uptimeNanos(); + return session->sendHints(hints, now, "HWUI hint"); } int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds, @@ -791,6 +886,26 @@ int APerformanceHint_reportActualWorkDuration2(APerformanceHintSession* session, return session->reportActualWorkDuration(workDurationPtr); } +int APerformanceHint_notifyWorkloadIncrease(APerformanceHintSession* session, bool cpu, bool gpu, + const char* debugName) { + VALIDATE_PTR(session) + VALIDATE_PTR(debugName) + if (!useNewLoadHintBehavior()) { + return ENOTSUP; + } + return session->notifyWorkloadIncrease(cpu, gpu, debugName); +} + +int APerformanceHint_notifyWorkloadReset(APerformanceHintSession* session, bool cpu, bool gpu, + const char* debugName) { + VALIDATE_PTR(session) + VALIDATE_PTR(debugName) + if (!useNewLoadHintBehavior()) { + return ENOTSUP; + } + return session->notifyWorkloadReset(cpu, gpu, debugName); +} + AWorkDuration* AWorkDuration_create() { return new AWorkDuration(); } @@ -838,3 +953,13 @@ void APerformanceHint_setIHintManagerForTesting(void* iManager) { void APerformanceHint_setUseFMQForTesting(bool enabled) { gForceFMQEnabled = enabled; } + +void APerformanceHint_getRateLimiterPropertiesForTesting(int32_t* maxLoadHintsPerInterval, + int64_t* loadHintInterval) { + *maxLoadHintsPerInterval = kMaxLoadHintsPerInterval; + *loadHintInterval = kLoadHintInterval; +} + +void APerformanceHint_setUseNewLoadHintBehaviorForTesting(bool newBehavior) { + kForceNewHintBehavior = newBehavior; +} diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp index 9de3a6f525e6..f707a0e9b0b2 100644 --- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp +++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp @@ -66,6 +66,18 @@ public: std::optional<hal::ChannelConfig>* _aidl_return), (override)); MOCK_METHOD(ScopedAStatus, closeSessionChannel, (), (override)); + MOCK_METHOD(ScopedAStatus, getCpuHeadroom, + (const ::aidl::android::os::CpuHeadroomParamsInternal& in_params, + std::vector<float>* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, getCpuHeadroomMinIntervalMillis, (int64_t* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, getGpuHeadroom, + (const ::aidl::android::os::GpuHeadroomParamsInternal& in_params, + float* _aidl_return), + (override)); + MOCK_METHOD(ScopedAStatus, getGpuHeadroomMinIntervalMillis, (int64_t* _aidl_return), + (override)); MOCK_METHOD(SpAIBinder, asBinder, (), (override)); MOCK_METHOD(bool, isRemote, (), (override)); }; @@ -90,7 +102,10 @@ class PerformanceHintTest : public Test { public: void SetUp() override { mMockIHintManager = ndk::SharedRefBase::make<NiceMock<MockIHintManager>>(); + APerformanceHint_getRateLimiterPropertiesForTesting(&mMaxLoadHintsPerInterval, + &mLoadHintInterval); APerformanceHint_setIHintManagerForTesting(&mMockIHintManager); + APerformanceHint_setUseNewLoadHintBehaviorForTesting(true); } void TearDown() override { @@ -176,6 +191,9 @@ public: int kMockQueueSize = 20; bool mUsingFMQ = false; + int32_t mMaxLoadHintsPerInterval; + int64_t mLoadHintInterval; + template <HalChannelMessageContents::Tag T, class C = HalChannelMessageContents::_at<T>> void expectToReadFromFmq(C expected) { hal::ChannelMessage readData; @@ -218,7 +236,6 @@ TEST_F(PerformanceHintTest, TestSession) { EXPECT_CALL(*mMockSession, reportActualWorkDuration2(_)).Times(Exactly(1)); result = APerformanceHint_reportActualWorkDuration(session, actualDurationNanos); EXPECT_EQ(0, result); - result = APerformanceHint_updateTargetWorkDuration(session, -1L); EXPECT_EQ(EINVAL, result); result = APerformanceHint_reportActualWorkDuration(session, -1L); @@ -228,18 +245,28 @@ TEST_F(PerformanceHintTest, TestSession) { EXPECT_CALL(*mMockSession, sendHint(Eq(hintId))).Times(Exactly(1)); result = APerformanceHint_sendHint(session, hintId); EXPECT_EQ(0, result); - usleep(110000); // Sleep for longer than the update timeout. - EXPECT_CALL(*mMockSession, sendHint(Eq(hintId))).Times(Exactly(1)); - result = APerformanceHint_sendHint(session, hintId); + EXPECT_CALL(*mMockSession, sendHint(Eq(SessionHint::CPU_LOAD_UP))).Times(Exactly(1)); + result = APerformanceHint_notifyWorkloadIncrease(session, true, false, "Test hint"); EXPECT_EQ(0, result); - // Expect to get rate limited if we try to send faster than the limiter allows - EXPECT_CALL(*mMockSession, sendHint(Eq(hintId))).Times(Exactly(0)); - result = APerformanceHint_sendHint(session, hintId); + EXPECT_CALL(*mMockSession, sendHint(Eq(SessionHint::CPU_LOAD_RESET))).Times(Exactly(1)); + EXPECT_CALL(*mMockSession, sendHint(Eq(SessionHint::GPU_LOAD_RESET))).Times(Exactly(1)); + result = APerformanceHint_notifyWorkloadReset(session, true, true, "Test hint"); EXPECT_EQ(0, result); result = APerformanceHint_sendHint(session, static_cast<SessionHint>(-1)); EXPECT_EQ(EINVAL, result); + Mock::VerifyAndClearExpectations(mMockSession.get()); + for (int i = 0; i < mMaxLoadHintsPerInterval; ++i) { + APerformanceHint_sendHint(session, hintId); + } + + // Expect to get rate limited if we try to send faster than the limiter allows + EXPECT_CALL(*mMockSession, sendHint(_)).Times(Exactly(0)); + result = APerformanceHint_notifyWorkloadIncrease(session, true, true, "Test hint"); + EXPECT_EQ(result, EBUSY); + EXPECT_CALL(*mMockSession, sendHint(_)).Times(Exactly(0)); + result = APerformanceHint_notifyWorkloadReset(session, true, true, "Test hint"); EXPECT_CALL(*mMockSession, close()).Times(Exactly(1)); APerformanceHint_closeSession(session); } diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index c25f77b12116..79a0607bbd1c 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -89,12 +89,12 @@ package android.nfc { method public void onBootFinished(int); method public void onBootStarted(); method public void onCardEmulationActivated(boolean); - method public void onDisable(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onDisableFinished(int); + method public void onDisableRequested(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onDisableStarted(); method public void onEeListenActivated(boolean); - method public void onEnable(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onEnableFinished(int); + method public void onEnableRequested(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onEnableStarted(); method public void onGetOemAppSearchIntent(@NonNull java.util.List<java.lang.String>, @NonNull java.util.function.Consumer<android.content.Intent>); method public void onHceEventReceived(int); @@ -183,6 +183,12 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int); method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void overrideRoutingTable(@NonNull android.app.Activity, int, int); method @FlaggedApi("android.nfc.nfc_override_recover_routing_table") public void recoverRoutingTable(@NonNull android.app.Activity); + method @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setServiceEnabledForCategoryOther(@NonNull android.content.ComponentName, boolean); + field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET = 3; // 0x3 + field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED = 1; // 0x1 + field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE = 2; // 0x2 + field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR = 4; // 0x4 + field @FlaggedApi("android.nfc.nfc_set_service_enabled_for_category_other") public static final int SET_SERVICE_ENABLED_STATUS_OK = 0; // 0x0 } } diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl index 5e2e92d958a4..633d8bfbbb67 100644 --- a/nfc/java/android/nfc/INfcCardEmulation.aidl +++ b/nfc/java/android/nfc/INfcCardEmulation.aidl @@ -47,7 +47,7 @@ interface INfcCardEmulation boolean unsetPreferredService(); boolean supportsAidPrefixRegistration(); ApduServiceInfo getPreferredPaymentService(int userHandle); - boolean setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status); + int setServiceEnabledForCategoryOther(int userHandle, in ComponentName app, boolean status); boolean isDefaultPaymentRegistered(); void overrideRoutingTable(int userHandle, String protocol, String technology, in String pkg); diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index 57ee981caf9c..c677cd68610c 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -27,7 +27,6 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.content.ComponentName; import android.content.Context; @@ -233,8 +232,7 @@ public final class NfcOemExtension { * {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}. * false if NFC cannot be enabled at this time. */ - @SuppressLint("MethodNameTense") - void onEnable(@NonNull Consumer<Boolean> isAllowed); + void onEnableRequested(@NonNull Consumer<Boolean> isAllowed); /** * Method to check if Nfc is allowed to be disabled by OEMs. * @param isAllowed The {@link Consumer} to be completed. If disabling NFC is allowed, @@ -242,7 +240,7 @@ public final class NfcOemExtension { * {@link Boolean#TRUE}, otherwise call with {@link Boolean#FALSE}. * false if NFC cannot be disabled at this time. */ - void onDisable(@NonNull Consumer<Boolean> isAllowed); + void onDisableRequested(@NonNull Consumer<Boolean> isAllowed); /** * Callback to indicate that Nfc starts to boot. @@ -255,7 +253,7 @@ public final class NfcOemExtension { void onEnableStarted(); /** - * Callback to indicate that Nfc starts to enable. + * Callback to indicate that Nfc starts to disable. */ void onDisableStarted(); @@ -799,13 +797,13 @@ public final class NfcOemExtension { public void onEnable(ResultReceiver isAllowed) throws RemoteException { mCallbackMap.forEach((cb, ex) -> handleVoidCallback( - new ReceiverWrapper<>(isAllowed), cb::onEnable, ex)); + new ReceiverWrapper<>(isAllowed), cb::onEnableRequested, ex)); } @Override public void onDisable(ResultReceiver isAllowed) throws RemoteException { mCallbackMap.forEach((cb, ex) -> handleVoidCallback( - new ReceiverWrapper<>(isAllowed), cb::onDisable, ex)); + new ReceiverWrapper<>(isAllowed), cb::onDisableRequested, ex)); } @Override public void onBootStarted() throws RemoteException { diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index eb28c3b9c930..891752475824 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -185,6 +185,65 @@ public final class CardEmulation { @FlaggedApi(Flags.FLAG_NFC_OVERRIDE_RECOVER_ROUTING_TABLE) public static final int PROTOCOL_AND_TECHNOLOGY_ROUTE_UNSET = -1; + /** + * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)} + * succeeded. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER) + public static final int SET_SERVICE_ENABLED_STATUS_OK = 0; + + /** + * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)} + * failed due to the unsupported feature. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER) + public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED = 1; + + /** + * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)} + * failed due to the invalid service. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER) + public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE = 2; + + /** + * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)} + * failed due to the service is already set to the requested status. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER) + public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET = 3; + + /** + * Status code returned when {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)} + * failed due to unknown error. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER) + public static final int SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR = 4; + + /** + * Status code returned by {@link #setServiceEnabledForCategoryOther(ComponentName, boolean)} + * @hide + */ + @IntDef(prefix = "SET_SERVICE_ENABLED_STATUS_", value = { + SET_SERVICE_ENABLED_STATUS_OK, + SET_SERVICE_ENABLED_STATUS_FAILURE_FEATURE_UNSUPPORTED, + SET_SERVICE_ENABLED_STATUS_FAILURE_INVALID_SERVICE, + SET_SERVICE_ENABLED_STATUS_FAILURE_ALREADY_SET, + SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SetServiceEnabledStatusCode {} + static boolean sIsInitialized = false; static HashMap<Context, CardEmulation> sCardEmus = new HashMap<Context, CardEmulation>(); static INfcCardEmulation sService; @@ -883,22 +942,24 @@ public final class CardEmulation { } /** - * Allows to set or unset preferred service (category other) to avoid AID Collision. + * Allows to set or unset preferred service (category other) to avoid AID Collision. The user + * should use corresponding context using {@link Context#createContextAsUser(UserHandle, int)} * * @param service The ComponentName of the service * @param status true to enable, false to disable - * @param userId the user handle of the user whose information is being requested. - * @return set service for the category and true if service is already set return false. + * @return true if preferred service is successfully set or unset, otherwise return false. * * @hide */ - public boolean setServiceEnabledForCategoryOther(ComponentName service, boolean status, - int userId) { - if (service == null) { - throw new NullPointerException("activity or service or category is null"); - } + @SystemApi + @FlaggedApi(Flags.FLAG_NFC_SET_SERVICE_ENABLED_FOR_CATEGORY_OTHER) + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @SetServiceEnabledStatusCode + public int setServiceEnabledForCategoryOther(@NonNull ComponentName service, + boolean status) { return callServiceReturn(() -> - sService.setServiceEnabledForCategoryOther(userId, service, status), false); + sService.setServiceEnabledForCategoryOther(mContext.getUser().getIdentifier(), + service, status), SET_SERVICE_ENABLED_STATUS_FAILURE_UNKNOWN_ERROR); } /** @hide */ diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index 34f020012d3c..8a37aa28cf9d 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -173,3 +173,11 @@ flag { description: "Share wallet role routing priority with associated services" bug: "366243361" } + +flag { + name: "nfc_set_service_enabled_for_category_other" + is_exported: true + namespace: "nfc" + description: "Enable set service enabled for category other" + bug: "338157113" +} diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt index 5ceee6d09978..088bef230374 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt @@ -22,11 +22,13 @@ import com.android.settingslib.graph.proto.PreferenceGraphProto import com.android.settingslib.ipc.ApiHandler import com.android.settingslib.ipc.MessageCodec import com.android.settingslib.metadata.PreferenceScreenRegistry +import com.android.settingslib.preference.PreferenceScreenProvider import java.util.Locale /** API to get preference graph. */ -abstract class GetPreferenceGraphApiHandler : - ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> { +abstract class GetPreferenceGraphApiHandler( + private val preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>> +) : ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> { override val requestCodec: MessageCodec<GetPreferenceGraphRequest> get() = GetPreferenceGraphRequestCodec @@ -40,14 +42,16 @@ abstract class GetPreferenceGraphApiHandler : callingUid: Int, request: GetPreferenceGraphRequest, ): PreferenceGraphProto { - val builderRequest = - if (request.screenKeys.isEmpty()) { - val keys = PreferenceScreenRegistry.preferenceScreens.keys - GetPreferenceGraphRequest(keys, request.visitedScreens, request.locale) - } else { - request + val builder = PreferenceGraphBuilder.of(application, request) + if (request.screenKeys.isEmpty()) { + for (key in PreferenceScreenRegistry.preferenceScreens.keys) { + builder.addPreferenceScreenFromRegistry(key) } - return PreferenceGraphBuilder.of(application, builderRequest).build() + for (provider in preferenceScreenProviders) { + builder.addPreferenceScreenProvider(provider) + } + } + return builder.build() } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index 2256bb38dd2c..6760e72be4a6 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -133,7 +133,7 @@ private constructor(private val context: Context, private val request: GetPrefer null } - private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean { + suspend fun addPreferenceScreenFromRegistry(key: String): Boolean { val metadata = PreferenceScreenRegistry[key] ?: return false return addPreferenceScreenMetadata(metadata) } @@ -146,7 +146,7 @@ private constructor(private val context: Context, private val request: GetPrefer } } - private suspend fun addPreferenceScreenProvider(activityClass: Class<*>) { + suspend fun addPreferenceScreenProvider(activityClass: Class<*>) { Log.d(TAG, "add $activityClass") createPreferenceScreen { activityClass.newInstance() } ?.let { 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 6e4db1d90484..7cfce0d85cd4 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -22,6 +22,7 @@ import androidx.annotation.IntDef import com.android.settingslib.graph.proto.PreferenceValueProto import com.android.settingslib.ipc.ApiDescriptor import com.android.settingslib.ipc.ApiHandler +import com.android.settingslib.ipc.ApiPermissionChecker import com.android.settingslib.ipc.IntMessageCodec import com.android.settingslib.ipc.MessageCodec import com.android.settingslib.metadata.BooleanValue @@ -45,7 +46,11 @@ data class PreferenceSetterRequest( PreferenceSetterResult.OK, PreferenceSetterResult.UNSUPPORTED, PreferenceSetterResult.DISABLED, + PreferenceSetterResult.RESTRICTED, PreferenceSetterResult.UNAVAILABLE, + PreferenceSetterResult.REQUIRE_APP_PERMISSION, + PreferenceSetterResult.REQUIRE_USER_AGREEMENT, + PreferenceSetterResult.DISALLOW, PreferenceSetterResult.INVALID_REQUEST, PreferenceSetterResult.INTERNAL_ERROR, ) @@ -87,14 +92,17 @@ class PreferenceSetterApiDescriptor(override val id: Int) : } /** Preference setter API implementation. */ -class PreferenceSetterApiHandler(override val id: Int) : ApiHandler<PreferenceSetterRequest, Int> { +class PreferenceSetterApiHandler( + override val id: Int, + private val permissionChecker: ApiPermissionChecker<PreferenceSetterRequest>, +) : ApiHandler<PreferenceSetterRequest, Int> { override fun hasPermission( application: Application, myUid: Int, callingUid: Int, request: PreferenceSetterRequest, - ): Boolean = true + ) = permissionChecker.hasPermission(application, myUid, callingUid, request) override suspend fun invoke( application: Application, diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt index d9b9590f60e2..1bda277f2018 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt @@ -16,8 +16,10 @@ package com.android.settingslib.graph +import android.content.ComponentName import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import com.android.settingslib.graph.proto.BundleProto import com.android.settingslib.graph.proto.BundleProto.BundleValue @@ -42,6 +44,20 @@ fun Intent.toProto(): IntentProto = intentProto { this@toProto.type?.let { mimeType = it } } +fun IntentProto.toIntent(): Intent? { + if (!hasComponent()) return null + val componentName = ComponentName.unflattenFromString(component) ?: return null + val intent = Intent() + intent.component = componentName + if (hasAction()) intent.action = action + if (hasData()) intent.data = Uri.parse(data) + if (hasPkg()) intent.`package` = pkg + if (hasFlags()) intent.flags = flags + if (hasExtras()) intent.putExtras(extras.toBundle()) + if (hasMimeType()) intent.setType(mimeType) + return intent +} + fun Bundle.toProto(): BundleProto = bundleProto { fun toProto(value: Any): BundleValue = bundleValueProto { when (value) { @@ -61,14 +77,18 @@ fun Bundle.toProto(): BundleProto = bundleProto { } } -fun BundleValue.stringify(): String = - when { - hasBooleanValue() -> "$valueCase" - hasBytesValue() -> "$bytesValue" - hasIntValue() -> "$intValue" - hasLongValue() -> "$longValue" - hasStringValue() -> stringValue - hasDoubleValue() -> "$doubleValue" - hasBundleValue() -> "$bundleValue" - else -> "Unknown" +fun BundleProto.toBundle(): Bundle = + Bundle().apply { + for ((key, value) in valuesMap) { + when { + value.hasBooleanValue() -> putBoolean(key, value.booleanValue) + value.hasBytesValue() -> putByteArray(key, value.bytesValue.toByteArray()) + value.hasIntValue() -> putInt(key, value.intValue) + value.hasLongValue() -> putLong(key, value.longValue) + value.hasStringValue() -> putString(key, value.stringValue) + value.hasDoubleValue() -> putDouble(key, value.doubleValue) + value.hasBundleValue() -> putBundle(key, value.bundleValue.toBundle()) + else -> throw IllegalArgumentException("Unknown type: ${value.javaClass} $value") + } + } } diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt index 802141dae7ec..4febd89a7da4 100644 --- a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt +++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt @@ -56,6 +56,27 @@ interface ApiDescriptor<Request, Response> { val responseCodec: MessageCodec<Response> } +/** Permission checker for api. */ +fun interface ApiPermissionChecker<R> { + /** + * Returns if the request is permitted. + * + * @param application application context + * @param myUid uid of current process + * @param callingUid uid of peer process + * @param request API request + * @return `false` if permission is denied, otherwise `true` + */ + fun hasPermission(application: Application, myUid: Int, callingUid: Int, request: R): Boolean + + companion object { + private val ALWAYS_ALLOW = ApiPermissionChecker<Any> { _, _, _, _ -> true } + + @Suppress("UNCHECKED_CAST") + fun <T> alwaysAllow(): ApiPermissionChecker<T> = ALWAYS_ALLOW as ApiPermissionChecker<T> + } +} + /** * Handler of API. * @@ -64,18 +85,8 @@ interface ApiDescriptor<Request, Response> { * * The implementation must be threadsafe. */ -interface ApiHandler<Request, Response> : ApiDescriptor<Request, Response> { - /** - * Returns if the request is permitted. - * - * @return `false` if permission is denied, otherwise `true` - */ - fun hasPermission( - application: Application, - myUid: Int, - callingUid: Int, - request: Request, - ): Boolean +interface ApiHandler<Request, Response> : + ApiDescriptor<Request, Response>, ApiPermissionChecker<Request> { /** * Invokes the API. diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt index 7ffefed239a4..ef907e17d824 100644 --- a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt +++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt @@ -320,6 +320,11 @@ constructor( } } + override fun onNullBinding(name: ComponentName) { + Log.i(TAG, "onNullBinding $name") + close(ClientBindServiceException(null)) + } + internal open fun drainPendingRequests() { disposableHandle = null if (pendingRequests.isEmpty()) { diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt index 6e38df11156f..1823ba641775 100644 --- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt +++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt @@ -19,9 +19,14 @@ package com.android.settingslib.service import android.app.Application import com.android.settingslib.graph.GetPreferenceGraphApiHandler import com.android.settingslib.graph.GetPreferenceGraphRequest +import com.android.settingslib.ipc.ApiPermissionChecker +import com.android.settingslib.preference.PreferenceScreenProvider /** Api to get preference graph. */ -internal class PreferenceGraphApi : GetPreferenceGraphApiHandler() { +internal class PreferenceGraphApi( + preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>>, + private val permissionChecker: ApiPermissionChecker<GetPreferenceGraphRequest>, +) : GetPreferenceGraphApiHandler(preferenceScreenProviders) { override val id: Int get() = API_GET_PREFERENCE_GRAPH @@ -31,5 +36,5 @@ internal class PreferenceGraphApi : GetPreferenceGraphApiHandler() { myUid: Int, callingUid: Int, request: GetPreferenceGraphRequest, - ) = true + ) = permissionChecker.hasPermission(application, myUid, callingUid, request) } diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt index 8ebb14522c3d..ed748bb58a9a 100644 --- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt +++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt @@ -16,10 +16,14 @@ package com.android.settingslib.service +import com.android.settingslib.graph.GetPreferenceGraphRequest import com.android.settingslib.graph.PreferenceSetterApiHandler +import com.android.settingslib.graph.PreferenceSetterRequest import com.android.settingslib.ipc.ApiHandler +import com.android.settingslib.ipc.ApiPermissionChecker import com.android.settingslib.ipc.MessengerService import com.android.settingslib.ipc.PermissionChecker +import com.android.settingslib.preference.PreferenceScreenProvider /** * Preference service providing a bunch of APIs. @@ -28,14 +32,21 @@ import com.android.settingslib.ipc.PermissionChecker * [PREFERENCE_SERVICE_ACTION]. */ open class PreferenceService( - permissionChecker: PermissionChecker, name: String = "PreferenceService", + permissionChecker: PermissionChecker = PermissionChecker { _, _, _ -> true }, + preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>> = setOf(), + graphPermissionChecker: ApiPermissionChecker<GetPreferenceGraphRequest>? = null, + setterPermissionChecker: ApiPermissionChecker<PreferenceSetterRequest>? = null, + vararg apiHandlers: ApiHandler<*, *>, ) : MessengerService( - listOf<ApiHandler<*, *>>( - PreferenceGraphApi(), - PreferenceSetterApiHandler(API_PREFERENCE_SETTER), - ), + mutableListOf<ApiHandler<*, *>>().apply { + graphPermissionChecker?.let { add(PreferenceGraphApi(preferenceScreenProviders, it)) } + setterPermissionChecker?.let { + add(PreferenceSetterApiHandler(API_PREFERENCE_SETTER, it)) + } + addAll(apiHandlers) + }, permissionChecker, name, ) diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt index 1f38a6678eae..7655daa6cf21 100644 --- a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt +++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt @@ -18,5 +18,14 @@ package com.android.settingslib.service const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE" +/** API id for retrieving preference graph. */ internal const val API_GET_PREFERENCE_GRAPH = 1 + +/** API id for preference value setter. */ internal const val API_PREFERENCE_SETTER = 2 + +/** + * The max API id reserved for internal preference service usages. Custom API id should start with + * **1000** to avoid conflict. + */ +internal const val API_MAX_RESERVED = 999 diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java index 83ee9751329f..80e5e5981912 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java @@ -15,6 +15,7 @@ */ package com.android.settingslib.media; +import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO; import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC; import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY; @@ -103,7 +104,8 @@ public class InputMediaDevice extends MediaDevice { TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY, - TYPE_BLUETOOTH_SCO -> + TYPE_BLUETOOTH_SCO, + TYPE_BLE_HEADSET -> true; default -> false; }; @@ -124,7 +126,7 @@ public class InputMediaDevice extends MediaDevice { mProductName != null ? mProductName : mContext.getString(R.string.media_transfer_usb_device_mic_name); - case TYPE_BLUETOOTH_SCO -> + case TYPE_BLUETOOTH_SCO, TYPE_BLE_HEADSET -> mProductName != null ? mProductName : mContext.getString(R.string.media_transfer_bt_device_mic_name); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java index 7775b912e51d..8624c4df833b 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java @@ -38,6 +38,7 @@ public class InputMediaDeviceTest { private final int WIRED_HEADSET_ID = 2; private final int USB_HEADSET_ID = 3; private final int BT_HEADSET_ID = 4; + private final int BLE_HEADSET_ID = 5; private final int MAX_VOLUME = 1; private final int CURRENT_VOLUME = 0; private final boolean IS_VOLUME_FIXED = true; @@ -45,6 +46,7 @@ public class InputMediaDeviceTest { private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset"; private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset"; private static final String PRODUCT_NAME_BT_HEADSET = "My Bluetooth Headset"; + private static final String PRODUCT_NAME_BLE_HEADSET = "My BLE Headset"; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -163,4 +165,35 @@ public class InputMediaDeviceTest { assertThat(btMediaDevice.getName()) .isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name)); } + + @Test + public void getName_returnCorrectName_bleHeadset() { + InputMediaDevice bleMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(BLE_HEADSET_ID), + AudioDeviceInfo.TYPE_BLE_HEADSET, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED, + PRODUCT_NAME_BLE_HEADSET); + assertThat(bleMediaDevice).isNotNull(); + assertThat(bleMediaDevice.getName()).isEqualTo(PRODUCT_NAME_BLE_HEADSET); + } + + @Test + public void getName_returnCorrectName_bleHeadset_nullProductName() { + InputMediaDevice bleMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(BLE_HEADSET_ID), + AudioDeviceInfo.TYPE_BLE_HEADSET, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED, + null); + assertThat(bleMediaDevice).isNotNull(); + assertThat(bleMediaDevice.getName()) + .isEqualTo(mContext.getString(R.string.media_transfer_bt_device_mic_name)); + } } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 7b6321d1cc7d..859445eb9dd0 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -953,6 +953,13 @@ <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" android:featureFlag="android.security.aapm_api"/> + <!-- Permission required for CTS test - ForensicManagerTest --> + <uses-permission android:name="android.permission.READ_FORENSIC_STATE" + android:featureFlag="android.security.afl_api"/> + <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE" + android:featureFlag="android.security.afl_api"/> + + <!-- Permission required for CTS test - CtsAppTestCases --> <uses-permission android:name="android.permission.KILL_UID" /> diff --git a/packages/Shell/OWNERS b/packages/Shell/OWNERS index 8feefa5d892f..d4b5b86223ea 100644 --- a/packages/Shell/OWNERS +++ b/packages/Shell/OWNERS @@ -12,3 +12,4 @@ patb@google.com cbrubaker@google.com omakoto@google.com michaelwr@google.com +ronish@google.com diff --git a/packages/Shell/aconfig/Android.bp b/packages/Shell/aconfig/Android.bp new file mode 100644 index 000000000000..1d797b29c1a4 --- /dev/null +++ b/packages/Shell/aconfig/Android.bp @@ -0,0 +1,13 @@ +aconfig_declarations { + name: "wear_aconfig_declarations", + package: "com.android.shell.flags", + container: "system", + srcs: [ + "wear.aconfig", + ], +} + +java_aconfig_library { + name: "wear_aconfig_declarations_flags_java_lib", + aconfig_declarations: "wear_aconfig_declarations", +} diff --git a/packages/Shell/aconfig/wear.aconfig b/packages/Shell/aconfig/wear.aconfig new file mode 100644 index 000000000000..e07bd963a4aa --- /dev/null +++ b/packages/Shell/aconfig/wear.aconfig @@ -0,0 +1,9 @@ +package: "com.android.shell.flags" +container: "system" + +flag { + name: "handle_bugreports_for_wear" + namespace: "wear_services" + description: "This flag enables Shell to propagate bugreport results to WearServices." + bug: "378060870" +}
\ No newline at end of file diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java index 7c478ac78a29..7f25b51e35ca 100644 --- a/packages/Shell/src/com/android/shell/BugreportProgressService.java +++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java @@ -153,6 +153,10 @@ public class BugreportProgressService extends Service { static final String INTENT_BUGREPORT_FINISHED = "com.android.internal.intent.action.BUGREPORT_FINISHED"; + // Intent sent to notify external apps that bugreport aborted due to error. + static final String INTENT_BUGREPORT_ABORTED = + "com.android.internal.intent.action.BUGREPORT_ABORTED"; + // Internal intents used on notification actions. static final String INTENT_BUGREPORT_CANCEL = "android.intent.action.BUGREPORT_CANCEL"; static final String INTENT_BUGREPORT_SHARE = "android.intent.action.BUGREPORT_SHARE"; @@ -174,6 +178,8 @@ public class BugreportProgressService extends Service { static final String EXTRA_INFO = "android.intent.extra.INFO"; static final String EXTRA_EXTRA_ATTACHMENT_URIS = "android.intent.extra.EXTRA_ATTACHMENT_URIS"; + static final String EXTRA_ABORTED_ERROR_CODE = + "android.intent.extra.EXTRA_ABORTED_ERROR_CODE"; private static final ThreadFactory sBugreportManagerCallbackThreadFactory = new ThreadFactory() { @@ -404,6 +410,7 @@ public class BugreportProgressService extends Service { @Override public void onError(@BugreportErrorCode int errorCode) { synchronized (mLock) { + sendBugreportAbortedBroadcastLocked(errorCode); stopProgressLocked(mInfo.id); mInfo.deleteEmptyFiles(); } @@ -460,6 +467,13 @@ public class BugreportProgressService extends Service { onBugreportFinished(mInfo); } } + + @GuardedBy("mLock") + private void sendBugreportAbortedBroadcastLocked(@BugreportErrorCode int errorCode) { + final Intent intent = new Intent(INTENT_BUGREPORT_ABORTED); + intent.putExtra(EXTRA_ABORTED_ERROR_CODE, errorCode); + mContext.sendBroadcast(intent, android.Manifest.permission.DUMP); + } } private void sendRemoteBugreportFinishedBroadcast(Context context, diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 380344a23c99..e47704ebdf86 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -153,5 +153,11 @@ { "path": "cts/tests/tests/multiuser" } + ], + + "sysui-e2e-presubmit": [ + { + "name": "PlatformScenarioTests_SysUI_Presubmit" + } ] } 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 18f40c98fe04..eee0cafd34fe 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt @@ -24,18 +24,22 @@ import android.app.WindowConfiguration import android.content.ComponentName import android.graphics.Color import android.graphics.Matrix +import android.graphics.PointF import android.graphics.Rect import android.graphics.RectF import android.os.Binder import android.os.Build import android.os.Handler +import android.os.IBinder import android.os.Looper import android.os.RemoteException +import android.util.ArrayMap import android.util.Log import android.view.IRemoteAnimationFinishedCallback import android.view.IRemoteAnimationRunner import android.view.RemoteAnimationAdapter import android.view.RemoteAnimationTarget +import android.view.SurfaceControl import android.view.SyncRtSurfaceTransactionApplier import android.view.View import android.view.ViewGroup @@ -45,8 +49,12 @@ import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.view.animation.PathInterpolator +import android.window.IRemoteTransition +import android.window.IRemoteTransitionFinishedCallback import android.window.RemoteTransition import android.window.TransitionFilter +import android.window.TransitionInfo +import android.window.WindowAnimationState import androidx.annotation.AnyThread import androidx.annotation.BinderThread import androidx.annotation.UiThread @@ -55,11 +63,14 @@ import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.ScreenDecorationsUtils import com.android.systemui.Flags.activityTransitionUseLargestWindow import com.android.systemui.Flags.translucentOccludingActivityFix +import com.android.systemui.animation.TransitionAnimator.Companion.assertLongLivedReturnAnimations +import com.android.systemui.animation.TransitionAnimator.Companion.assertReturnAnimations +import com.android.systemui.animation.TransitionAnimator.Companion.longLivedReturnAnimationsEnabled +import com.android.systemui.animation.TransitionAnimator.Companion.returnAnimationsEnabled import com.android.systemui.animation.TransitionAnimator.Companion.toTransitionState -import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary -import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived import com.android.wm.shell.shared.IShellTransitions import com.android.wm.shell.shared.ShellTransitions +import com.android.wm.shell.shared.TransitionUtil import java.util.concurrent.Executor import kotlin.math.roundToInt @@ -194,7 +205,13 @@ constructor( private const val LONG_TRANSITION_TIMEOUT = 5_000L private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator { - return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS) + return TransitionAnimator( + mainExecutor, + TIMINGS, + INTERPOLATORS, + SPRING_TIMINGS, + SPRING_INTERPOLATORS, + ) } private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator { @@ -275,7 +292,7 @@ constructor( "ActivityTransitionAnimator.callback must be set before using this animator" ) val runner = createRunner(controller) - val runnerDelegate = runner.delegate!! + val runnerDelegate = runner.delegate val hideKeyguardWithAnimation = callback.isOnKeyguard() && !showOverLockscreen // Pass the RemoteAnimationAdapter to the intent starter only if we are not hiding the @@ -330,7 +347,11 @@ constructor( // If we expect an animation, post a timeout to cancel it in case the remote animation is // never started. if (willAnimate) { - runnerDelegate.postTimeouts() + if (longLivedReturnAnimationsEnabled()) { + runner.postTimeouts() + } else { + runnerDelegate!!.postTimeouts() + } // Hide the keyguard using the launch animation instead of the default unlock animation. if (hideKeyguardWithAnimation) { @@ -390,7 +411,7 @@ constructor( launchController: Controller, transitionRegister: TransitionRegister?, ) { - if (!returnAnimationFrameworkLibrary()) return + if (!returnAnimationsEnabled()) return var cleanUpRunnable: Runnable? = null val returnRunner = @@ -413,7 +434,8 @@ constructor( private fun cleanUp() { cleanUpRunnable?.run() } - } + }, + initializeLazily = longLivedReturnAnimationsEnabled(), ) // mTypeSet and mModes match back signals only, and not home. This is on purpose, because @@ -435,7 +457,11 @@ constructor( "${launchController.transitionCookie}_returnTransition", ) - transitionRegister?.register(filter, transition) + transitionRegister?.register( + filter, + transition, + includeTakeover = longLivedReturnAnimationsEnabled(), + ) cleanUpRunnable = Runnable { transitionRegister?.unregister(transition) } } @@ -451,7 +477,10 @@ constructor( /** Create a new animation [Runner] controlled by [controller]. */ @VisibleForTesting - fun createRunner(controller: Controller): Runner { + @JvmOverloads + fun createRunner(controller: Controller, initializeLazily: Boolean = false): Runner { + if (initializeLazily) assertLongLivedReturnAnimations() + // Make sure we use the modified timings when animating a dialog into an app. val transitionAnimator = if (controller.isDialogLaunch) { @@ -460,7 +489,13 @@ constructor( transitionAnimator } - return Runner(controller, callback!!, transitionAnimator, lifecycleListener) + return Runner( + controller, + callback!!, + transitionAnimator, + lifecycleListener, + initializeLazily, + ) } interface PendingIntentStarter { @@ -628,10 +663,7 @@ constructor( * this registration. */ fun register(controller: Controller) { - check(returnAnimationFrameworkLongLived()) { - "Long-lived registrations cannot be used when the returnAnimationFrameworkLongLived " + - "flag is disabled" - } + assertLongLivedReturnAnimations() if (transitionRegister == null) { throw IllegalStateException( @@ -667,10 +699,10 @@ constructor( } val launchRemoteTransition = RemoteTransition( - RemoteAnimationRunnerCompat.wrap(createRunner(controller)), + OriginTransition(createRunner(controller, initializeLazily = true)), "${cookie}_launchTransition", ) - transitionRegister.register(launchFilter, launchRemoteTransition) + transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true) val returnController = object : Controller by controller { @@ -689,10 +721,10 @@ constructor( } val returnRemoteTransition = RemoteTransition( - RemoteAnimationRunnerCompat.wrap(createRunner(returnController)), + OriginTransition(createRunner(returnController, initializeLazily = true)), "${cookie}_returnTransition", ) - transitionRegister.register(returnFilter, returnRemoteTransition) + transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true) longLivedTransitions[cookie] = Pair(launchRemoteTransition, returnRemoteTransition) } @@ -738,14 +770,154 @@ constructor( } } + /** [Runner] wrapper that supports animation takeovers. */ + private inner class OriginTransition(private val runner: Runner) : IRemoteTransition { + private val delegate = RemoteAnimationRunnerCompat.wrap(runner) + + init { + assertLongLivedReturnAnimations() + } + + override fun startAnimation( + token: IBinder?, + info: TransitionInfo?, + t: SurfaceControl.Transaction?, + finishCallback: IRemoteTransitionFinishedCallback?, + ) { + delegate.startAnimation(token, info, t, finishCallback) + } + + override fun mergeAnimation( + transition: IBinder?, + info: TransitionInfo?, + t: SurfaceControl.Transaction?, + mergeTarget: IBinder?, + finishCallback: IRemoteTransitionFinishedCallback?, + ) { + delegate.mergeAnimation(transition, info, t, mergeTarget, finishCallback) + } + + override fun onTransitionConsumed(transition: IBinder?, aborted: Boolean) { + delegate.onTransitionConsumed(transition, aborted) + } + + override fun takeOverAnimation( + token: IBinder?, + info: TransitionInfo?, + t: SurfaceControl.Transaction?, + finishCallback: IRemoteTransitionFinishedCallback?, + states: Array<WindowAnimationState>, + ) { + if (info == null || t == null) { + Log.e( + TAG, + "Skipping the animation takeover because the required data is missing: " + + "info=$info, transaction=$t", + ) + return + } + + // The following code converts the contents of the given TransitionInfo into + // RemoteAnimationTargets. This is necessary because we must currently support both the + // new (Shell, remote transitions) and old (remote animations) framework to maintain + // functionality for all users of the library. + val apps = ArrayList<RemoteAnimationTarget>() + val filteredStates = ArrayList<WindowAnimationState>() + val leashMap = ArrayMap<SurfaceControl, SurfaceControl>() + val leafTaskFilter = TransitionUtil.LeafTaskFilter() + + // About layering: we divide up the "layer space" into 2 regions (each the size of the + // change count). This lets us categorize things into above and below while + // maintaining their relative ordering. + val belowLayers = info.changes.size + val aboveLayers = info.changes.size * 2 + for (i in info.changes.indices) { + val change = info.changes[i] + if (change == null || change.taskInfo == null) { + continue + } + + val taskInfo = change.taskInfo + + if (TransitionUtil.isWallpaper(change)) { + val target = + TransitionUtil.newTarget( + change, + belowLayers - i, // wallpapers go into the "below" layer space + info, + t, + leashMap, + ) + + // Make all the wallpapers opaque. + t.setAlpha(target.leash, 1f) + } else if (leafTaskFilter.test(change)) { + // Start by putting everything into the "below" layer space. + val target = + TransitionUtil.newTarget(change, belowLayers - i, info, t, leashMap) + apps.add(target) + filteredStates.add(states[i]) + + // Make all the apps opaque. + t.setAlpha(target.leash, 1f) + + if ( + TransitionUtil.isClosingType(change.mode) && + taskInfo?.topActivityType != WindowConfiguration.ACTIVITY_TYPE_HOME + ) { + // Raise closing task to "above" layer so it isn't covered. + t.setLayer(target.leash, aboveLayers - i) + } + } else if (TransitionInfo.isIndependent(change, info)) { + // Root tasks + if (TransitionUtil.isClosingType(change.mode)) { + // Raise closing task to "above" layer so it isn't covered. + t.setLayer(change.leash, aboveLayers - i) + } else if (TransitionUtil.isOpeningType(change.mode)) { + // Put into the "below" layer space. + t.setLayer(change.leash, belowLayers - i) + } + } else if (TransitionUtil.isDividerBar(change)) { + val target = + TransitionUtil.newTarget(change, belowLayers - i, info, t, leashMap) + apps.add(target) + filteredStates.add(states[i]) + } + } + + val wrappedCallback: IRemoteAnimationFinishedCallback = + object : IRemoteAnimationFinishedCallback.Stub() { + override fun onAnimationFinished() { + leashMap.clear() + val finishTransaction = SurfaceControl.Transaction() + finishCallback?.onTransitionFinished(null, finishTransaction) + finishTransaction.close() + } + } + + runner.takeOverAnimation( + apps.toTypedArray(), + filteredStates.toTypedArray(), + t, + wrappedCallback, + ) + } + + override fun asBinder(): IBinder { + return delegate.asBinder() + } + } + @VisibleForTesting inner class Runner( - controller: Controller, - callback: Callback, + private val controller: Controller, + private val callback: Callback, /** The animator to use to animate the window transition. */ - transitionAnimator: TransitionAnimator, + private val transitionAnimator: TransitionAnimator, /** Listener for animation lifecycle events. */ - listener: Listener? = null, + private val listener: Listener? = null, + /** Whether the internal [delegate] should be initialized lazily. */ + private val initializeLazily: Boolean = false, ) : IRemoteAnimationRunner.Stub() { // This is being passed across IPC boundaries and cycles (through PendingIntentRecords, // etc.) are possible. So we need to make sure we drop any references that might @@ -753,15 +925,14 @@ constructor( @VisibleForTesting var delegate: AnimationDelegate? init { - delegate = - AnimationDelegate( - mainExecutor, - controller, - callback, - DelegatingAnimationCompletionListener(listener, this::dispose), - transitionAnimator, - disableWmTimeout, - ) + delegate = null + if (!initializeLazily) { + // Ephemeral launches bundle the runner with the launch request (instead of being + // registered ahead of time for later use). This means that there could be a timeout + // between creation and invocation, so the delegate needs to exist from the + // beginning in order to handle such timeout. + createDelegate() + } } @BinderThread @@ -772,6 +943,36 @@ constructor( nonApps: Array<out RemoteAnimationTarget>?, finishedCallback: IRemoteAnimationFinishedCallback?, ) { + initAndRun(finishedCallback) { delegate -> + delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback) + } + } + + @VisibleForTesting + @BinderThread + fun takeOverAnimation( + apps: Array<RemoteAnimationTarget>?, + windowAnimationStates: Array<WindowAnimationState>, + startTransaction: SurfaceControl.Transaction, + finishedCallback: IRemoteAnimationFinishedCallback?, + ) { + assertLongLivedReturnAnimations() + initAndRun(finishedCallback) { delegate -> + delegate.takeOverAnimation( + apps, + windowAnimationStates, + startTransaction, + finishedCallback, + ) + } + } + + @BinderThread + private fun initAndRun( + finishedCallback: IRemoteAnimationFinishedCallback?, + performAnimation: (AnimationDelegate) -> Unit, + ) { + maybeSetUp() val delegate = delegate mainExecutor.execute { if (delegate == null) { @@ -780,7 +981,7 @@ constructor( // signal back that we're done with it. finishedCallback?.onAnimationFinished() } else { - delegate.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback) + performAnimation(delegate) } } } @@ -794,6 +995,32 @@ constructor( } } + @VisibleForTesting + @UiThread + fun postTimeouts() { + maybeSetUp() + delegate?.postTimeouts() + } + + @AnyThread + private fun maybeSetUp() { + if (!initializeLazily || delegate != null) return + createDelegate() + } + + @AnyThread + private fun createDelegate() { + delegate = + AnimationDelegate( + mainExecutor, + controller, + callback, + DelegatingAnimationCompletionListener(listener, this::dispose), + transitionAnimator, + disableWmTimeout, + ) + } + @AnyThread fun dispose() { // Drop references to animation controller once we're done with the animation @@ -867,7 +1094,7 @@ constructor( init { // We do this check here to cover all entry points, including Launcher which doesn't // call startIntentWithAnimation() - if (!controller.isLaunching) TransitionAnimator.checkReturnAnimationFrameworkFlag() + if (!controller.isLaunching) assertReturnAnimations() } @UiThread @@ -893,19 +1120,73 @@ constructor( nonApps: Array<out RemoteAnimationTarget>?, callback: IRemoteAnimationFinishedCallback?, ) { + val window = setUpAnimation(apps, callback) ?: return + + if (controller.windowAnimatorState == null || !longLivedReturnAnimationsEnabled()) { + val navigationBar = + nonApps?.firstOrNull { + it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR + } + + startAnimation(window, navigationBar, iCallback = callback) + } else { + // If a [controller.windowAnimatorState] exists, treat this like a takeover. + takeOverAnimationInternal( + window, + startWindowStates = null, + startTransaction = null, + callback, + ) + } + } + + @UiThread + internal fun takeOverAnimation( + apps: Array<out RemoteAnimationTarget>?, + startWindowStates: Array<WindowAnimationState>, + startTransaction: SurfaceControl.Transaction, + callback: IRemoteAnimationFinishedCallback?, + ) { + val window = setUpAnimation(apps, callback) ?: return + takeOverAnimationInternal(window, startWindowStates, startTransaction, callback) + } + + private fun takeOverAnimationInternal( + window: RemoteAnimationTarget, + startWindowStates: Array<WindowAnimationState>?, + startTransaction: SurfaceControl.Transaction?, + callback: IRemoteAnimationFinishedCallback?, + ) { + val useSpring = + !controller.isLaunching && startWindowStates != null && startTransaction != null + startAnimation( + window, + navigationBar = null, + useSpring, + startWindowStates, + startTransaction, + callback, + ) + } + + @UiThread + private fun setUpAnimation( + apps: Array<out RemoteAnimationTarget>?, + callback: IRemoteAnimationFinishedCallback?, + ): RemoteAnimationTarget? { removeTimeouts() // The animation was started too late and we already notified the controller that it // timed out. if (timedOut) { callback?.invoke() - return + return null } // This should not happen, but let's make sure we don't start the animation if it was // cancelled before and we already notified the controller. if (cancelled) { - return + return null } val window = findTargetWindowIfPossible(apps) @@ -921,15 +1202,10 @@ constructor( } controller.onTransitionAnimationCancelled() listener?.onTransitionAnimationCancelled() - return + return null } - val navigationBar = - nonApps?.firstOrNull { - it.windowType == WindowManager.LayoutParams.TYPE_NAVIGATION_BAR - } - - startAnimation(window, navigationBar, callback) + return window } private fun findTargetWindowIfPossible( @@ -950,7 +1226,7 @@ constructor( for (it in apps) { if (it.mode == targetMode) { if (activityTransitionUseLargestWindow()) { - if (returnAnimationFrameworkLibrary()) { + if (returnAnimationsEnabled()) { // If the controller contains a cookie, _only_ match if either the // candidate contains the matching cookie, or a component is also // defined and is a match. @@ -995,8 +1271,11 @@ constructor( private fun startAnimation( window: RemoteAnimationTarget, - navigationBar: RemoteAnimationTarget?, - iCallback: IRemoteAnimationFinishedCallback?, + navigationBar: RemoteAnimationTarget? = null, + useSpring: Boolean = false, + startingWindowStates: Array<WindowAnimationState>? = null, + startTransaction: SurfaceControl.Transaction? = null, + iCallback: IRemoteAnimationFinishedCallback? = null, ) { if (TransitionAnimator.DEBUG) { Log.d(TAG, "Remote animation started") @@ -1046,18 +1325,66 @@ constructor( val controller = object : Controller by delegate { override fun createAnimatorState(): TransitionAnimator.State { - if (isLaunching) return delegate.createAnimatorState() - return delegate.windowAnimatorState?.toTransitionState() - ?: getWindowRadius(isExpandingFullyAbove).let { - TransitionAnimator.State( - top = windowBounds.top, - bottom = windowBounds.bottom, - left = windowBounds.left, - right = windowBounds.right, - topCornerRadius = it, - bottomCornerRadius = it, - ) + if (isLaunching) { + return delegate.createAnimatorState() + } else if (!longLivedReturnAnimationsEnabled()) { + return delegate.windowAnimatorState?.toTransitionState() + ?: getWindowRadius(isExpandingFullyAbove).let { + TransitionAnimator.State( + top = windowBounds.top, + bottom = windowBounds.bottom, + left = windowBounds.left, + right = windowBounds.right, + topCornerRadius = it, + bottomCornerRadius = it, + ) + } + } + + // The states are sorted matching the changes inside the transition info. + // Using this info, the RemoteAnimationTargets are created, with their + // prefixOrderIndex fields in reverse order to that of changes. To extract + // the right state, we need to invert again. + val windowState = + if (startingWindowStates != null) { + startingWindowStates[ + startingWindowStates.size - window.prefixOrderIndex] + } else { + controller.windowAnimatorState } + + // TODO(b/323863002): use the timestamp and velocity to update the initial + // position. + val bounds = windowState?.bounds + val left: Int = bounds?.left?.toInt() ?: windowBounds.left + val top: Int = bounds?.top?.toInt() ?: windowBounds.top + val right: Int = bounds?.right?.toInt() ?: windowBounds.right + val bottom: Int = bounds?.bottom?.toInt() ?: windowBounds.bottom + + val width = windowBounds.right - windowBounds.left + val height = windowBounds.bottom - windowBounds.top + // Scale the window. We use the max of (widthRatio, heightRatio) so that + // there is no blank space on any side. + val widthRatio = (right - left).toFloat() / width + val heightRatio = (bottom - top).toFloat() / height + val startScale = maxOf(widthRatio, heightRatio) + + val maybeRadius = windowState?.topLeftRadius + val windowRadius = + if (maybeRadius != null) { + maybeRadius * startScale + } else { + getWindowRadius(isExpandingFullyAbove) + } + + return TransitionAnimator.State( + top = top, + bottom = bottom, + left = left, + right = right, + topCornerRadius = windowRadius, + bottomCornerRadius = windowRadius, + ) } override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) { @@ -1071,6 +1398,19 @@ constructor( "[controller=$delegate]", ) } + + if (startTransaction != null) { + // Calling applyStateToWindow() here avoids skipping a frame when taking + // over an animation. + applyStateToWindow( + window, + createAnimatorState(), + linearProgress = 0f, + useSpring, + startTransaction, + ) + } + delegate.onTransitionAnimationStart(isExpandingFullyAbove) } @@ -1094,14 +1434,29 @@ constructor( progress: Float, linearProgress: Float, ) { - applyStateToWindow(window, state, linearProgress) + applyStateToWindow(window, state, linearProgress, useSpring) navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) } listener?.onTransitionAnimationProgress(linearProgress) delegate.onTransitionAnimationProgress(state, progress, linearProgress) } } - + val windowState = + if (startingWindowStates != null) { + startingWindowStates[startingWindowStates.size - window.prefixOrderIndex] + } else { + controller.windowAnimatorState + } + val velocityPxPerS = + if (longLivedReturnAnimationsEnabled() && windowState?.velocityPxPerMs != null) { + val xVelocityPxPerS = windowState.velocityPxPerMs.x * 1000 + val yVelocityPxPerS = windowState.velocityPxPerMs.y * 1000 + PointF(xVelocityPxPerS, yVelocityPxPerS) + } else if (useSpring) { + PointF(0f, 0f) + } else { + null + } animation = transitionAnimator.startAnimation( controller, @@ -1109,6 +1464,7 @@ constructor( windowBackgroundColor, fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow, drawHole = !controller.isBelowAnimatingWindow, + startVelocity = velocityPxPerS, ) } @@ -1128,6 +1484,8 @@ constructor( window: RemoteAnimationTarget, state: TransitionAnimator.State, linearProgress: Float, + useSpring: Boolean, + transaction: SurfaceControl.Transaction? = null, ) { if (transactionApplierView.viewRootImpl == null || !window.leash.isValid) { // Don't apply any transaction if the view root we synchronize with was detached or @@ -1171,25 +1529,47 @@ constructor( windowCropF.bottom.roundToInt(), ) - val windowAnimationDelay = + val interpolators: TransitionAnimator.Interpolators + val windowProgress: Float + + if (useSpring) { + val windowAnimationDelay: Float + val windowAnimationDuration: Float if (controller.isLaunching) { - TIMINGS.contentAfterFadeInDelay + windowAnimationDelay = SPRING_TIMINGS.contentAfterFadeInDelay + windowAnimationDuration = SPRING_TIMINGS.contentAfterFadeInDuration } else { - TIMINGS.contentBeforeFadeOutDelay + windowAnimationDelay = SPRING_TIMINGS.contentBeforeFadeOutDelay + windowAnimationDuration = SPRING_TIMINGS.contentBeforeFadeOutDuration } - val windowAnimationDuration = + + interpolators = SPRING_INTERPOLATORS + windowProgress = + TransitionAnimator.getProgress( + linearProgress, + windowAnimationDelay, + windowAnimationDuration, + ) + } else { + val windowAnimationDelay: Long + val windowAnimationDuration: Long if (controller.isLaunching) { - TIMINGS.contentAfterFadeInDuration + windowAnimationDelay = TIMINGS.contentAfterFadeInDelay + windowAnimationDuration = TIMINGS.contentAfterFadeInDuration } else { - TIMINGS.contentBeforeFadeOutDuration + windowAnimationDelay = TIMINGS.contentBeforeFadeOutDelay + windowAnimationDuration = TIMINGS.contentBeforeFadeOutDuration } - val windowProgress = - TransitionAnimator.getProgress( - TIMINGS, - linearProgress, - windowAnimationDelay, - windowAnimationDuration, - ) + + interpolators = INTERPOLATORS + windowProgress = + TransitionAnimator.getProgress( + TIMINGS, + linearProgress, + windowAnimationDelay, + windowAnimationDuration, + ) + } // The alpha of the opening window. If it opens above the expandable, then it should // fade in progressively. Otherwise, it should be fully opaque and will be progressively @@ -1197,12 +1577,12 @@ constructor( val alpha = if (controller.isBelowAnimatingWindow) { if (controller.isLaunching) { - INTERPOLATORS.contentAfterFadeInInterpolator.getInterpolation( + interpolators.contentAfterFadeInInterpolator.getInterpolation( windowProgress ) } else { 1 - - INTERPOLATORS.contentBeforeFadeOutInterpolator.getInterpolation( + interpolators.contentBeforeFadeOutInterpolator.getInterpolation( windowProgress ) } @@ -1216,6 +1596,7 @@ constructor( // especially important for lock screen animations, where the window is not clipped by // the shade. val cornerRadius = maxOf(state.topCornerRadius, state.bottomCornerRadius) / scale + val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(window.leash) .withAlpha(alpha) @@ -1223,11 +1604,15 @@ constructor( .withWindowCrop(windowCrop) .withCornerRadius(cornerRadius) .withVisibility(true) - .build() + if (transaction != null) params.withMergeTransaction(transaction) - transactionApplier.scheduleApply(params) + transactionApplier.scheduleApply(params.build()) } + // TODO(b/377643129): remote transitions have no way of identifying the navbar when + // converting to RemoteAnimationTargets (and in my testing it was never included in the + // transition at all). So this method is not used anymore. Remove or adapt once we fully + // convert to remote transitions. private fun applyStateToNavigationBar( navigationBar: RemoteAnimationTarget, state: TransitionAnimator.State, @@ -1362,9 +1747,17 @@ constructor( } /** Register [remoteTransition] with WM Shell using the given [filter]. */ - internal fun register(filter: TransitionFilter, remoteTransition: RemoteTransition) { + internal fun register( + filter: TransitionFilter, + remoteTransition: RemoteTransition, + includeTakeover: Boolean, + ) { shellTransitions?.registerRemote(filter, remoteTransition) iShellTransitions?.registerRemote(filter, remoteTransition) + if (includeTakeover) { + shellTransitions?.registerRemoteForTakeover(filter, remoteTransition) + iShellTransitions?.registerRemoteForTakeover(filter, remoteTransition) + } } /** Unregister [remoteTransition] from WM Shell. */ diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index fdb4871423c3..de4bdbc284c4 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -38,6 +38,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.internal.dynamicanimation.animation.SpringAnimation import com.android.internal.dynamicanimation.animation.SpringForce import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary +import com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived import java.util.concurrent.Executor import kotlin.math.abs import kotlin.math.max @@ -113,13 +114,26 @@ class TransitionAnimator( ) } - internal fun checkReturnAnimationFrameworkFlag() { - check(returnAnimationFrameworkLibrary()) { - "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag is " + - "disabled" + internal fun assertReturnAnimations() { + check(returnAnimationsEnabled()) { + "isLaunching cannot be false when the returnAnimationFrameworkLibrary flag " + + "is disabled" } } + internal fun returnAnimationsEnabled() = returnAnimationFrameworkLibrary() + + internal fun assertLongLivedReturnAnimations() { + check(longLivedReturnAnimationsEnabled()) { + "Long-lived registrations cannot be used when the " + + "returnAnimationFrameworkLibrary or the " + + "returnAnimationFrameworkLongLived flag are disabled" + } + } + + internal fun longLivedReturnAnimationsEnabled() = + returnAnimationFrameworkLibrary() && returnAnimationFrameworkLongLived() + internal fun WindowAnimationState.toTransitionState() = State().also { bounds?.let { b -> @@ -467,7 +481,8 @@ class TransitionAnimator( drawHole: Boolean = false, startVelocity: PointF? = null, ): Animation { - if (!controller.isLaunching || startVelocity != null) checkReturnAnimationFrameworkFlag() + if (!controller.isLaunching) assertReturnAnimations() + if (startVelocity != null) assertLongLivedReturnAnimations() // We add an extra layer with the same color as the dialog/app splash screen background // color, which is usually the same color of the app background. We first fade in this layer diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index 20c8c6a3f4bf..2d32fd768eaa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor @@ -84,6 +85,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonVi import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.qs.ui.composable.QuickSettingsTheme +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import kotlinx.coroutines.launch @@ -264,7 +266,11 @@ private fun IconButton(model: FooterActionsButtonViewModel, modifier: Modifier = color = colorAttr(model.backgroundColor), shape = CircleShape, onClick = model.onClick, - modifier = modifier, + modifier = + modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + CornerSize(percent = 50), + ), ) { val tint = model.iconTint?.let { Color(it) } ?: Color.Unspecified Icon(model.icon, tint = tint, modifier = Modifier.size(20.dp)) @@ -291,7 +297,11 @@ private fun NumberButton( shape = CircleShape, onClick = onClick, interactionSource = interactionSource, - modifier = modifier, + modifier = + modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + CornerSize(percent = 50), + ), ) { Box(Modifier.size(40.dp)) { Box( @@ -342,7 +352,10 @@ private fun TextButton( color = colorAttr(R.attr.underSurface), contentColor = MaterialTheme.colorScheme.onSurfaceVariant, borderStroke = BorderStroke(1.dp, colorAttr(R.attr.shadeInactive)), - modifier = modifier.padding(horizontal = 4.dp), + modifier = + modifier + .padding(horizontal = 4.dp) + .borderOnFocus(color = MaterialTheme.colorScheme.secondary, CornerSize(50)), onClick = onClick, ) { Row( 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 2cde6787f730..9de7a5d659ae 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 @@ -15,7 +15,10 @@ import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransit import com.android.systemui.scene.ui.composable.transitions.bouncerToLockscreenPreview import com.android.systemui.scene.ui.composable.transitions.communalToBouncerTransition import com.android.systemui.scene.ui.composable.transitions.communalToShadeTransition +import com.android.systemui.scene.ui.composable.transitions.dreamToBouncerTransition +import com.android.systemui.scene.ui.composable.transitions.dreamToCommunalTransition import com.android.systemui.scene.ui.composable.transitions.dreamToGoneTransition +import com.android.systemui.scene.ui.composable.transitions.dreamToShadeTransition import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition @@ -55,7 +58,10 @@ val SceneContainerTransitions = transitions { // Scene transitions from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() } + from(Scenes.Dream, to = Scenes.Bouncer) { dreamToBouncerTransition() } + from(Scenes.Dream, to = Scenes.Communal) { dreamToCommunalTransition() } from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() } + from(Scenes.Dream, to = Scenes.Shade) { dreamToShadeTransition() } from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() } from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() } from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToBouncerTransition.kt new file mode 100644 index 000000000000..8cb89f91a831 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToBouncerTransition.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.scene.ui.composable.transitions + +import com.android.compose.animation.scene.TransitionBuilder + +fun TransitionBuilder.dreamToBouncerTransition() { + toBouncerTransition() +} 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 new file mode 100644 index 000000000000..93c10b6224ab --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToCommunalTransition.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.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) } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToShadeTransition.kt new file mode 100644 index 000000000000..e35aaae19e29 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToShadeTransition.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.scene.ui.composable.transitions + +import com.android.compose.animation.scene.TransitionBuilder + +fun TransitionBuilder.dreamToShadeTransition(durationScale: Double = 1.0) { + toShadeTransition(durationScale = durationScale) +} 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 71fa6c9e567a..ce7a85b19fb4 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 @@ -19,11 +19,8 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance -import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings @@ -31,24 +28,17 @@ import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.goneToSplitShadeTransition( - durationScale: Double = 1.0, -) { +fun TransitionBuilder.goneToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) - distance = - object : UserActionDistance { - override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, - orientation: Orientation, - ): Float { - return fromSceneSize.height.toFloat() * 2 / 3f - } - } + distance = UserActionDistance { fromContent, _, _ -> + val fromContentSize = checkNotNull(fromContent.targetSize()) + fromContentSize.height.toFloat() * 2 / 3f + } fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) } 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 1486ea7d53b5..1f7a7380bbc6 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 @@ -19,35 +19,25 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance -import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.lockscreenToSplitShadeTransition( - durationScale: Double = 1.0, -) { +fun TransitionBuilder.lockscreenToSplitShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) - distance = - object : UserActionDistance { - override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, - orientation: Orientation, - ): Float { - return fromSceneSize.height.toFloat() * 2 / 3f - } - } + distance = UserActionDistance { fromContent, _, _ -> + val fromContentSize = checkNotNull(fromContent.targetSize()) + fromContentSize.height.toFloat() * 2 / 3f + } fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt index 8cd357ef92a5..ba1972fbcc5a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt @@ -1,46 +1,36 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance -import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.shadeToQuickSettingsTransition( - durationScale: Double = 1.0, -) { +fun TransitionBuilder.shadeToQuickSettingsTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) - distance = - object : UserActionDistance { - override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, - orientation: Orientation, - ): Float { - val distance = - Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y - ?: return 0f - return fromSceneSize.height - distance - } - } + distance = UserActionDistance { fromContent, _, _ -> + val distance = + Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y + ?: return@UserActionDistance 0f + val fromContentSize = checkNotNull(fromContent.targetSize()) + fromContentSize.height - distance + } translate(Notifications.Elements.NotificationScrim, Edge.Bottom) timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) } translate( ShadeHeader.Elements.CollapsedContentStart, - y = ShadeHeader.Dimensions.CollapsedHeight + y = ShadeHeader.Dimensions.CollapsedHeight, ) translate(ShadeHeader.Elements.CollapsedContentEnd, y = ShadeHeader.Dimensions.CollapsedHeight) translate( ShadeHeader.Elements.ExpandedContent, - y = -(ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight) + y = -(ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight), ) translate(ShadeHeader.Elements.ShadeCarrierGroup, y = -ShadeHeader.Dimensions.CollapsedHeight) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt index de76f708c1c7..d35537a74c85 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToBouncerTransition.kt @@ -28,8 +28,9 @@ private const val TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f fun TransitionBuilder.toBouncerTransition() { spec = tween(durationMillis = 500) - distance = UserActionDistance { fromSceneSize, _ -> - fromSceneSize.height * TO_BOUNCER_SWIPE_DISTANCE_FRACTION + distance = UserActionDistance { fromContent, _, _ -> + val fromContentSize = checkNotNull(fromContent.targetSize()) + fromContentSize.height * TO_BOUNCER_SWIPE_DISTANCE_FRACTION } translate(Bouncer.Elements.Content, y = 300.dp) 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 55fa6ad94ed3..e78bc6afcc4f 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 @@ -33,7 +33,10 @@ fun TransitionBuilder.toQuickSettingsShadeTransition(durationScale: Double = 1.0 stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) - distance = UserActionDistance { fromSceneSize, _ -> fromSceneSize.height.toFloat() * 2 / 3f } + distance = UserActionDistance { fromContent, _, _ -> + val fromContentSize = checkNotNull(fromContent.targetSize()) + fromContentSize.height.toFloat() * 2 / 3f + } translate(OverlayShade.Elements.Panel, Edge.Top) 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 b677dff2dcf9..bfae4897dc68 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 @@ -19,12 +19,9 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder import com.android.compose.animation.scene.UserActionDistance -import com.android.compose.animation.scene.UserActionDistanceScope import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.ui.composable.QuickSettings @@ -33,24 +30,16 @@ import com.android.systemui.shade.ui.composable.Shade import com.android.systemui.shade.ui.composable.ShadeHeader import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.toShadeTransition( - durationScale: Double = 1.0, -) { +fun TransitionBuilder.toShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold, ) - distance = - object : UserActionDistance { - override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, - orientation: Orientation, - ): Float { - return Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y ?: 0f - } - } + distance = UserActionDistance { _, _, _ -> + Notifications.Elements.NotificationScrim.targetOffset(Scenes.Shade)?.y ?: 0f + } fractionRange(start = .58f) { fade(ShadeHeader.Elements.Clock) 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 dbf7d7b29834..d3ddb5003469 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 @@ -637,8 +637,8 @@ sealed class UserActionResult( fun interface UserActionDistance { /** - * Return the **absolute** distance of the user action given the size of the scene we are - * animating from and the [orientation]. + * Return the **absolute** distance of the user action when going from [fromContent] to + * [toContent] in the given [orientation]. * * Note: This function will be called for each drag event until it returns a value > 0f. This * for instance allows you to return 0f or a negative value until the first layout pass of a @@ -646,7 +646,8 @@ fun interface UserActionDistance { * transitioning to when computing this absolute distance. */ fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, + fromContent: ContentKey, + toContent: ContentKey, orientation: Orientation, ): Float } @@ -656,7 +657,8 @@ interface UserActionDistanceScope : Density, ElementStateScope /** The user action has a fixed [absoluteDistance]. */ class FixedDistance(private val distance: Dp) : UserActionDistance { override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, + fromContent: ContentKey, + toContent: ContentKey, orientation: Orientation, ): Float = distance.toPx() } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 7eb5a3f8b362..09156d5c1fba 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -267,11 +267,12 @@ internal class MutableSceneTransitionLayoutStateImpl( private set /** - * The flattened list of [SharedElementTransformation] within all the transitions in + * The flattened list of [SharedElementTransformation.Factory] within all the transitions in * [transitionStates]. */ - private val transformationsWithElevation: List<SharedElementTransformation> by derivedStateOf { - transformationsWithElevation(transitionStates) + private val transformationFactoriesWithElevation: + List<SharedElementTransformation.Factory> by derivedStateOf { + transformationFactoriesWithElevation(transitionStates) } override val currentScene: SceneKey @@ -692,22 +693,23 @@ internal class MutableSceneTransitionLayoutStateImpl( animate() } - private fun transformationsWithElevation( + private fun transformationFactoriesWithElevation( transitionStates: List<TransitionState> - ): List<SharedElementTransformation> { + ): List<SharedElementTransformation.Factory> { return buildList { transitionStates.fastForEach { state -> if (state !is TransitionState.Transition) { return@fastForEach } - state.transformationSpec.transformations.fastForEach { transformationWithRange -> - val transformation = transformationWithRange.transformation + state.transformationSpec.transformationMatchers.fastForEach { transformationMatcher + -> + val factory = transformationMatcher.factory if ( - transformation is SharedElementTransformation && - transformation.elevateInContent != null + factory is SharedElementTransformation.Factory && + factory.elevateInContent != null ) { - add(transformation) + add(factory) } } } @@ -722,10 +724,10 @@ internal class MutableSceneTransitionLayoutStateImpl( * necessary, for performance. */ internal fun isElevationPossible(content: ContentKey, element: ElementKey?): Boolean { - if (transformationsWithElevation.isEmpty()) return false - return transformationsWithElevation.fastAny { transformation -> - transformation.elevateInContent == content && - (element == null || transformation.matcher.matches(element, content)) + if (transformationFactoriesWithElevation.isEmpty()) return false + return transformationFactoriesWithElevation.fastAny { factory -> + factory.elevateInContent == content && + (element == null || factory.matcher.matches(element, content)) } } } 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 569593c3eb59..8df3f2d932b3 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 @@ -26,17 +26,9 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastForEach import com.android.compose.animation.scene.content.state.TransitionState -import com.android.compose.animation.scene.transformation.CustomAlphaTransformation -import com.android.compose.animation.scene.transformation.CustomOffsetTransformation -import com.android.compose.animation.scene.transformation.CustomScaleTransformation -import com.android.compose.animation.scene.transformation.CustomSizeTransformation -import com.android.compose.animation.scene.transformation.InterpolatedAlphaTransformation -import com.android.compose.animation.scene.transformation.InterpolatedOffsetTransformation -import com.android.compose.animation.scene.transformation.InterpolatedScaleTransformation -import com.android.compose.animation.scene.transformation.InterpolatedSizeTransformation import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.SharedElementTransformation -import com.android.compose.animation.scene.transformation.Transformation +import com.android.compose.animation.scene.transformation.TransformationMatcher import com.android.compose.animation.scene.transformation.TransformationWithRange /** The transitions configuration of a [SceneTransitionLayout]. */ @@ -169,7 +161,7 @@ internal constructor( } /** The definition of a transition between [from] and [to]. */ -interface TransitionSpec { +internal interface TransitionSpec { /** The key of this [TransitionSpec]. */ val key: TransitionKey? @@ -209,7 +201,7 @@ interface TransitionSpec { fun previewTransformationSpec(transition: TransitionState.Transition): TransformationSpec? } -interface TransformationSpec { +internal interface TransformationSpec { /** * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when * the transition is triggered (i.e. it is not gesture-based). @@ -232,8 +224,8 @@ interface TransformationSpec { */ val distance: UserActionDistance? - /** The list of [Transformation] applied to elements during this transition. */ - val transformations: List<TransformationWithRange<*>> + /** The list of [TransformationMatcher] applied to elements during this transformation. */ + val transformationMatchers: List<TransformationMatcher> companion object { internal val Empty = @@ -241,7 +233,7 @@ interface TransformationSpec { progressSpec = snap(), swipeSpec = null, distance = null, - transformations = emptyList(), + transformationMatchers = emptyList(), ) internal val EmptyProvider = { _: TransitionState.Transition -> Empty } } @@ -272,7 +264,14 @@ internal class TransitionSpecImpl( progressSpec = reverse.progressSpec, swipeSpec = reverse.swipeSpec, distance = reverse.distance, - transformations = reverse.transformations.map { it.reversed() }, + transformationMatchers = + reverse.transformationMatchers.map { + TransformationMatcher( + matcher = it.matcher, + factory = it.factory, + range = it.range?.reversed(), + ) + }, ) }, ) @@ -288,7 +287,7 @@ internal class TransitionSpecImpl( } /** The definition of the overscroll behavior of the [content]. */ -interface OverscrollSpec { +internal interface OverscrollSpec { /** The scene we are over scrolling. */ val content: ContentKey @@ -325,7 +324,7 @@ internal class TransformationSpecImpl( override val progressSpec: AnimationSpec<Float>, override val swipeSpec: SpringSpec<Float>?, override val distance: UserActionDistance?, - override val transformations: List<TransformationWithRange<*>>, + override val transformationMatchers: List<TransformationMatcher>, ) : TransformationSpec { private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>() @@ -335,7 +334,7 @@ internal class TransformationSpecImpl( .getOrPut(content) { computeTransformations(element, content) } } - /** Filter [transformations] to compute the [ElementTransformations] of [element]. */ + /** Filter [transformationMatchers] to compute the [ElementTransformations] of [element]. */ private fun computeTransformations( element: ElementKey, content: ContentKey, @@ -346,46 +345,55 @@ internal class TransformationSpecImpl( var drawScale: TransformationWithRange<PropertyTransformation<Scale>>? = null var alpha: TransformationWithRange<PropertyTransformation<Float>>? = null - transformations.fastForEach { transformationWithRange -> - val transformation = transformationWithRange.transformation - if (!transformation.matcher.matches(element, content)) { + transformationMatchers.fastForEach { transformationMatcher -> + if (!transformationMatcher.matcher.matches(element, content)) { return@fastForEach } - when (transformation) { - is SharedElementTransformation -> { - throwIfNotNull(shared, element, name = "shared") - shared = - transformationWithRange - as TransformationWithRange<SharedElementTransformation> + val transformation = transformationMatcher.factory.create() + val property = + when (transformation) { + is SharedElementTransformation -> { + throwIfNotNull(shared, element, name = "shared") + shared = + TransformationWithRange(transformation, transformationMatcher.range) + return@fastForEach + } + is PropertyTransformation<*> -> transformation.property } - is InterpolatedOffsetTransformation, - is CustomOffsetTransformation -> { + + when (property) { + is PropertyTransformation.Property.Offset -> { throwIfNotNull(offset, element, name = "offset") offset = - transformationWithRange - as TransformationWithRange<PropertyTransformation<Offset>> + TransformationWithRange( + transformation as PropertyTransformation<Offset>, + transformationMatcher.range, + ) } - is InterpolatedSizeTransformation, - is CustomSizeTransformation -> { + is PropertyTransformation.Property.Size -> { throwIfNotNull(size, element, name = "size") size = - transformationWithRange - as TransformationWithRange<PropertyTransformation<IntSize>> + TransformationWithRange( + transformation as PropertyTransformation<IntSize>, + transformationMatcher.range, + ) } - is InterpolatedScaleTransformation, - is CustomScaleTransformation -> { + is PropertyTransformation.Property.Scale -> { throwIfNotNull(drawScale, element, name = "drawScale") drawScale = - transformationWithRange - as TransformationWithRange<PropertyTransformation<Scale>> + TransformationWithRange( + transformation as PropertyTransformation<Scale>, + transformationMatcher.range, + ) } - is InterpolatedAlphaTransformation, - is CustomAlphaTransformation -> { + is PropertyTransformation.Property.Alpha -> { throwIfNotNull(alpha, element, name = "alpha") alpha = - transformationWithRange - as TransformationWithRange<PropertyTransformation<Float>> + TransformationWithRange( + transformation as PropertyTransformation<Float>, + transformationMatcher.range, + ) } } } 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 f0043e1e89b0..dbfeb5cd0168 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,7 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified import kotlin.math.absoluteValue @@ -66,8 +65,9 @@ internal fun createSwipeAnimation( val absoluteDistance = with(animation.contentTransition.transformationSpec.distance ?: DefaultSwipeDistance) { layoutImpl.userActionDistanceScope.absoluteDistance( - layoutImpl.content(animation.fromContent).targetSize, - orientation, + fromContent = animation.fromContent, + toContent = animation.toContent, + orientation = orientation, ) } @@ -475,12 +475,14 @@ internal class SwipeAnimation<T : ContentKey>( private object DefaultSwipeDistance : UserActionDistance { override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, + fromContent: ContentKey, + toContent: ContentKey, orientation: Orientation, ): Float { + val fromContentSize = checkNotNull(fromContent.targetSize()) return when (orientation) { - Orientation.Horizontal -> fromSceneSize.width - Orientation.Vertical -> fromSceneSize.height + Orientation.Horizontal -> fromContentSize.width + Orientation.Vertical -> fromContentSize.height }.toFloat() } } 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 1fdfca9d9509..48f08a7086d6 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 @@ -26,7 +26,7 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.content.state.TransitionState -import com.android.compose.animation.scene.transformation.CustomPropertyTransformation +import com.android.compose.animation.scene.transformation.Transformation import kotlin.math.tanh /** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */ @@ -76,7 +76,7 @@ interface SceneTransitionsBuilder { preview: (TransitionBuilder.() -> Unit)? = null, reversePreview: (TransitionBuilder.() -> Unit)? = null, builder: TransitionBuilder.() -> Unit = {}, - ): TransitionSpec + ) /** * Define the animation to be played when transitioning [from] the specified content. For the @@ -102,7 +102,7 @@ interface SceneTransitionsBuilder { preview: (TransitionBuilder.() -> Unit)? = null, reversePreview: (TransitionBuilder.() -> Unit)? = null, builder: TransitionBuilder.() -> Unit = {}, - ): TransitionSpec + ) /** * Define the animation to be played when the [content] is overscrolled in the given @@ -115,13 +115,13 @@ interface SceneTransitionsBuilder { content: ContentKey, orientation: Orientation, builder: OverscrollBuilder.() -> Unit, - ): OverscrollSpec + ) /** * Prevents overscroll the [content] in the given [orientation], allowing ancestors to * eventually consume the remaining gesture. */ - fun overscrollDisabled(content: ContentKey, orientation: Orientation): OverscrollSpec + fun overscrollDisabled(content: ContentKey, orientation: Orientation) } interface BaseTransitionBuilder : PropertyTransformationBuilder { @@ -529,15 +529,8 @@ interface PropertyTransformationBuilder { anchorHeight: Boolean = true, ) - /** - * Apply a [CustomPropertyTransformation] to one or more elements. - * - * @see com.android.compose.animation.scene.transformation.CustomSizeTransformation - * @see com.android.compose.animation.scene.transformation.CustomOffsetTransformation - * @see com.android.compose.animation.scene.transformation.CustomAlphaTransformation - * @see com.android.compose.animation.scene.transformation.CustomScaleTransformation - */ - fun transformation(transformation: CustomPropertyTransformation<*>) + /** Apply a [transformation] to the element(s) matching [matcher]. */ + fun transformation(matcher: ElementMatcher, transformation: Transformation.Factory) } /** This converter lets you change a linear progress into a function of your choice. */ 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 79f8cd47d07d..6742b3200ac4 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 @@ -30,7 +30,6 @@ import androidx.compose.ui.unit.Dp import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.transformation.AnchoredSize import com.android.compose.animation.scene.transformation.AnchoredTranslate -import com.android.compose.animation.scene.transformation.CustomPropertyTransformation import com.android.compose.animation.scene.transformation.DrawScale import com.android.compose.animation.scene.transformation.EdgeTranslate import com.android.compose.animation.scene.transformation.Fade @@ -38,8 +37,8 @@ import com.android.compose.animation.scene.transformation.OverscrollTranslate import com.android.compose.animation.scene.transformation.ScaleSize import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.animation.scene.transformation.Transformation +import com.android.compose.animation.scene.transformation.TransformationMatcher import com.android.compose.animation.scene.transformation.TransformationRange -import com.android.compose.animation.scene.transformation.TransformationWithRange import com.android.compose.animation.scene.transformation.Translate internal fun transitionsImpl(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions { @@ -67,8 +66,8 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { preview: (TransitionBuilder.() -> Unit)?, reversePreview: (TransitionBuilder.() -> Unit)?, builder: TransitionBuilder.() -> Unit, - ): TransitionSpec { - return transition(from = null, to = to, key = key, preview, reversePreview, builder) + ) { + transition(from = null, to = to, key = key, preview, reversePreview, builder) } override fun from( @@ -78,25 +77,25 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { preview: (TransitionBuilder.() -> Unit)?, reversePreview: (TransitionBuilder.() -> Unit)?, builder: TransitionBuilder.() -> Unit, - ): TransitionSpec { - return transition(from = from, to = to, key = key, preview, reversePreview, builder) + ) { + transition(from = from, to = to, key = key, preview, reversePreview, builder) } override fun overscroll( content: ContentKey, orientation: Orientation, builder: OverscrollBuilder.() -> Unit, - ): OverscrollSpec { + ) { val impl = OverscrollBuilderImpl().apply(builder) - check(impl.transformations.isNotEmpty()) { + check(impl.transformationMatchers.isNotEmpty()) { "This method does not allow empty transformations. " + "Use overscrollDisabled($content, $orientation) instead." } - return overscrollSpec(content, orientation, impl) + overscrollSpec(content, orientation, impl) } - override fun overscrollDisabled(content: ContentKey, orientation: Orientation): OverscrollSpec { - return overscrollSpec(content, orientation, OverscrollBuilderImpl()) + override fun overscrollDisabled(content: ContentKey, orientation: Orientation) { + overscrollSpec(content, orientation, OverscrollBuilderImpl()) } private fun overscrollSpec( @@ -113,7 +112,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { progressSpec = snap(), swipeSpec = null, distance = impl.distance, - transformations = impl.transformations, + transformationMatchers = impl.transformationMatchers, ), progressConverter = impl.progressConverter, ) @@ -138,7 +137,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { progressSpec = impl.spec, swipeSpec = impl.swipeSpec, distance = impl.distance, - transformations = impl.transformations, + transformationMatchers = impl.transformationMatchers, ) } @@ -158,7 +157,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { } internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { - val transformations = mutableListOf<TransformationWithRange<*>>() + val transformationMatchers = mutableListOf<TransformationMatcher>() private var range: TransformationRange? = null protected var reversed = false override var distance: UserActionDistance? = null @@ -174,23 +173,31 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { range = null } - protected fun addTransformation(transformation: Transformation) { - val transformationWithRange = TransformationWithRange(transformation, range) - transformations.add( - if (reversed) { - transformationWithRange.reversed() - } else { - transformationWithRange - } + protected fun addTransformation( + matcher: ElementMatcher, + transformation: Transformation.Factory, + ) { + transformationMatchers.add( + TransformationMatcher( + matcher, + transformation, + range?.let { range -> + if (reversed) { + range.reversed() + } else { + range + } + }, + ) ) } override fun fade(matcher: ElementMatcher) { - addTransformation(Fade(matcher)) + addTransformation(matcher, Fade.Factory) } override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) { - addTransformation(Translate(matcher, x, y)) + addTransformation(matcher, Translate.Factory(x, y)) } override fun translate( @@ -198,19 +205,19 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { edge: Edge, startsOutsideLayoutBounds: Boolean, ) { - addTransformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds)) + addTransformation(matcher, EdgeTranslate.Factory(edge, startsOutsideLayoutBounds)) } override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) { - addTransformation(AnchoredTranslate(matcher, anchor)) + addTransformation(matcher, AnchoredTranslate.Factory(anchor)) } override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) { - addTransformation(ScaleSize(matcher, width, height)) + addTransformation(matcher, ScaleSize.Factory(width, height)) } override fun scaleDraw(matcher: ElementMatcher, scaleX: Float, scaleY: Float, pivot: Offset) { - addTransformation(DrawScale(matcher, scaleX, scaleY, pivot)) + addTransformation(matcher, DrawScale.Factory(scaleX, scaleY, pivot)) } override fun anchoredSize( @@ -219,12 +226,12 @@ internal abstract class BaseTransitionBuilderImpl : BaseTransitionBuilder { anchorWidth: Boolean, anchorHeight: Boolean, ) { - addTransformation(AnchoredSize(matcher, anchor, anchorWidth, anchorHeight)) + addTransformation(matcher, AnchoredSize.Factory(anchor, anchorWidth, anchorHeight)) } - override fun transformation(transformation: CustomPropertyTransformation<*>) { + override fun transformation(matcher: ElementMatcher, transformation: Transformation.Factory) { check(range == null) { "Custom transformations can not be applied inside a range" } - addTransformation(transformation) + addTransformation(matcher, transformation) } } @@ -263,7 +270,10 @@ internal class TransitionBuilderImpl(override val transition: TransitionState.Tr "(${transition.toContent.debugName})" } - addTransformation(SharedElementTransformation(matcher, enabled, elevateInContent)) + addTransformation( + matcher, + SharedElementTransformation.Factory(matcher, enabled, elevateInContent), + ) } override fun timestampRange( @@ -294,6 +304,6 @@ internal open class OverscrollBuilderImpl : BaseTransitionBuilderImpl(), Overscr x: OverscrollScope.() -> Float, y: OverscrollScope.() -> Float, ) { - addTransformation(OverscrollTranslate(matcher, x, y)) + addTransformation(matcher, OverscrollTranslate.Factory(x, y)) } } 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 38b1aaa7558d..33f015fe49d9 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 @@ -401,7 +401,7 @@ sealed interface TransitionState { else -> null } ?: return true - return specForCurrentScene.transformationSpec.transformations.isNotEmpty() + return specForCurrentScene.transformationSpec.transformationMatchers.isNotEmpty() } internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float { 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 new file mode 100644 index 000000000000..bfb5ca733d90 --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/reveal/ContainerReveal.kt @@ -0,0 +1,289 @@ +/* + * 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.reveal + +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.DeferredTargetAnimation +import androidx.compose.animation.core.ExperimentalAnimatableApi +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.spring +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastCoerceAtLeast +import androidx.compose.ui.util.fastCoerceAtMost +import com.android.compose.animation.scene.ContentKey +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.OverlayKey +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.TransitionBuilder +import com.android.compose.animation.scene.UserActionDistance +import com.android.compose.animation.scene.content.state.TransitionState +import com.android.compose.animation.scene.transformation.CustomPropertyTransformation +import com.android.compose.animation.scene.transformation.PropertyTransformation +import com.android.compose.animation.scene.transformation.PropertyTransformationScope +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope + +interface ContainerRevealHaptics { + /** + * Called when the reveal threshold is crossed while the user was dragging on screen. + * + * Important: This callback is called during layout and its implementation should therefore be + * very fast or posted to a different thread. + * + * @param revealed whether we go from hidden to revealed, i.e. whether the container size is + * going to jump from a smaller size to a bigger size. + */ + fun onRevealThresholdCrossed(revealed: Boolean) +} + +/** Animate the reveal of [container] by animating its size. */ +fun TransitionBuilder.verticalContainerReveal( + container: ElementKey, + haptics: ContainerRevealHaptics, +) { + // Make the swipe distance be exactly the target height of the container. + // TODO(b/376438969): Make sure that this works correctly when the target size of the element + // is changing during the transition (e.g. a notification was added). At the moment, the user + // action distance is only called until it returns a value > 0f, which is then cached. + distance = UserActionDistance { fromContent, toContent, _ -> + val targetSizeInFromContent = container.targetSize(fromContent) + val targetSizeInToContent = container.targetSize(toContent) + if (targetSizeInFromContent != null && targetSizeInToContent != null) { + error( + "verticalContainerReveal should not be used with shared elements, but " + + "${container.debugName} is in both ${fromContent.debugName} and " + + toContent.debugName + ) + } + + (targetSizeInToContent?.height ?: targetSizeInFromContent?.height)?.toFloat() ?: 0f + } + + // TODO(b/376438969): Improve the motion of this gesture using Motion Mechanics. + + // The min distance to swipe before triggering the reveal spring. + val distanceThreshold = 80.dp + + // The minimum height of the container. + val minHeight = 10.dp + + // The amount removed from the container width at 0% progress. + val widthDelta = 140.dp + + // The ratio at which the distance is tracked before reaching the threshold, e.g. if the user + // drags 60dp then the height will be 60dp * 0.25f = 15dp. + val trackingRatio = 0.25f + + // The max progress starting from which the container should always be visible, even if we are + // animating the container out. This is used so that we don't immediately fade out the container + // when triggering a one-off animation that hides it. + val alphaProgressThreshold = 0.05f + + // The spring animating the size of the container. + val sizeSpec = spring<Float>(stiffness = 380f, dampingRatio = 0.9f) + + // The spring animating the alpha of the container. + val alphaSpec = spring<Float>(stiffness = 1200f, dampingRatio = 0.99f) + + // The spring animating the progress when releasing the finger. + swipeSpec = + spring( + stiffness = Spring.StiffnessMediumLow, + dampingRatio = Spring.DampingRatioNoBouncy, + visibilityThreshold = 0.5f, + ) + + // Size transformation. + transformation(container) { + VerticalContainerRevealSizeTransformation( + haptics, + distanceThreshold, + trackingRatio, + minHeight, + widthDelta, + sizeSpec, + ) + } + + // Alpha transformation. + transformation(container) { + ContainerRevealAlphaTransformation(alphaSpec, alphaProgressThreshold) + } +} + +@OptIn(ExperimentalAnimatableApi::class) +private class VerticalContainerRevealSizeTransformation( + private val haptics: ContainerRevealHaptics, + private val distanceThreshold: Dp, + private val trackingRatio: Float, + private val minHeight: Dp, + private val widthDelta: Dp, + private val spec: FiniteAnimationSpec<Float>, +) : CustomPropertyTransformation<IntSize> { + override val property = PropertyTransformation.Property.Size + + private val widthAnimation = DeferredTargetAnimation(Float.VectorConverter) + private val heightAnimation = DeferredTargetAnimation(Float.VectorConverter) + + private var previousHasReachedThreshold: Boolean? = null + + override fun PropertyTransformationScope.transform( + content: ContentKey, + element: ElementKey, + transition: TransitionState.Transition, + transitionScope: CoroutineScope, + ): IntSize { + // The distance to go to 100%. Note that we don't use + // TransitionState.HasOverscrollProperties.absoluteDistance because the transition will not + // implement HasOverscrollProperties if the transition is triggered and not gesture based. + val idleSize = checkNotNull(element.targetSize(content)) + val userActionDistance = idleSize.height + val progress = + when ((transition as? TransitionState.HasOverscrollProperties)?.bouncingContent) { + null -> transition.progressTo(content) + content -> 1f + else -> 0f + } + val distance = (progress * userActionDistance).fastCoerceAtLeast(0f) + val threshold = distanceThreshold.toPx() + + // Width. + val widthDelta = widthDelta.toPx() + val width = + (idleSize.width - widthDelta + + animateSize( + size = widthDelta, + distance = distance, + threshold = threshold, + transitionScope = transitionScope, + animation = widthAnimation, + )) + .roundToInt() + + // Height. + val minHeight = minHeight.toPx() + val height = + ( + // 1) The minimum size of the container. + minHeight + + + // 2) The animated size between the minimum size and the threshold. + animateSize( + size = threshold - minHeight, + distance = distance, + threshold = threshold, + transitionScope = transitionScope, + animation = heightAnimation, + ) + + + // 3) The remaining height after the threshold, tracking the finger. + (distance - threshold).fastCoerceAtLeast(0f)) + .roundToInt() + .fastCoerceAtMost(idleSize.height) + + // Haptics. + val hasReachedThreshold = distance >= threshold + if ( + previousHasReachedThreshold != null && + hasReachedThreshold != previousHasReachedThreshold && + transition.isUserInputOngoing + ) { + haptics.onRevealThresholdCrossed(revealed = hasReachedThreshold) + } + previousHasReachedThreshold = hasReachedThreshold + + return IntSize(width = width, height = height) + } + + /** + * Animate a size up to [size], so that it is equal to 0f when distance is 0f and equal to + * [size] when `distance >= threshold`, taking the [trackingRatio] into account. + */ + @OptIn(ExperimentalAnimatableApi::class) + private fun animateSize( + size: Float, + distance: Float, + threshold: Float, + transitionScope: CoroutineScope, + animation: DeferredTargetAnimation<Float, AnimationVector1D>, + ): Float { + val trackingSize = distance.fastCoerceAtMost(threshold) / threshold * size * trackingRatio + val springTarget = + if (distance >= threshold) { + size * (1f - trackingRatio) + } else { + 0f + } + val springSize = animation.updateTarget(springTarget, transitionScope, spec) + return trackingSize + springSize + } +} + +@OptIn(ExperimentalAnimatableApi::class) +private class ContainerRevealAlphaTransformation( + private val spec: FiniteAnimationSpec<Float>, + private val progressThreshold: Float, +) : CustomPropertyTransformation<Float> { + override val property = PropertyTransformation.Property.Alpha + private val alphaAnimation = DeferredTargetAnimation(Float.VectorConverter) + + override fun PropertyTransformationScope.transform( + content: ContentKey, + element: ElementKey, + transition: TransitionState.Transition, + transitionScope: CoroutineScope, + ): Float { + return alphaAnimation.updateTarget(targetAlpha(transition, content), transitionScope, spec) + } + + private fun targetAlpha(transition: TransitionState.Transition, content: ContentKey): Float { + if (transition.isUserInputOngoing) { + if (transition !is TransitionState.HasOverscrollProperties) { + error( + "Unsupported transition driven by user input but that does not have " + + "overscroll properties: $transition" + ) + } + + val bouncingContent = transition.bouncingContent + return if (bouncingContent != null) { + if (bouncingContent == content) 1f else 0f + } else { + if (transition.progressTo(content) > 0f) 1f else 0f + } + } + + // The transition was committed (the user released their finger), so the alpha depends on + // whether we are animating towards the content (showing the container) or away from it + // (hiding the container). + val isShowingContainer = + when (content) { + is SceneKey -> transition.currentScene == content + is OverlayKey -> transition.currentOverlays.contains(content) + } + + return if (isShowingContainer || transition.progressTo(content) >= progressThreshold) { + 1f + } else { + 0f + } + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt index 85bb5336d574..6575068201d8 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt @@ -19,16 +19,17 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.content.state.TransitionState /** Anchor the size of an element to the size of another element. */ -internal class AnchoredSize( - override val matcher: ElementMatcher, +internal class AnchoredSize +private constructor( private val anchor: ElementKey, private val anchorWidth: Boolean, private val anchorHeight: Boolean, -) : InterpolatedSizeTransformation { +) : InterpolatedPropertyTransformation<IntSize> { + override val property = PropertyTransformation.Property.Size + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -59,4 +60,12 @@ internal class AnchoredSize( anchorSizeIn(transition.fromContent) } } + + class Factory( + private val anchor: ElementKey, + private val anchorWidth: Boolean, + private val anchorHeight: Boolean, + ) : Transformation.Factory { + override fun create(): Transformation = AnchoredSize(anchor, anchorWidth, anchorHeight) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt index 04cd68344252..890902b7ab67 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt @@ -19,14 +19,13 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.content.state.TransitionState /** Anchor the translation of an element to another element. */ -internal class AnchoredTranslate( - override val matcher: ElementMatcher, - private val anchor: ElementKey, -) : InterpolatedOffsetTransformation { +internal class AnchoredTranslate private constructor(private val anchor: ElementKey) : + InterpolatedPropertyTransformation<Offset> { + override val property = PropertyTransformation.Property.Offset + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -56,6 +55,10 @@ internal class AnchoredTranslate( Offset(idleValue.x + offset.x, idleValue.y + offset.y) } } + + class Factory(private val anchor: ElementKey) : Transformation.Factory { + override fun create(): Transformation = AnchoredTranslate(anchor) + } } internal fun throwMissingAnchorException( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt index 45d6d4037c49..347f1c325058 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt @@ -19,7 +19,6 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scale import com.android.compose.animation.scene.content.state.TransitionState @@ -27,12 +26,14 @@ import com.android.compose.animation.scene.content.state.TransitionState * Scales the draw size of an element. Note this will only scale the draw inside of an element, * therefore it won't impact layout of elements around it. */ -internal class DrawScale( - override val matcher: ElementMatcher, +internal class DrawScale +private constructor( private val scaleX: Float, private val scaleY: Float, - private val pivot: Offset = Offset.Unspecified, -) : InterpolatedScaleTransformation { + private val pivot: Offset, +) : InterpolatedPropertyTransformation<Scale> { + override val property = PropertyTransformation.Property.Scale + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -41,4 +42,9 @@ internal class DrawScale( ): Scale { return Scale(scaleX, scaleY, pivot) } + + class Factory(private val scaleX: Float, private val scaleY: Float, private val pivot: Offset) : + Transformation.Factory { + override fun create(): Transformation = DrawScale(scaleX, scaleY, pivot) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt index 21d66d784e2d..f8e6dc09ce75 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt @@ -20,15 +20,14 @@ import androidx.compose.ui.geometry.Offset import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.content.state.TransitionState /** Translate an element from an edge of the layout. */ -internal class EdgeTranslate( - override val matcher: ElementMatcher, - private val edge: Edge, - private val startsOutsideLayoutBounds: Boolean = true, -) : InterpolatedOffsetTransformation { +internal class EdgeTranslate +private constructor(private val edge: Edge, private val startsOutsideLayoutBounds: Boolean) : + InterpolatedPropertyTransformation<Offset> { + override val property = PropertyTransformation.Property.Offset + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -67,4 +66,9 @@ internal class EdgeTranslate( } } } + + class Factory(private val edge: Edge, private val startsOutsideLayoutBounds: Boolean) : + Transformation.Factory { + override fun create(): Transformation = EdgeTranslate(edge, startsOutsideLayoutBounds) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt index d942273ab9ab..d92419ef368d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt @@ -18,11 +18,12 @@ package com.android.compose.animation.scene.transformation import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.content.state.TransitionState /** Fade an element in or out. */ -internal class Fade(override val matcher: ElementMatcher) : InterpolatedAlphaTransformation { +internal object Fade : InterpolatedPropertyTransformation<Float> { + override val property = PropertyTransformation.Property.Alpha + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -33,4 +34,8 @@ internal class Fade(override val matcher: ElementMatcher) : InterpolatedAlphaTra // fading out, which is `0` in both cases. return 0f } + + object Factory : Transformation.Factory { + override fun create(): Transformation = Fade + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt index 5f3cdab3c572..5d31fd9ca196 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt @@ -19,7 +19,6 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.content.state.TransitionState import kotlin.math.roundToInt @@ -27,11 +26,10 @@ import kotlin.math.roundToInt * Scales the size of an element. Note that this makes the element resize every frame and will * therefore impact the layout of other elements. */ -internal class ScaleSize( - override val matcher: ElementMatcher, - private val width: Float = 1f, - private val height: Float = 1f, -) : InterpolatedSizeTransformation { +internal class ScaleSize private constructor(private val width: Float, private val height: Float) : + InterpolatedPropertyTransformation<IntSize> { + override val property = PropertyTransformation.Property.Size + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -43,4 +41,9 @@ internal class ScaleSize( height = (idleValue.height * height).roundToInt(), ) } + + class Factory(private val width: Float = 1f, private val height: Float = 1f) : + Transformation.Factory { + override fun create(): Transformation = ScaleSize(width, height) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index d5143d729f2e..e0b42189854a 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -35,26 +35,59 @@ import kotlinx.coroutines.CoroutineScope /** A transformation applied to one or more elements during a transition. */ sealed interface Transformation { - /** - * The matcher that should match the element(s) to which this transformation should be applied. - */ - val matcher: ElementMatcher + fun interface Factory { + fun create(): Transformation + } } -internal class SharedElementTransformation( - override val matcher: ElementMatcher, +// Important: SharedElementTransformation must be a data class because we check that we don't +// provide 2 different transformations for the same element in Element.kt +internal data class SharedElementTransformation( internal val enabled: Boolean, internal val elevateInContent: ContentKey?, -) : Transformation +) : Transformation { + class Factory( + internal val matcher: ElementMatcher, + internal val enabled: Boolean, + internal val elevateInContent: ContentKey?, + ) : Transformation.Factory { + override fun create(): Transformation { + return SharedElementTransformation(enabled, elevateInContent) + } + } +} + +/** + * A transformation that changes the value of an element [Property], like its [size][Property.Size] + * or [offset][Property.Offset]. + */ +sealed interface PropertyTransformation<T> : Transformation { + /** The property to which this transformation is applied. */ + val property: Property<T> + + sealed class Property<T> { + /** The size of an element. */ + data object Size : Property<IntSize>() + + /** The offset (position) of an element. */ + data object Offset : Property<androidx.compose.ui.geometry.Offset>() + + /** The alpha of an element. */ + data object Alpha : Property<Float>() -/** A transformation that changes the value of an element property, like its size or offset. */ -sealed interface PropertyTransformation<T> : Transformation + /** + * The drawing scale of an element. Animating the scale does not have any effect on the + * layout. + */ + data object Scale : Property<com.android.compose.animation.scene.Scale>() + } +} /** * A transformation to a target/transformed value that is automatically interpolated using the * transition progress and transformation range. */ -sealed interface InterpolatedPropertyTransformation<T> : PropertyTransformation<T> { +interface InterpolatedPropertyTransformation<T> : PropertyTransformation<T> { /** * Return the transformed value for the given property, i.e.: * - the value at progress = 0% for elements that are entering the layout (i.e. elements in the @@ -73,19 +106,7 @@ sealed interface InterpolatedPropertyTransformation<T> : PropertyTransformation< ): T } -/** An [InterpolatedPropertyTransformation] applied to the size of one or more elements. */ -interface InterpolatedSizeTransformation : InterpolatedPropertyTransformation<IntSize> - -/** An [InterpolatedPropertyTransformation] applied to the offset of one or more elements. */ -interface InterpolatedOffsetTransformation : InterpolatedPropertyTransformation<Offset> - -/** An [InterpolatedPropertyTransformation] applied to the alpha of one or more elements. */ -interface InterpolatedAlphaTransformation : InterpolatedPropertyTransformation<Float> - -/** An [InterpolatedPropertyTransformation] applied to the scale of one or more elements. */ -interface InterpolatedScaleTransformation : InterpolatedPropertyTransformation<Scale> - -sealed interface CustomPropertyTransformation<T> : PropertyTransformation<T> { +interface CustomPropertyTransformation<T> : PropertyTransformation<T> { /** * Return the value that the property should have in the current frame for the given [content] * and [element]. @@ -105,25 +126,20 @@ sealed interface CustomPropertyTransformation<T> : PropertyTransformation<T> { ): T } -/** A [CustomPropertyTransformation] applied to the size of one or more elements. */ -interface CustomSizeTransformation : CustomPropertyTransformation<IntSize> - -/** A [CustomPropertyTransformation] applied to the offset of one or more elements. */ -interface CustomOffsetTransformation : CustomPropertyTransformation<Offset> - -/** A [CustomPropertyTransformation] applied to the alpha of one or more elements. */ -interface CustomAlphaTransformation : CustomPropertyTransformation<Float> - -/** A [CustomPropertyTransformation] applied to the scale of one or more elements. */ -interface CustomScaleTransformation : CustomPropertyTransformation<Scale> - interface PropertyTransformationScope : Density, ElementStateScope { /** The current [direction][LayoutDirection] of the layout. */ val layoutDirection: LayoutDirection } +/** Defines the transformation-type to be applied to all elements matching [matcher]. */ +internal class TransformationMatcher( + val matcher: ElementMatcher, + val factory: Transformation.Factory, + val range: TransformationRange?, +) + /** A pair consisting of a [transformation] and optional [range]. */ -class TransformationWithRange<out T : Transformation>( +internal data class TransformationWithRange<out T : Transformation>( val transformation: T, val range: TransformationRange?, ) { @@ -135,7 +151,7 @@ class TransformationWithRange<out T : Transformation>( } /** The progress-based range of a [PropertyTransformation]. */ -data class TransformationRange(val start: Float, val end: Float, val easing: Easing) { +internal data class TransformationRange(val start: Float, val end: Float, val easing: Easing) { constructor( start: Float? = null, end: Float? = null, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index d756c86f680c..2f4d5bff8b41 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -19,18 +19,15 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.OverscrollScope import com.android.compose.animation.scene.content.state.TransitionState -internal class Translate( - override val matcher: ElementMatcher, - private val x: Dp = 0.dp, - private val y: Dp = 0.dp, -) : InterpolatedOffsetTransformation { +internal class Translate private constructor(private val x: Dp, private val y: Dp) : + InterpolatedPropertyTransformation<Offset> { + override val property = PropertyTransformation.Property.Offset + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -39,13 +36,19 @@ internal class Translate( ): Offset { return Offset(idleValue.x + x.toPx(), idleValue.y + y.toPx()) } + + class Factory(private val x: Dp, private val y: Dp) : Transformation.Factory { + override fun create(): Transformation = Translate(x, y) + } } -internal class OverscrollTranslate( - override val matcher: ElementMatcher, - val x: OverscrollScope.() -> Float = { 0f }, - val y: OverscrollScope.() -> Float = { 0f }, -) : InterpolatedOffsetTransformation { +internal class OverscrollTranslate +private constructor( + private val x: OverscrollScope.() -> Float, + private val y: OverscrollScope.() -> Float, +) : InterpolatedPropertyTransformation<Offset> { + override val property = PropertyTransformation.Property.Offset + private val cachedOverscrollScope = CachedOverscrollScope() override fun PropertyTransformationScope.transform( @@ -63,6 +66,13 @@ internal class OverscrollTranslate( return Offset(x = value.x + overscrollScope.x(), y = value.y + overscrollScope.y()) } + + class Factory( + private val x: OverscrollScope.() -> Float, + private val y: OverscrollScope.() -> Float, + ) : Transformation.Factory { + override fun create(): Transformation = OverscrollTranslate(x, y) + } } /** 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 61e9bb0db0f7..10057b280d28 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 @@ -644,7 +644,7 @@ class DraggableHandlerTest { mutableUserActionsA = emptyMap() mutableUserActionsB = emptyMap() - // start accelaratedScroll and scroll over to B -> null + // start acceleratedScroll and scroll over to B -> null val dragController2 = onDragStartedImmediately() dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f) dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f), expectedConsumed = 0f) @@ -1530,7 +1530,7 @@ class DraggableHandlerTest { fun interceptingTransitionKeepsDistance() = runGestureTest { var swipeDistance = 75f layoutState.transitions = transitions { - from(SceneA, to = SceneB) { distance = UserActionDistance { _, _ -> swipeDistance } } + from(SceneA, to = SceneB) { distance = UserActionDistance { _, _, _ -> swipeDistance } } } // Start transition. diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 57ad81ebf7f0..397ca0eb396a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -153,7 +153,7 @@ class SceneTransitionLayoutStateTest { // Default transition from A to B. assertThat(state.setTargetScene(SceneB, animationScope = this)).isNotNull() - assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1) + assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(1) // Go back to A. state.setTargetScene(SceneA, animationScope = this) @@ -166,7 +166,7 @@ class SceneTransitionLayoutStateTest { state.setTargetScene(SceneB, animationScope = this, transitionKey = transitionkey) ) .isNotNull() - assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2) + assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(2) } @Test 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 97a96a4333cb..b3a3261122a8 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 @@ -55,7 +55,6 @@ import androidx.compose.ui.test.swipeRight import androidx.compose.ui.test.swipeUp import androidx.compose.ui.test.swipeWithVelocity import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -672,12 +671,12 @@ class SwipeToSceneTest { } assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue() - assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1) + assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(1) // Move the pointer up to swipe to scene B using the new transition. rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) } assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue() - assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2) + assertThat(state.currentTransition?.transformationSpec?.transformationMatchers).hasSize(2) } @Test @@ -685,7 +684,8 @@ class SwipeToSceneTest { val swipeDistance = object : UserActionDistance { override fun UserActionDistanceScope.absoluteDistance( - fromSceneSize: IntSize, + fromContent: ContentKey, + toContent: ContentKey, orientation: Orientation, ): Float { // Foo is going to have a vertical offset of 50dp. Let's make the swipe distance 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 1f9ba9ee9372..70f2ff80f9d7 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 @@ -28,11 +28,12 @@ import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.content.state.TransitionState -import com.android.compose.animation.scene.transformation.CustomSizeTransformation +import com.android.compose.animation.scene.transformation.CustomPropertyTransformation import com.android.compose.animation.scene.transformation.OverscrollTranslate +import com.android.compose.animation.scene.transformation.PropertyTransformation import com.android.compose.animation.scene.transformation.PropertyTransformationScope +import com.android.compose.animation.scene.transformation.TransformationMatcher import com.android.compose.animation.scene.transformation.TransformationRange -import com.android.compose.animation.scene.transformation.TransformationWithRange import com.android.compose.test.transition import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat @@ -103,7 +104,7 @@ class TransitionDslTest { val transitions = transitions { from(SceneA, to = SceneB) { fade(TestElements.Foo) } } val transformations = - transitions.transitionSpecs.single().transformationSpec(aToB()).transformations + transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers assertThat(transformations.size).isEqualTo(1) assertThat(transformations.single().range).isEqualTo(null) } @@ -126,7 +127,7 @@ class TransitionDslTest { } val transformations = - transitions.transitionSpecs.single().transformationSpec(aToB()).transformations + transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers assertThat(transformations) .comparingElementsUsing(TRANSFORMATION_RANGE) .containsExactly( @@ -157,7 +158,7 @@ class TransitionDslTest { } val transformations = - transitions.transitionSpecs.single().transformationSpec(aToB()).transformations + transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers assertThat(transformations) .comparingElementsUsing(TRANSFORMATION_RANGE) .containsExactly( @@ -185,7 +186,7 @@ class TransitionDslTest { } val transformations = - transitions.transitionSpecs.single().transformationSpec(aToB()).transformations + transitions.transitionSpecs.single().transformationSpec(aToB()).transformationMatchers assertThat(transformations) .comparingElementsUsing(TRANSFORMATION_RANGE) .containsExactly( @@ -215,7 +216,7 @@ class TransitionDslTest { // to B we defined. val transitionSpec = transitions.transitionSpec(from = SceneB, to = SceneA, key = null) - val transformations = transitionSpec.transformationSpec(aToB()).transformations + val transformations = transitionSpec.transformationSpec(aToB()).transformationMatchers assertThat(transformations) .comparingElementsUsing(TRANSFORMATION_RANGE) @@ -225,7 +226,7 @@ class TransitionDslTest { ) val previewTransformations = - transitionSpec.previewTransformationSpec(aToB())?.transformations + transitionSpec.previewTransformationSpec(aToB())?.transformationMatchers assertThat(previewTransformations) .comparingElementsUsing(TRANSFORMATION_RANGE) @@ -255,7 +256,7 @@ class TransitionDslTest { key = TransitionKey.PredictiveBack, ) - val transformations = transitionSpec.transformationSpec(aToB()).transformations + val transformations = transitionSpec.transformationSpec(aToB()).transformationMatchers assertThat(transformations) .comparingElementsUsing(TRANSFORMATION_RANGE) @@ -265,7 +266,7 @@ class TransitionDslTest { ) val previewTransformations = - transitionSpec.previewTransformationSpec(aToB())?.transformations + transitionSpec.previewTransformationSpec(aToB())?.transformationMatchers assertThat(previewTransformations) .comparingElementsUsing(TRANSFORMATION_RANGE) @@ -316,7 +317,7 @@ class TransitionDslTest { val overscrollSpec = transitions.overscrollSpecs.single() val transformation = - overscrollSpec.transformationSpec.transformations.single().transformation + overscrollSpec.transformationSpec.transformationMatchers.single().factory.create() assertThat(transformation).isInstanceOf(OverscrollTranslate::class.java) } @@ -324,7 +325,7 @@ class TransitionDslTest { fun overscrollSpec_for_overscrollDisabled() { val transitions = transitions { overscrollDisabled(SceneA, Orientation.Vertical) } val overscrollSpec = transitions.overscrollSpecs.single() - assertThat(overscrollSpec.transformationSpec.transformations).isEmpty() + assertThat(overscrollSpec.transformationSpec.transformationMatchers).isEmpty() } @Test @@ -353,9 +354,9 @@ class TransitionDslTest { val transitions = transitions { from(SceneA, to = SceneB) { fractionRange { - transformation( - object : CustomSizeTransformation { - override val matcher: ElementMatcher = TestElements.Foo + transformation(TestElements.Foo) { + object : CustomPropertyTransformation<IntSize> { + override val property = PropertyTransformation.Property.Size override fun PropertyTransformationScope.transform( content: ContentKey, @@ -364,7 +365,7 @@ class TransitionDslTest { transitionScope: CoroutineScope, ): IntSize = IntSize.Zero } - ) + } } } } @@ -377,7 +378,7 @@ class TransitionDslTest { companion object { private val TRANSFORMATION_RANGE = - Correspondence.transforming<TransformationWithRange<*>, TransformationRange?>( + Correspondence.transforming<TransformationMatcher, TransformationRange?>( { it?.range }, "has range equal to", ) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt index 313379f4c74b..0adb4809dd2d 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt @@ -157,7 +157,7 @@ abstract class BaseTransitionSubject<T : TransitionState.Transition>( check("isUserInputOngoing").that(actual.isUserInputOngoing).isEqualTo(isUserInputOngoing) } - fun hasOverscrollSpec(): OverscrollSpec { + internal fun hasOverscrollSpec(): OverscrollSpec { check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNotNull() return actual.currentOverscrollSpec!! } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt index 487b0992c3e5..444ec4ead561 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/CustomTransformationTest.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey -import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.testTransition @@ -47,8 +46,9 @@ class CustomTransformationTest { @Test fun customSize() { /** A size transformation that adds [add] to the size of the transformed element(s). */ - class AddSizeTransformation(override val matcher: ElementMatcher, private val add: Dp) : - CustomSizeTransformation { + class AddSizeTransformation(private val add: Dp) : CustomPropertyTransformation<IntSize> { + override val property = PropertyTransformation.Property.Size + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -69,7 +69,7 @@ class CustomTransformationTest { spec = tween(16 * 4, easing = LinearEasing) // Add 80dp to the width and height of Foo. - transformation(AddSizeTransformation(TestElements.Foo, 80.dp)) + transformation(TestElements.Foo) { AddSizeTransformation(80.dp) } }, ) { before { onElement(TestElements.Foo).assertSizeIsEqualTo(40.dp, 20.dp) } @@ -84,8 +84,9 @@ class CustomTransformationTest { @Test fun customOffset() { /** An offset transformation that adds [add] to the offset of the transformed element(s). */ - class AddOffsetTransformation(override val matcher: ElementMatcher, private val add: Dp) : - CustomOffsetTransformation { + class AddOffsetTransformation(private val add: Dp) : CustomPropertyTransformation<Offset> { + override val property = PropertyTransformation.Property.Offset + override fun PropertyTransformationScope.transform( content: ContentKey, element: ElementKey, @@ -106,7 +107,7 @@ class CustomTransformationTest { spec = tween(16 * 4, easing = LinearEasing) // Add 80dp to the offset of Foo. - transformation(AddOffsetTransformation(TestElements.Foo, 80.dp)) + transformation(TestElements.Foo) { AddOffsetTransformation(80.dp) } }, ) { before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 0.dp) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt deleted file mode 100644 index 2a2d33308d12..000000000000 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt +++ /dev/null @@ -1,252 +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.shared.clocks - -import android.content.Context -import android.content.res.Resources -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.util.TypedValue -import com.android.internal.policy.SystemBarUtils -import com.android.systemui.log.core.Logger -import com.android.systemui.log.core.MessageBuffer -import com.android.systemui.monet.Style as MonetStyle -import java.io.IOException - -class AssetLoader -private constructor( - private val pluginCtx: Context, - private val sysuiCtx: Context, - private val baseDir: String, - var seedColor: Int?, - var overrideChroma: Float?, - val typefaceCache: TypefaceCache, - messageBuffer: MessageBuffer, -) { - val logger = Logger(messageBuffer, TAG) - private val resources = - listOf( - Pair(pluginCtx.resources, pluginCtx.packageName), - Pair(sysuiCtx.resources, sysuiCtx.packageName), - ) - - constructor( - pluginCtx: Context, - sysuiCtx: Context, - baseDir: String, - messageBuffer: MessageBuffer, - ) : this( - pluginCtx, - sysuiCtx, - baseDir, - seedColor = null, - overrideChroma = null, - typefaceCache = - TypefaceCache(messageBuffer) { - // TODO(b/364680873): Move constant to config_clockFontFamily when shipping - return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL) - }, - messageBuffer = messageBuffer, - ) - - fun listAssets(path: String): List<String> { - return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList() - } - - fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString) - - fun readString(resStr: String): String { - val resPair = resolveResourceId(resStr) - if (resPair == null) { - throw IOException("Failed to parse string: $resStr") - } - - val (res, id) = resPair - return res.getString(id) - } - - fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr) - - fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset) - - fun readTextAsset(path: String): String { - return pluginCtx.resources.assets.open("$baseDir$path").use { stream -> - val buffer = ByteArray(stream.available()) - stream.read(buffer) - String(buffer) - } - } - - fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset) - - fun readDrawableAsset(path: String): Drawable { - var result: Drawable? - - if (path.startsWith("@")) { - val pair = resolveResourceId(path) - if (pair == null) { - throw IOException("Failed to parse $path to an id") - } - val (res, id) = pair - result = res.getDrawable(id) - } else if (path.endsWith("xml")) { - // TODO(b/248609434): Support xml files in assets - throw IOException("Cannot load xml files from assets") - } else { - // Attempt to load as if it's a bitmap and directly loadable - result = - pluginCtx.resources.assets.open("$baseDir$path").use { stream -> - Drawable.createFromResourceStream( - pluginCtx.resources, - TypedValue(), - stream, - null, - ) - } - } - - return result ?: throw IOException("Failed to load: $baseDir$path") - } - - fun parseResourceId(resStr: String): Triple<String?, String, String> { - if (!resStr.startsWith("@")) { - throw IOException("Invalid resource id: $resStr; Must start with '@'") - } - - // Parse out resource string - val parts = resStr.drop(1).split('/', ':') - return when (parts.size) { - 2 -> Triple(null, parts[0], parts[1]) - 3 -> Triple(parts[0], parts[1], parts[2]) - else -> throw IOException("Failed to parse resource string: $resStr") - } - } - - fun resolveResourceId(resStr: String): Pair<Resources, Int>? { - val (packageName, category, name) = parseResourceId(resStr) - return resolveResourceId(packageName, category, name) - } - - fun resolveResourceId( - packageName: String?, - category: String, - name: String, - ): Pair<Resources, Int>? { - for ((res, ctxPkgName) in resources) { - val result = res.getIdentifier(name, category, packageName ?: ctxPkgName) - if (result != 0) { - return Pair(res, result) - } - } - return null - } - - private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? { - try { - if (arg == null) { - return null - } - return fn(arg) - } catch (ex: IOException) { - logger.w("Failed to read $arg", ex) - return null - } - } - - fun assetExists(path: String): Boolean { - try { - if (path.startsWith("@")) { - val pair = resolveResourceId(path) - return pair != null - } else { - val stream = pluginCtx.resources.assets.open("$baseDir$path") - if (stream == null) { - return false - } - - stream.close() - return true - } - } catch (ex: IOException) { - return false - } - } - - fun copy(messageBuffer: MessageBuffer? = null): AssetLoader = - AssetLoader( - pluginCtx, - sysuiCtx, - baseDir, - seedColor, - overrideChroma, - typefaceCache, - messageBuffer ?: logger.buffer, - ) - - fun setSeedColor(seedColor: Int?, style: MonetStyle?) { - this.seedColor = seedColor - } - - fun getClockPaddingStart(): Int { - val result = resolveResourceId(null, "dimen", "clock_padding_start") - if (result != null) { - val (res, id) = result - return res.getDimensionPixelSize(id) - } - return -1 - } - - fun getStatusBarHeight(): Int { - val display = pluginCtx.getDisplayNoVerify() - if (display != null) { - return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout) - } - - logger.w("No display available; falling back to android.R.dimen.status_bar_height") - val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height") - if (statusBarHeight != null) { - val (res, resId) = statusBarHeight - return res.getDimensionPixelSize(resId) - } - - throw Exception("Could not fetch StatusBarHeight") - } - - fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id } - - fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize) - - fun getString(name: String): String = getResource("string", name, Resources::getString) - - private fun <T> getResource( - category: String, - name: String, - getter: (res: Resources, id: Int) -> T, - ): T { - val result = resolveResourceId(null, category, name) - if (result != null) { - val (res, id) = result - if (id == -1) throw Exception("Cannot find id of $id from $TAG") - return getter(res, id) - } - throw Exception("Cannot find id of $name from $TAG") - } - - companion object { - private val TAG = AssetLoader::class.simpleName!! - } -} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt index 4ed8fd8f631d..d0a32dcf9865 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -16,12 +16,9 @@ package com.android.systemui.shared.clocks -import android.content.Context -import android.content.res.Resources import android.graphics.Rect import androidx.annotation.VisibleForTesting import com.android.systemui.log.core.Logger -import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents @@ -37,31 +34,22 @@ import java.util.Locale import java.util.TimeZone class ComposedDigitalLayerController( - private val ctx: Context, - private val resources: Resources, - private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources + private val clockCtx: ClockContext, private val layer: ComposedDigitalHandLayer, - messageBuffer: MessageBuffer, ) : SimpleClockLayerController { - private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!) + private val logger = + Logger(clockCtx.messageBuffer, ComposedDigitalLayerController::class.simpleName!!) val layerControllers = mutableListOf<SimpleClockLayerController>() val dozeState = DefaultClockController.AnimationState(1F) - override val view = FlexClockView(ctx, assets, messageBuffer) + override val view = FlexClockView(clockCtx) init { layer.digitalLayers.forEach { - val childView = SimpleDigitalClockTextView(ctx, messageBuffer) + val childView = SimpleDigitalClockTextView(clockCtx) val controller = - SimpleDigitalHandLayerController( - ctx, - resources, - assets, - it as DigitalHandLayer, - childView, - messageBuffer, - ) + SimpleDigitalHandLayerController(clockCtx, it as DigitalHandLayer, childView) view.addView(childView) layerControllers.add(controller) @@ -156,8 +144,9 @@ class ComposedDigitalLayerController( val color = when { theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100) - else -> resources.getColor(android.R.color.system_accent2_600) + theme.isDarkTheme -> + clockCtx.resources.getColor(android.R.color.system_accent1_100) + else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) } view.updateColor(color) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index be4ebdfcb992..935737c5c79c 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -15,10 +15,10 @@ package com.android.systemui.shared.clocks import android.content.Context import android.content.res.Resources +import android.graphics.Typeface import android.view.LayoutInflater import com.android.systemui.customization.R -import com.android.systemui.log.core.LogLevel -import com.android.systemui.log.core.LogcatOnlyMessageBuffer +import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFontAxis import com.android.systemui.plugins.clocks.ClockFontAxisSetting @@ -33,6 +33,15 @@ import com.android.systemui.shared.clocks.view.VerticalAlignment private val TAG = DefaultClockProvider::class.simpleName const val DEFAULT_CLOCK_ID = "DEFAULT" +data class ClockContext( + val context: Context, + val resources: Resources, + val settings: ClockSettings, + val typefaceCache: TypefaceCache, + val messageBuffers: ClockMessageBuffers, + val messageBuffer: MessageBuffer, +) + /** Provides the default system clock */ class DefaultClockProvider( val ctx: Context, @@ -55,18 +64,24 @@ class DefaultClockProvider( } return if (isClockReactiveVariantsEnabled) { - val buffer = - messageBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.INFO) - val assets = AssetLoader(ctx, ctx, "clocks/", buffer) - assets.setSeedColor(settings.seedColor, null) + val buffers = messageBuffers ?: ClockMessageBuffers(LogUtil.DEFAULT_MESSAGE_BUFFER) val fontAxes = ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes) + val clockSettings = settings.copy(axes = fontAxes.map { it.toSetting() }) + val typefaceCache = + TypefaceCache(buffers.infraMessageBuffer) { + // TODO(b/364680873): Move constant to config_clockFontFamily when shipping + return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL) + } FlexClockController( - ctx, - resources, - settings.copy(axes = fontAxes.map { it.toSetting() }), - assets, + ClockContext( + ctx, + resources, + clockSettings, + typefaceCache, + buffers, + buffers.infraMessageBuffer, + ), FLEX_DESIGN, - messageBuffers, ) } else { DefaultClockController( 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 6c627e27a19b..c7a3f63e92e7 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 @@ -16,8 +16,6 @@ package com.android.systemui.shared.clocks -import android.content.Context -import android.content.res.Resources import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.AxisType @@ -26,8 +24,6 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFontAxis import com.android.systemui.plugins.clocks.ClockFontAxisSetting -import com.android.systemui.plugins.clocks.ClockMessageBuffers -import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData @@ -38,42 +34,28 @@ import java.util.TimeZone /** Controller for the default flex clock */ class FlexClockController( - private val ctx: Context, - private val resources: Resources, - private val settings: ClockSettings, - private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources + private val clockCtx: ClockContext, val design: ClockDesign, // TODO(b/364680879): Remove when done inlining - val messageBuffers: ClockMessageBuffers?, ) : ClockController { - override val smallClock = run { - val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER + override val smallClock = FlexClockFaceController( - ctx, - resources, - assets.copy(messageBuffer = buffer), + clockCtx.copy(messageBuffer = clockCtx.messageBuffers.smallClockMessageBuffer), design.small ?: design.large!!, - false, - buffer, + isLargeClock = false, ) - } - override val largeClock = run { - val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER + override val largeClock = FlexClockFaceController( - ctx, - resources, - assets.copy(messageBuffer = buffer), + clockCtx.copy(messageBuffer = clockCtx.messageBuffers.largeClockMessageBuffer), design.large ?: design.small!!, - true, - buffer, + isLargeClock = true, ) - } override val config: ClockConfig by lazy { ClockConfig( DEFAULT_CLOCK_ID, - resources.getString(R.string.clock_default_name), - resources.getString(R.string.clock_default_description), + clockCtx.resources.getString(R.string.clock_default_name), + clockCtx.resources.getString(R.string.clock_default_description), isReactiveToTone = true, ) } @@ -125,8 +107,8 @@ class FlexClockController( } override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) { - val theme = ThemeConfig(isDarkTheme, assets.seedColor) - events.onFontAxesChanged(settings.axes) + val theme = ThemeConfig(isDarkTheme, clockCtx.settings.seedColor) + events.onFontAxesChanged(clockCtx.settings.axes) smallClock.run { events.onThemeChanged(theme) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index ee21ea6ee126..a8890e6aa934 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -16,15 +16,12 @@ package com.android.systemui.shared.clocks -import android.content.Context -import android.content.res.Resources import android.graphics.Rect import android.view.Gravity import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import com.android.systemui.customization.R -import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents @@ -45,22 +42,19 @@ import kotlin.math.max // TODO(b/364680879): Merge w/ ComposedDigitalLayerController class FlexClockFaceController( - ctx: Context, - private val resources: Resources, - val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources + clockCtx: ClockContext, face: ClockFace, private val isLargeClock: Boolean, - messageBuffer: MessageBuffer, ) : ClockFaceController { override val view: View get() = layerController.view override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) - override var theme = ThemeConfig(true, assets.seedColor) + override var theme = ThemeConfig(true, clockCtx.settings.seedColor) private val keyguardLargeClockTopMargin = - resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin) + clockCtx.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin) val layerController: SimpleClockLayerController val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm") @@ -72,23 +66,10 @@ class FlexClockFaceController( layerController = if (isLargeClock) { - ComposedDigitalLayerController( - ctx, - resources, - assets, - layer as ComposedDigitalHandLayer, - messageBuffer, - ) + ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer) } else { - val childView = SimpleDigitalClockTextView(ctx, messageBuffer) - SimpleDigitalHandLayerController( - ctx, - resources, - assets, - layer as DigitalHandLayer, - childView, - messageBuffer, - ) + val childView = SimpleDigitalClockTextView(clockCtx) + SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView) } layerController.view.layoutParams = lp } @@ -97,7 +78,7 @@ class FlexClockFaceController( private fun offsetGlyphsForStepClockAnimation( clockStartLeft: Int, direction: Int, - fraction: Float + fraction: Float, ) { (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation( clockStartLeft, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index 143b28f1e82b..ebac4b24732b 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -16,8 +16,6 @@ package com.android.systemui.shared.clocks -import android.content.Context -import android.content.res.Resources import android.graphics.Rect import android.view.View import android.view.ViewGroup @@ -25,7 +23,6 @@ import android.widget.RelativeLayout import androidx.annotation.VisibleForTesting import com.android.systemui.customization.R import com.android.systemui.log.core.Logger -import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents @@ -42,14 +39,11 @@ import java.util.TimeZone private val TAG = SimpleDigitalHandLayerController::class.simpleName!! open class SimpleDigitalHandLayerController<T>( - private val ctx: Context, - private val resources: Resources, - private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources + private val clockCtx: ClockContext, private val layer: DigitalHandLayer, override val view: T, - messageBuffer: MessageBuffer, ) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView { - private val logger = Logger(messageBuffer, TAG) + private val logger = Logger(clockCtx.messageBuffer, TAG) val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat) @VisibleForTesting @@ -75,12 +69,12 @@ open class SimpleDigitalHandLayerController<T>( layer.alignment.verticalAlignment?.let { view.verticalAlignment = it } layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it } } - view.applyStyles(assets, layer.style, layer.aodStyle) + view.applyStyles(layer.style, layer.aodStyle) view.id = - ctx.resources.getIdentifier( + clockCtx.resources.getIdentifier( generateDigitalLayerIdString(layer), "id", - ctx.getPackageName(), + clockCtx.context.getPackageName(), ) } @@ -306,8 +300,9 @@ open class SimpleDigitalHandLayerController<T>( val color = when { theme.seedColor != null -> theme.seedColor!! - theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100) - else -> resources.getColor(android.R.color.system_accent2_600) + theme.isDarkTheme -> + clockCtx.resources.getColor(android.R.color.system_accent1_100) + else -> clockCtx.resources.getColor(android.R.color.system_accent2_600) } view.updateColor(color) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt index b09332f34e99..d4eb7677c757 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt @@ -16,24 +16,23 @@ package com.android.systemui.shared.clocks.view -import android.content.Context import android.graphics.Canvas import android.graphics.Point import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.systemui.log.core.Logger -import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.AlarmData import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData +import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.LogUtil import java.util.Locale // TODO(b/364680879): Merge w/ only subclass FlexClockView -abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) { - protected val logger = Logger(messageBuffer, this::class.simpleName!!) +abstract class DigitalClockFaceView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) { + protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!) get() = field ?: LogUtil.FALLBACK_INIT_LOGGER abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView> diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index 593eba9d05cc..27ed099f097a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -16,7 +16,6 @@ package com.android.systemui.shared.clocks.view -import android.content.Context import android.graphics.Canvas import android.graphics.Point import android.util.MathUtils.constrainedMap @@ -25,8 +24,7 @@ import android.view.ViewGroup import android.widget.RelativeLayout import com.android.app.animation.Interpolators import com.android.systemui.customization.R -import com.android.systemui.log.core.MessageBuffer -import com.android.systemui.shared.clocks.AssetLoader +import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.DigitTranslateAnimator import kotlin.math.abs import kotlin.math.max @@ -34,8 +32,7 @@ import kotlin.math.min fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal) -class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: MessageBuffer) : - DigitalClockFaceView(context, messageBuffer) { +class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) { override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>() val digitLeftTopMap = mutableMapOf<Int, Point>() var maxSingleDigitSize = Point(-1, -1) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index 5c84f2d04ccc..bd56dbf9bfe3 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -17,7 +17,6 @@ package com.android.systemui.shared.clocks.view import android.annotation.SuppressLint -import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint @@ -37,13 +36,11 @@ import android.view.animation.Interpolator import android.widget.TextView import com.android.internal.annotations.VisibleForTesting import com.android.systemui.animation.TextAnimator -import com.android.systemui.animation.TypefaceVariantCache import com.android.systemui.customization.R import com.android.systemui.log.core.Logger -import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.ClockFontAxisSetting -import com.android.systemui.shared.clocks.AssetLoader import com.android.systemui.shared.clocks.ClockAnimation +import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.DigitTranslateAnimator import com.android.systemui.shared.clocks.DimensionParser import com.android.systemui.shared.clocks.FontTextStyle @@ -57,18 +54,15 @@ import kotlin.math.min private val TAG = SimpleDigitalClockTextView::class.simpleName!! @SuppressLint("AppCompatCustomView") -open class SimpleDigitalClockTextView( - ctx: Context, - messageBuffer: MessageBuffer, - attrs: AttributeSet? = null, -) : TextView(ctx, attrs), SimpleDigitalClockView { +open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) : + TextView(clockCtx.context, attrs), SimpleDigitalClockView { val lockScreenPaint = TextPaint() override lateinit var textStyle: FontTextStyle lateinit var aodStyle: FontTextStyle private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION) private var aodFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_AOD_VARIATION) - private val parser = DimensionParser(ctx) + private val parser = DimensionParser(clockCtx.context) var maxSingleDigitHeight = -1 var maxSingleDigitWidth = -1 var digitTranslateAnimator: DigitTranslateAnimator? = null @@ -91,29 +85,19 @@ open class SimpleDigitalClockTextView( private val prevTextBounds = Rect() // targetTextBounds holds the state we are interpolating to private val targetTextBounds = Rect() - protected val logger = Logger(messageBuffer, this::class.simpleName!!) + protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!) get() = field ?: LogUtil.FALLBACK_INIT_LOGGER private var aodDozingInterpolator: Interpolator? = null @VisibleForTesting lateinit var textAnimator: TextAnimator - lateinit var typefaceCache: TypefaceVariantCache - private set - - private fun setTypefaceCache(value: TypefaceVariantCache) { - typefaceCache = value - if (this::textAnimator.isInitialized) { - textAnimator.typefaceCache = value - } - } + private val typefaceCache = clockCtx.typefaceCache.getVariantCache("") @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb -> TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also { - if (this::typefaceCache.isInitialized) { - it.typefaceCache = typefaceCache - } + it.typefaceCache = typefaceCache } } @@ -436,10 +420,9 @@ open class SimpleDigitalClockTextView( return updateXtranslation(localTranslation, interpolatedTextBounds) } - override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) { + override fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) { this.textStyle = textStyle as FontTextStyle val typefaceName = "fonts/" + textStyle.fontFamily - setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName)) lockScreenPaint.strokeJoin = Paint.Join.ROUND lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) textStyle.fontFeatureSettings?.let { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt index 3d2ed4a1ef40..e8be28fb8df2 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt @@ -18,7 +18,6 @@ package com.android.systemui.shared.clocks.view import androidx.annotation.VisibleForTesting import com.android.systemui.plugins.clocks.ClockFontAxisSetting -import com.android.systemui.shared.clocks.AssetLoader import com.android.systemui.shared.clocks.TextStyle interface SimpleDigitalClockView { @@ -29,7 +28,7 @@ interface SimpleDigitalClockView { val textStyle: TextStyle @VisibleForTesting var isAnimationEnabled: Boolean - fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) + fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt index 72163e4d7710..f82c8b0e56e0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt @@ -31,7 +31,8 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.whenever @@ -62,7 +63,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() { private val testScope = kosmos.testScope private val clock = FakeSystemClock() private val userRepository = FakeUserRepository() - private val mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository + private val mobileConnectionsRepository = kosmos.mobileConnectionsRepository private lateinit var underTest: AuthenticationRepository @@ -110,7 +111,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() { .isEqualTo(AuthenticationMethodModel.None) currentSecurityMode = KeyguardSecurityModel.SecurityMode.SimPin - mobileConnectionsRepository.isAnySimSecure.value = true + mobileConnectionsRepository.fake.isAnySimSecure.value = true assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Sim) assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Sim) } @@ -199,7 +200,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() { } private fun setSecurityModeAndDispatchBroadcast( - securityMode: KeyguardSecurityModel.SecurityMode, + securityMode: KeyguardSecurityModel.SecurityMode ) { currentSecurityMode = securityMode dispatchBroadcast() @@ -208,23 +209,15 @@ class AuthenticationRepositoryTest : SysuiTestCase() { private fun dispatchBroadcast() { fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, - Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED) + Intent(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED), ) } companion object { private val USER_INFOS = listOf( - UserInfo( - /* id= */ 100, - /* name= */ "First user", - /* flags= */ 0, - ), - UserInfo( - /* id= */ 101, - /* name= */ "Second user", - /* flags= */ 0, - ), + UserInfo(/* id= */ 100, /* name= */ "First user", /* flags= */ 0), + UserInfo(/* id= */ 101, /* name= */ "Second user", /* flags= */ 0), ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt index 566cd70598de..10bf5233e61e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt @@ -35,7 +35,8 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.telephony.data.repository.fakeTelephonyRepository import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor @@ -78,7 +79,7 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository + mobileConnectionsRepository = kosmos.mobileConnectionsRepository.fake overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL) overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index 8062358f670c..a65e7ed48797 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -19,7 +19,9 @@ import android.app.WindowConfiguration import android.content.ComponentName import android.content.Intent import android.os.RemoteException +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.service.dreams.Flags import android.service.dreams.IDreamOverlay import android.service.dreams.IDreamOverlayCallback @@ -33,7 +35,6 @@ import android.view.WindowManagerImpl import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.viewcapture.ViewCapture import com.android.app.viewcapture.ViewCaptureAwareWindowManager @@ -43,6 +44,7 @@ import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent @@ -62,12 +64,16 @@ import com.android.systemui.complication.ComplicationLayoutEngine import com.android.systemui.complication.dagger.ComplicationComponent import com.android.systemui.dreams.complication.HideComplicationTouchHandler import com.android.systemui.dreams.dagger.DreamOverlayComponent +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.gesture.domain.gestureInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.navigationbar.gestural.domain.GestureInteractor import com.android.systemui.navigationbar.gestural.domain.TaskInfo import com.android.systemui.navigationbar.gestural.domain.TaskMatcher +import com.android.systemui.scene.data.repository.sceneContainerRepository +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.android.systemui.touch.TouchInsetManager import com.android.systemui.util.concurrency.FakeExecutor @@ -98,12 +104,14 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidJUnit4::class) -class DreamOverlayServiceTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() { private val mFakeSystemClock = FakeSystemClock() private val mMainExecutor = FakeExecutor(mFakeSystemClock) private val kosmos = testKosmos() @@ -245,6 +253,10 @@ class DreamOverlayServiceTest : SysuiTestCase() { ) } + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setup() { MockitoAnnotations.initMocks(this) @@ -287,6 +299,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { mKeyguardUpdateMonitor, mScrimManager, mCommunalInteractor, + kosmos.sceneInteractor, mSystemDialogsCloser, mUiEventLogger, mTouchInsetManager, @@ -768,6 +781,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Test @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB) + @DisableFlags(FLAG_SCENE_CONTAINER) @kotlin.Throws(RemoteException::class) fun testTransitionToGlanceableHub() = testScope.runTest { @@ -793,6 +807,35 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test + @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_SCENE_CONTAINER, FLAG_COMMUNAL_HUB) + @kotlin.Throws(RemoteException::class) + fun testTransitionToGlanceableHub_sceneContainer() = + testScope.runTest { + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*isPreview*/, + false, /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + verify(mDreamOverlayCallback).onRedirectWake(false) + clearInvocations(mDreamOverlayCallback) + kosmos.setCommunalAvailable(true) + mMainExecutor.runAllReady() + runCurrent() + verify(mDreamOverlayCallback).onRedirectWake(true) + client.onWakeRequested() + mMainExecutor.runAllReady() + runCurrent() + assertThat(kosmos.sceneContainerRepository.currentScene.value) + .isEqualTo(Scenes.Communal) + verify(mUiEventLogger).log(CommunalUiEvent.DREAM_TO_COMMUNAL_HUB_DREAM_AWAKE_START) + } + + @Test @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB) @Throws(RemoteException::class) fun testRedirectExit() = @@ -911,6 +954,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Verifies that the touch handling lifecycle is STARTED even if the dream starts while not // focused. @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun testLifecycle_dreamNotFocusedOnStart_isStarted() { val transitionState: MutableStateFlow<ObservableTransitionState> = MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank)) @@ -1024,6 +1068,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun testCommunalVisible_setsLifecycleState() { val client = client @@ -1060,6 +1105,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Verifies the dream's lifecycle @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun testLifecycleStarted_whenAnyOcclusion() { val client = client @@ -1256,5 +1302,11 @@ class DreamOverlayServiceTest : SysuiTestCase() { ComponentName("package", "homeControlPanel") private const val DREAM_COMPONENT = "package/dream" private const val WINDOW_NAME = "test" + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_HUB).andSceneContainer() + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt new file mode 100644 index 000000000000..aacfaed1ab4a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.dreams.ui.viewmodel + +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.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.shared.flag.DualShade +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class DreamUserActionsViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: DreamUserActionsViewModel + + @Before + fun setUp() { + underTest = kosmos.dreamUserActionsViewModel + underTest.activateIn(testScope) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun actions_singleShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Single, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true)) + + setUpState( + isShadeTouchable = false, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Single, + ) + assertThat(actions).isEmpty() + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = true, + shadeMode = ShadeMode.Single, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true)) + } + + @Test + @DisableFlags(DualShade.FLAG_NAME) + fun actions_splitShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Split, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true)) + + setUpState( + isShadeTouchable = false, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Split, + ) + assertThat(actions).isEmpty() + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = true, + shadeMode = ShadeMode.Split, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true)) + } + + @Test + @EnableFlags(DualShade.FLAG_NAME) + fun actions_dualShade() = + testScope.runTest { + val actions by collectLastValue(underTest.actions) + + setUpState( + isShadeTouchable = true, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Dual, + ) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo( + UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true) + ) + + setUpState( + isShadeTouchable = false, + isDeviceUnlocked = false, + shadeMode = ShadeMode.Dual, + ) + assertThat(actions).isEmpty() + + setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual) + assertThat(actions).isNotEmpty() + assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home)) + assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone)) + assertThat(actions?.get(Swipe.Down)) + .isEqualTo( + UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true) + ) + } + + private fun TestScope.setUpState( + isShadeTouchable: Boolean, + isDeviceUnlocked: Boolean, + shadeMode: ShadeMode, + ) { + if (isShadeTouchable) { + kosmos.powerInteractor.setAwakeForTest() + } else { + kosmos.powerInteractor.setAsleepForTest() + } + + if (isDeviceUnlocked) { + unlockDevice() + } else { + lockDevice() + } + + if (shadeMode == ShadeMode.Dual) { + assertThat(DualShade.isEnabled).isTrue() + } else { + assertThat(DualShade.isEnabled).isFalse() + kosmos.fakeShadeRepository.setShadeLayoutWide(shadeMode == ShadeMode.Split) + } + runCurrent() + } + + private fun TestScope.lockDevice() { + val deviceUnlockStatus by + collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + runCurrent() + } + + private fun TestScope.unlockDevice() { + val deviceUnlockStatus by + collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + kosmos.sceneInteractor.changeScene(Scenes.Gone, "reason") + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt index 5efb6171cdde..f6a6e5465e1b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModelTest.kt @@ -119,22 +119,6 @@ class TileHapticsViewModelTest : SysuiTestCase() { } @Test - fun onLongClick_whenTileDoesNotHandleLongClick_playsFailureHaptics() = - testScope.runTest { - // WHEN the tile is long-clicked but the tile does not handle a long-click - val state = QSTile.State().apply { handlesLongClick = false } - qsTile.changeState(state) - underTest.setTileInteractionState( - TileHapticsViewModel.TileInteractionState.LONG_CLICKED - ) - runCurrent() - - // THEN the failure token plays - assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE) - assertThat(msdlPlayer.latestPropertiesPlayed).isNull() - } - - @Test fun whenLaunchingFromClick_doesNotPlayHaptics() = testScope.runTest { // WHEN the tile is clicked and its action state changes accordingly diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt index c646d8f11706..cc4c7c419a11 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt @@ -218,11 +218,11 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { simpleShortcutCategory(System, "System controls", "View recent apps"), simpleShortcutCategory(AppCategories, "Applications", "Calculator"), simpleShortcutCategory(AppCategories, "Applications", "Calendar"), - simpleShortcutCategory(AppCategories, "Applications", "Chrome"), + simpleShortcutCategory(AppCategories, "Applications", "Browser"), simpleShortcutCategory(AppCategories, "Applications", "Contacts"), - simpleShortcutCategory(AppCategories, "Applications", "Gmail"), + simpleShortcutCategory(AppCategories, "Applications", "Email"), simpleShortcutCategory(AppCategories, "Applications", "Maps"), - simpleShortcutCategory(AppCategories, "Applications", "Messages"), + simpleShortcutCategory(AppCategories, "Applications", "SMS"), simpleShortcutCategory(MultiTasking, "Recent apps", "Cycle forward through recent apps"), ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt index 4e429c34bf2a..69fb03dc6433 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt @@ -47,7 +47,8 @@ import com.android.systemui.keyguard.data.repository.BiometricType.SIDE_FINGERPR import com.android.systemui.keyguard.data.repository.BiometricType.UNDER_DISPLAY_FINGERPRINT import com.android.systemui.keyguard.shared.model.DevicePosture import com.android.systemui.res.R -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository @@ -99,7 +100,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { private lateinit var devicePostureRepository: FakeDevicePostureRepository private lateinit var facePropertyRepository: FakeFacePropertyRepository private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository - private val mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository + private val mobileConnectionsRepository = kosmos.mobileConnectionsRepository private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -142,7 +143,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 1, SensorStrength.STRONG, FingerprintSensorType.UDFPS_OPTICAL, - emptyMap() + emptyMap(), ) verify(lockPatternUtils).registerStrongAuthTracker(strongAuthTracker.capture()) verify(authController, times(2)).addCallback(authControllerCallback.capture()) @@ -247,7 +248,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { private fun deviceIsInPostureThatSupportsFaceAuth() { overrideResource( R.integer.config_face_auth_supported_posture, - DevicePostureController.DEVICE_POSTURE_FLIPPED + DevicePostureController.DEVICE_POSTURE_FLIPPED, ) devicePostureRepository.setCurrentPosture(DevicePosture.FLIPPED) } @@ -459,7 +460,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { biometricsAreEnabledBySettings() doNotDisableKeyguardAuthFeatures() - mobileConnectionsRepository.isAnySimSecure.value = false + mobileConnectionsRepository.fake.isAnySimSecure.value = false runCurrent() val isFaceAuthEnabledAndEnrolled by @@ -467,7 +468,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { assertThat(isFaceAuthEnabledAndEnrolled).isTrue() - mobileConnectionsRepository.isAnySimSecure.value = true + mobileConnectionsRepository.fake.isAnySimSecure.value = true runCurrent() assertThat(isFaceAuthEnabledAndEnrolled).isFalse() @@ -485,13 +486,13 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { doNotDisableKeyguardAuthFeatures() faceAuthIsStrongBiometric() biometricsAreEnabledBySettings() - mobileConnectionsRepository.isAnySimSecure.value = false + mobileConnectionsRepository.fake.isAnySimSecure.value = false onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) onNonStrongAuthChanged(false, PRIMARY_USER_ID) assertThat(isFaceAuthCurrentlyAllowed).isTrue() - mobileConnectionsRepository.isAnySimSecure.value = true + mobileConnectionsRepository.fake.isAnySimSecure.value = true assertThat(isFaceAuthCurrentlyAllowed).isFalse() } @@ -584,7 +585,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { testScope.runTest { overrideResource( R.integer.config_face_auth_supported_posture, - DevicePostureController.DEVICE_POSTURE_UNKNOWN + DevicePostureController.DEVICE_POSTURE_UNKNOWN, ) createBiometricSettingsRepository() @@ -597,7 +598,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { testScope.runTest { overrideResource( R.integer.config_face_auth_supported_posture, - DevicePostureController.DEVICE_POSTURE_FLIPPED + DevicePostureController.DEVICE_POSTURE_FLIPPED, ) createBiometricSettingsRepository() @@ -749,7 +750,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 1, SensorStrength.STRONG, FingerprintSensorType.UDFPS_OPTICAL, - emptyMap() + emptyMap(), ) // Non strong auth is not allowed now, FP is marked strong onStrongAuthChanged(STRONG_AUTH_NOT_REQUIRED, PRIMARY_USER_ID) @@ -761,7 +762,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 1, SensorStrength.CONVENIENCE, FingerprintSensorType.UDFPS_OPTICAL, - emptyMap() + emptyMap(), ) assertThat(isFingerprintCurrentlyAllowed).isFalse() @@ -769,7 +770,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 1, SensorStrength.WEAK, FingerprintSensorType.UDFPS_OPTICAL, - emptyMap() + emptyMap(), ) assertThat(isFingerprintCurrentlyAllowed).isFalse() } @@ -791,7 +792,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { 1, SensorStrength.STRONG, FingerprintSensorType.UDFPS_OPTICAL, - emptyMap() + emptyMap(), ) // Non strong auth is not allowed now, FP is marked strong onStrongAuthChanged(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, PRIMARY_USER_ID) @@ -830,7 +831,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { UserInfo( /* id= */ PRIMARY_USER_ID, /* name= */ "primary user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val ANOTHER_USER_ID = 1 @@ -838,7 +839,7 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() { UserInfo( /* id= */ ANOTHER_USER_ID, /* name= */ "another user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index b72668bf5be6..921a8a610c37 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -41,8 +41,8 @@ import com.android.systemui.qs.panels.ui.viewmodel.setConfigurationForMediaInRow import com.android.systemui.res.R import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.statusbar.StatusBarState -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.util.animation.DisappearParameters import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt index bf97afed92f4..959081663b56 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt @@ -27,6 +27,7 @@ import com.android.compose.animation.scene.ObservableTransitionState.Transition. import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState @@ -194,6 +195,24 @@ class SceneContainerOcclusionInteractorTest : SysuiTestCase() { .isFalse() } + @Test + fun invisibleDueToOcclusion_isDreaming_emitsTrue() = + testScope.runTest { + val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion) + + // Verify that we start with unoccluded + assertWithMessage("Should start unoccluded").that(invisibleDueToOcclusion).isFalse() + + // Start dreaming, which is an occluding activity + showOccludingActivity() + kosmos.keyguardInteractor.setDreaming(true) + + // Verify not invisible when dreaming + assertWithMessage("Should be invisible when dreaming") + .that(invisibleDueToOcclusion) + .isTrue() + } + /** Simulates the appearance of a show-when-locked `Activity` in the foreground. */ private fun TestScope.showOccludingActivity() { keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( 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 af0274b1caaa..2e074da02103 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 @@ -73,6 +73,7 @@ import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.keyguard.dismissCallbackRegistry +import com.android.systemui.keyguard.domain.interactor.dozeInteractor import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscreenSceneTransitionInteractor @@ -143,6 +144,8 @@ class SceneContainerStartableTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val deviceEntryHapticsInteractor by lazy { kosmos.deviceEntryHapticsInteractor } + private val dozeInteractor by lazy { kosmos.dozeInteractor } + private val keyguardInteractor by lazy { kosmos.keyguardInteractor } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val sceneBackInteractor by lazy { kosmos.sceneBackInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } @@ -373,6 +376,64 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun hydrateVisibility_whileDreaming() = + testScope.runTest { + val isVisible by collectLastValue(sceneInteractor.isVisible) + + // GIVEN the device is dreaming + val transitionState = + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Dream) + underTest.start() + assertThat(isVisible).isFalse() + } + + @Test + fun hydrateVisibility_onCommunalWhileOccluded() = + testScope.runTest { + val isVisible by collectLastValue(sceneInteractor.isVisible) + + kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop( + true, + mock(), + ) + prepareState(isDeviceUnlocked = false, initialSceneKey = Scenes.Communal) + underTest.start() + runCurrent() + assertThat(isVisible).isTrue() + } + + @Test + fun hydrateVisibility_inCommunalTransition() = + testScope.runTest { + val isVisible by collectLastValue(sceneInteractor.isVisible) + + // GIVEN the device is dreaming + val transitionState = + prepareState( + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + initialSceneKey = Scenes.Dream, + ) + underTest.start() + assertThat(isVisible).isFalse() + + // WHEN a transition starts to the communal hub + sceneInteractor.changeScene(Scenes.Dream, "switching to dream for test") + transitionState.value = + ObservableTransitionState.Transition( + fromScene = Scenes.Dream, + toScene = Scenes.Communal, + currentScene = flowOf(Scenes.Dream), + progress = flowOf(0.5f), + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(false), + ) + runCurrent() + // THEN scenes are visible + assertThat(isVisible).isTrue() + } + + @Test fun startsInLockscreenScene() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) @@ -643,7 +704,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToAOD_whenAvailable_whenDeviceSleepsLocked() = testScope.runTest { kosmos.lockscreenSceneTransitionInteractor.start() - val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState) + val asleepState by collectLastValue(keyguardInteractor.asleepKeyguardState) val currentTransitionInfo by collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal) val transitionState = @@ -673,7 +734,7 @@ class SceneContainerStartableTest : SysuiTestCase() { fun switchToDozing_whenAodUnavailable_whenDeviceSleepsLocked() = testScope.runTest { kosmos.lockscreenSceneTransitionInteractor.start() - val asleepState by collectLastValue(kosmos.keyguardInteractor.asleepKeyguardState) + val asleepState by collectLastValue(keyguardInteractor.asleepKeyguardState) val currentTransitionInfo by collectLastValue(kosmos.keyguardTransitionRepository.currentTransitionInfoInternal) val transitionState = @@ -2360,6 +2421,66 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun stayOnLockscreen_whenDozingStarted() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + prepareState() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + underTest.start() + + // Stay on Lockscreen when dozing and dreaming + dozeInteractor.setIsDozing(true) + keyguardInteractor.setDreaming(true) + kosmos.fakeKeyguardRepository.setDreamingWithOverlay(false) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun switchFromLockscreenToDream_whenDreamStarted() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + prepareState() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + underTest.start() + + powerInteractor.setAwakeForTest() + keyguardInteractor.setDreaming(true) + // Move past initial delay with [KeyguardInteractor#isAbleToDream] + advanceTimeBy(600L) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Dream) + } + + @Test + fun switchFromDreamToLockscreen_whenLockedAndDreamStopped() = + testScope.runTest { + keyguardInteractor.setDreaming(true) + val currentScene by collectLastValue(sceneInteractor.currentScene) + prepareState(initialSceneKey = Scenes.Dream) + assertThat(currentScene).isEqualTo(Scenes.Dream) + underTest.start() + + keyguardInteractor.setDreaming(false) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun switchFromDreamToGone_whenUnlockedAndDreamStopped() = + testScope.runTest { + keyguardInteractor.setDreaming(true) + val currentScene by collectLastValue(sceneInteractor.currentScene) + prepareState(initialSceneKey = Scenes.Dream, isDeviceUnlocked = true) + assertThat(currentScene).isEqualTo(Scenes.Dream) + underTest.start() + + keyguardInteractor.setDreaming(false) + runCurrent() + assertThat(currentScene).isEqualTo(Scenes.Gone) + } + + @Test fun replacesLockscreenSceneOnBackStack_whenUnlockdViaAlternateBouncer_fromShade() = testScope.runTest { val transitionState = 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 01c17bd6285c..94a19c80dc00 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -148,7 +148,6 @@ import com.android.systemui.statusbar.QsFrameTranslateController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository; import com.android.systemui.statusbar.notification.ConversationNotificationManager; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.HeadsUpTouchHelper; @@ -428,7 +427,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), mKosmos.getDeviceProvisioningInteractor(), - new FakeDisableFlagsRepository(), + mKosmos.getDisableFlagsInteractor(), mDozeParameters, mFakeKeyguardRepository, mKeyguardTransitionInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java index 443595dbdf77..ef132d5a4989 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java @@ -149,7 +149,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { @Mock protected LargeScreenHeaderHelper mLargeScreenHeaderHelper; protected FakeDisableFlagsRepository mDisableFlagsRepository = - new FakeDisableFlagsRepository(); + mKosmos.getFakeDisableFlagsRepository(); protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); protected FakeShadeRepository mShadeRepository = new FakeShadeRepository(); @@ -185,7 +185,7 @@ public class QuickSettingsControllerImplBaseTest extends SysuiTestCase { mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), mKosmos.getDeviceProvisioningInteractor(), - mDisableFlagsRepository, + mKosmos.getDisableFlagsInteractor(), mDozeParameters, mKeyguardRepository, keyguardTransitionInteractor, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt index 46961d4db0f6..ee3f8016c410 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.shade import android.app.StatusBarManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelChildren @@ -61,7 +61,7 @@ class QuickSettingsControllerImplWithCoroutinesTest : QuickSettingsControllerImp mDisableFlagsRepository.disableFlags.value = DisableFlagsModel( StatusBarManager.DISABLE_NONE, - StatusBarManager.DISABLE2_QUICK_SETTINGS + StatusBarManager.DISABLE2_QUICK_SETTINGS, ) runCurrent() @@ -76,7 +76,7 @@ class QuickSettingsControllerImplWithCoroutinesTest : QuickSettingsControllerImp mDisableFlagsRepository.disableFlags.value = DisableFlagsModel( StatusBarManager.DISABLE_NONE, - StatusBarManager.DISABLE2_NOTIFICATION_SHADE + StatusBarManager.DISABLE2_NOTIFICATION_SHADE, ) runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt index 19ac0cf40160..da652c4e1b5e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt @@ -37,8 +37,8 @@ import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.shade.shadeTestUtil -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt index 907c68440b55..cd078211f934 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt @@ -32,7 +32,7 @@ import com.android.systemui.log.LogBufferFactory import com.android.systemui.res.R import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.disableflags.DisableFlagsLogger -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt index 79ff4be253e1..7eac7e86e372 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt @@ -21,8 +21,8 @@ import com.android.systemui.SysUITestComponent import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -51,10 +51,7 @@ class NotificationAlertsInteractorTest : SysuiTestCase() { fun disableFlags_notifAlertsNotDisabled_notifAlertsEnabledTrue() = with(testComponent) { disableFlags.disableFlags.value = - DisableFlagsModel( - StatusBarManager.DISABLE_NONE, - StatusBarManager.DISABLE2_NONE, - ) + DisableFlagsModel(StatusBarManager.DISABLE_NONE, StatusBarManager.DISABLE2_NONE) assertThat(underTest.areNotificationAlertsEnabled()).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index f6d439ab2639..5a77f3d40e82 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.os.ParcelUuid -import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET @@ -25,91 +24,78 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.log.table.logcatTableLogBuffer +import com.android.systemui.flags.fake +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryLogbufferName import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot -import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.fake import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository import com.android.systemui.testKosmos import com.android.systemui.util.CarrierConfigTracker -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.UUID import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class MobileIconsInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private lateinit var underTest: MobileIconsInteractor - private lateinit var connectivityRepository: FakeConnectivityRepository - private lateinit var connectionsRepository: FakeMobileConnectionsRepository - private val userSetupRepository = FakeUserSetupRepository() - private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val flags = - FakeFeatureFlagsClassic().apply { - set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) + private val kosmos by lazy { + testKosmos().apply { + mobileConnectionsRepositoryLogbufferName = "MobileIconsInteractorTest" + mobileConnectionsRepository.fake.run { + setMobileConnectionRepositoryMap( + mapOf( + SUB_1_ID to CONNECTION_1, + SUB_2_ID to CONNECTION_2, + SUB_3_ID to CONNECTION_3, + SUB_4_ID to CONNECTION_4, + ) + ) + setActiveMobileDataSubscriptionId(SUB_1_ID) + } + featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) } + } - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - - private val tableLogBuffer = logcatTableLogBuffer(kosmos, "MobileIconsInteractorTest") - - @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - - connectivityRepository = FakeConnectivityRepository() - - connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer) - connectionsRepository.setMobileConnectionRepositoryMap( - mapOf( - SUB_1_ID to CONNECTION_1, - SUB_2_ID to CONNECTION_2, - SUB_3_ID to CONNECTION_3, - SUB_4_ID to CONNECTION_4, - ) + // shortcut rename + private val Kosmos.connectionsRepository by Fixture { mobileConnectionsRepository.fake } + + private val Kosmos.carrierConfigTracker by Fixture { mock<CarrierConfigTracker>() } + + private val Kosmos.underTest by Fixture { + MobileIconsInteractorImpl( + mobileConnectionsRepository, + carrierConfigTracker, + tableLogger = mock(), + connectivityRepository, + FakeUserSetupRepository(), + testScope.backgroundScope, + context, + featureFlagsClassic, ) - connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) - - underTest = - MobileIconsInteractorImpl( - connectionsRepository, - carrierConfigTracker, - tableLogger = mock(), - connectivityRepository, - userSetupRepository, - testScope.backgroundScope, - context, - flags, - ) } @Test fun filteredSubscriptions_default() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.filteredSubscriptions) assertThat(latest).isEqualTo(listOf<SubscriptionModel>()) @@ -118,7 +104,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { // Based on the logic from the old pipeline, we'll never filter subs when there are more than 2 @Test fun filteredSubscriptions_moreThanTwo_doesNotFilter() = - testScope.runTest { + kosmos.runTest { connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) @@ -129,7 +115,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = - testScope.runTest { + kosmos.runTest { connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) val latest by collectLastValue(underTest.filteredSubscriptions) @@ -139,7 +125,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_opportunistic_differentGroups_doesNotFilter() = - testScope.runTest { + kosmos.runTest { connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) @@ -150,7 +136,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_opportunistic_nonGrouped_doesNotFilter() = - testScope.runTest { + kosmos.runTest { val (sub1, sub2) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_2_ID), @@ -167,7 +153,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_3() = - testScope.runTest { + kosmos.runTest { val (sub3, sub4) = createSubscriptionPair( subscriptionIds = Pair(SUB_3_ID, SUB_4_ID), @@ -187,7 +173,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_opportunistic_grouped_configFalse_showsActive_4() = - testScope.runTest { + kosmos.runTest { val (sub3, sub4) = createSubscriptionPair( subscriptionIds = Pair(SUB_3_ID, SUB_4_ID), @@ -207,7 +193,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_active_1() = - testScope.runTest { + kosmos.runTest { val (sub1, sub3) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), @@ -228,7 +214,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_oneOpportunistic_grouped_configTrue_showsPrimary_nonActive_1() = - testScope.runTest { + kosmos.runTest { val (sub1, sub3) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), @@ -249,7 +235,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_vcnSubId_agreesWithActiveSubId_usesActiveAkaVcnSub() = - testScope.runTest { + kosmos.runTest { val (sub1, sub3) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), @@ -258,7 +244,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { ) connectionsRepository.setSubscriptions(listOf(sub1, sub3)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) - connectivityRepository.vcnSubId.value = SUB_3_ID + kosmos.connectivityRepository.fake.vcnSubId.value = SUB_3_ID whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(false) @@ -269,7 +255,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_vcnSubId_disagreesWithActiveSubId_usesVcnSub() = - testScope.runTest { + kosmos.runTest { val (sub1, sub3) = createSubscriptionPair( subscriptionIds = Pair(SUB_1_ID, SUB_3_ID), @@ -278,7 +264,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { ) connectionsRepository.setSubscriptions(listOf(sub1, sub3)) connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) - connectivityRepository.vcnSubId.value = SUB_1_ID + kosmos.connectivityRepository.fake.vcnSubId.value = SUB_1_ID whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) .thenReturn(false) @@ -289,9 +275,9 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_doesNotFilterProvisioningWhenFlagIsFalse() = - testScope.runTest { + kosmos.runTest { // GIVEN the flag is false - flags.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false) + featureFlagsClassic.fake.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, false) // GIVEN 1 sub that is in PROFILE_CLASS_PROVISIONING val sub1 = @@ -313,7 +299,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_filtersOutProvisioningSubs() = - testScope.runTest { + kosmos.runTest { val sub1 = SubscriptionModel( subscriptionId = SUB_1_ID, @@ -326,7 +312,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { subscriptionId = SUB_2_ID, isOpportunistic = false, carrierName = "Carrier 2", - profileClass = SubscriptionManager.PROFILE_CLASS_PROVISIONING, + profileClass = PROFILE_CLASS_PROVISIONING, ) connectionsRepository.setSubscriptions(listOf(sub1, sub2)) @@ -339,7 +325,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { /** Note: I'm not sure if this will ever be the case, but we can test it at least */ @Test fun filteredSubscriptions_filtersOutProvisioningSubsBeforeOpportunistic() = - testScope.runTest { + kosmos.runTest { // This is a contrived test case, where the active subId is the one that would // also be filtered by opportunistic filtering. @@ -376,7 +362,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_groupedPairAndNonProvisioned_groupedFilteringStillHappens() = - testScope.runTest { + kosmos.runTest { // Grouped filtering only happens when the list of subs is length 2. In this case // we'll show that filtering of provisioning subs happens before, and thus grouped // filtering happens even though the unfiltered list is length 3 @@ -406,7 +392,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() = - testScope.runTest { + kosmos.runTest { val notExclusivelyNonTerrestrialSub = SubscriptionModel( isExclusivelyNonTerrestrial = false, @@ -424,7 +410,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() = - testScope.runTest { + kosmos.runTest { val exclusivelyNonTerrestrialSub = SubscriptionModel( isExclusivelyNonTerrestrial = true, @@ -442,7 +428,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() = - testScope.runTest { + kosmos.runTest { val exclusivelyNonTerrestrialSub = SubscriptionModel( isExclusivelyNonTerrestrial = true, @@ -476,7 +462,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() = - testScope.runTest { + kosmos.runTest { // Exclusively non-terrestrial sub val exclusivelyNonTerrestrialSub = SubscriptionModel( @@ -507,7 +493,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun activeDataConnection_turnedOn() = - testScope.runTest { + kosmos.runTest { CONNECTION_1.setDataEnabled(true) val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled) @@ -517,7 +503,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun activeDataConnection_turnedOff() = - testScope.runTest { + kosmos.runTest { CONNECTION_1.setDataEnabled(true) val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled) @@ -528,7 +514,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun activeDataConnection_invalidSubId() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled) connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID) @@ -539,7 +525,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_default_validated_notFailed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true @@ -550,7 +536,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_notDefault_notValidated_notFailed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = false @@ -561,7 +547,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_default_notValidated_failed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true @@ -572,7 +558,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_carrierMergedDefault_notValidated_failed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.hasCarrierMergedConnection.value = true @@ -584,7 +570,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { /** Regression test for b/275076959. */ @Test fun failedConnection_dataSwitchInSameGroup_notFailed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true @@ -602,7 +588,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun failedConnection_dataSwitchNotInSameGroup_isFailed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true @@ -618,7 +604,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysShowDataRatIcon_configHasTrue() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.alwaysShowDataRatIcon) val config = MobileMappings.Config() @@ -630,7 +616,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysShowDataRatIcon_configHasFalse() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.alwaysShowDataRatIcon) val config = MobileMappings.Config() @@ -642,7 +628,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysUseCdmaLevel_configHasTrue() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.alwaysUseCdmaLevel) val config = MobileMappings.Config() @@ -654,7 +640,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun alwaysUseCdmaLevel_configHasFalse() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.alwaysUseCdmaLevel) val config = MobileMappings.Config() @@ -666,7 +652,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun isSingleCarrier_zeroSubscriptions_false() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isSingleCarrier) connectionsRepository.setSubscriptions(emptyList()) @@ -676,7 +662,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun isSingleCarrier_oneSubscription_true() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isSingleCarrier) connectionsRepository.setSubscriptions(listOf(SUB_1)) @@ -686,7 +672,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun isSingleCarrier_twoSubscriptions_false() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isSingleCarrier) connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) @@ -696,7 +682,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun isSingleCarrier_updates() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isSingleCarrier) connectionsRepository.setSubscriptions(listOf(SUB_1)) @@ -708,7 +694,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.mobileIsDefault) connectionsRepository.mobileIsDefault.value = false @@ -719,7 +705,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.mobileIsDefault) connectionsRepository.mobileIsDefault.value = true @@ -731,7 +717,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { /** Regression test for b/272586234. */ @Test fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.mobileIsDefault) connectionsRepository.mobileIsDefault.value = false @@ -742,7 +728,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun mobileIsDefault_updatesWhenRepoUpdates() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.mobileIsDefault) connectionsRepository.mobileIsDefault.value = true @@ -760,7 +746,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true @@ -774,17 +760,17 @@ class MobileIconsInteractorTest : SysuiTestCase() { // After 1s, the force validation bit is still present, so the connection is not marked // as failed - advanceTimeBy(1000) + testScope.advanceTimeBy(1000) assertThat(latest).isFalse() // After 2s, the force validation expires so the connection updates to failed - advanceTimeBy(1001) + testScope.advanceTimeBy(1001) assertThat(latest).isTrue() } @Test fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true @@ -798,7 +784,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) // GIVEN the network starts validated @@ -819,17 +805,17 @@ class MobileIconsInteractorTest : SysuiTestCase() { // THEN the forced validation bit is still used... assertThat(latest).isFalse() - advanceTimeBy(1000) + testScope.advanceTimeBy(1000) assertThat(latest).isFalse() // ... but expires after 2s - advanceTimeBy(1001) + testScope.advanceTimeBy(1001) assertThat(latest).isTrue() } @Test fun dataSwitch_whileAlreadyForcingValidation_resetsClock() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDefaultConnectionFailed) connectionsRepository.mobileIsDefault.value = true connectionsRepository.defaultConnectionIsValidated.value = true @@ -837,7 +823,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) - advanceTimeBy(1000) + testScope.advanceTimeBy(1000) // WHEN another change in same group event happens connectionsRepository.activeSubChangedInGroupEvent.emit(Unit) @@ -847,37 +833,37 @@ class MobileIconsInteractorTest : SysuiTestCase() { // THEN the forced validation remains for exactly 2 more seconds from now // 1.500s from second event - advanceTimeBy(1500) + testScope.advanceTimeBy(1500) assertThat(latest).isFalse() // 2.001s from the second event - advanceTimeBy(501) + testScope.advanceTimeBy(501) assertThat(latest).isTrue() } @Test fun isForceHidden_repoHasMobileHidden_true() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isForceHidden) - connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) + kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) assertThat(latest).isTrue() } @Test fun isForceHidden_repoDoesNotHaveMobileHidden_false() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isForceHidden) - connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) + kosmos.connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) assertThat(latest).isFalse() } @Test fun iconInteractor_cachedPerSubId() = - testScope.runTest { + kosmos.runTest { val interactor1 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) val interactor2 = underTest.getMobileConnectionInteractorForSubId(SUB_1_ID) @@ -887,7 +873,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { @Test fun deviceBasedEmergencyMode_emergencyCallsOnly_followsDeviceServiceStateFromRepo() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.isDeviceInEmergencyCallsOnlyMode) connectionsRepository.isDeviceEmergencyCallCapable.value = true diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt index 46f822aae7b8..db24d4bc8070 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt @@ -21,18 +21,18 @@ 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 androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest -import org.junit.runner.RunWith; +import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) 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 c4d2569bba89..b9cdd06de5a2 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 @@ -58,8 +58,8 @@ import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsVie 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.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt index 89da46544f1e..fb9e96c820cf 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -139,7 +139,9 @@ data class ClockMessageBuffers( /** Message buffer for large clock rendering */ val largeClockMessageBuffer: MessageBuffer, -) +) { + constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {} +} data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float) diff --git a/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml b/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml index 7c59aad10c91..71c77a56b6a8 100644 --- a/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml +++ b/packages/SystemUI/res/layout/status_bar_notification_footer_redesign.xml @@ -44,13 +44,13 @@ android:layout_height="wrap_content"> <com.android.systemui.statusbar.notification.row.FooterViewButton - android:id="@+id/settings_button" + android:id="@+id/history_button" style="@style/TextAppearance.NotificationFooterButtonRedesign" android:layout_width="wrap_content" android:layout_height="48dp" android:background="@drawable/notif_footer_btn_background" - android:contentDescription="@string/notification_settings_button_description" - android:drawableStart="@drawable/notif_footer_btn_settings" + android:contentDescription="@string/notification_history_button_description" + android:drawableStart="@drawable/notif_footer_btn_history" android:focusable="true" app:layout_constraintStart_toStartOf="parent" /> @@ -64,17 +64,17 @@ android:contentDescription="@string/accessibility_clear_all" android:focusable="true" android:text="@string/clear_all_notifications_text" - app:layout_constraintEnd_toStartOf="@id/history_button" - app:layout_constraintStart_toEndOf="@id/settings_button" /> + app:layout_constraintEnd_toStartOf="@id/settings_button" + app:layout_constraintStart_toEndOf="@id/history_button" /> <com.android.systemui.statusbar.notification.row.FooterViewButton - android:id="@+id/history_button" + android:id="@+id/settings_button" style="@style/TextAppearance.NotificationFooterButtonRedesign" android:layout_width="wrap_content" android:layout_height="48dp" android:background="@drawable/notif_footer_btn_background" - android:contentDescription="@string/notification_history_button_description" - android:drawableStart="@drawable/notif_footer_btn_history" + android:contentDescription="@string/notification_settings_button_description" + android:drawableStart="@drawable/notif_footer_btn_settings" android:focusable="true" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 917a4ff9036b..ccd953de7d03 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -31,7 +32,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -61,6 +61,7 @@ import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import com.android.systemui.utils.PolicyRestriction @@ -102,11 +103,12 @@ private fun BrightnessSlider( null } - val overriddenByAppState by if (Flags.showToastWhenAppControlBrightness()) { - viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle() - } else { - mutableStateOf(false) - } + val overriddenByAppState = + if (Flags.showToastWhenAppControlBrightness()) { + viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle().value + } else { + false + } PlatformSlider( value = animatedValue, @@ -160,7 +162,7 @@ private fun BrightnessSlider( if (interaction is DragInteraction.Start && overriddenByAppState) { viewModel.showToast( context, - R.string.quick_settings_brightness_unable_adjust_msg + R.string.quick_settings_brightness_unable_adjust_msg, ) } } @@ -213,7 +215,11 @@ fun BrightnessSliderContainer( coroutineScope.launch { viewModel.onDrag(Drag.Stopped(GammaBrightness(it))) } }, modifier = - Modifier.then(if (viewModel.showMirror) Modifier.drawInOverlay() else Modifier) + Modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(32.dp), + ) + .then(if (viewModel.showMirror) Modifier.drawInOverlay() else Modifier) .sliderBackground(containerColor) .fillMaxWidth(), formatter = viewModel::formatValue, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index b74ca035a229..35eed5e6a6d9 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -35,6 +35,7 @@ import com.android.systemui.keyguard.domain.interactor.TrustInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import javax.inject.Inject @@ -363,6 +364,9 @@ constructor( private val interactor: DeviceUnlockedInteractor, ) : CoreStartable { override fun start() { + if (!SceneContainerFlag.isEnabled) + return + applicationScope.launch { interactor.activate() } } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 1ffbbd2a9f32..b330ba376810 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -65,6 +65,9 @@ import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.navigationbar.gestural.domain.GestureInteractor; import com.android.systemui.navigationbar.gestural.domain.TaskMatcher; +import com.android.systemui.scene.domain.interactor.SceneInteractor; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; +import com.android.systemui.scene.shared.model.Scenes; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -162,6 +165,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private TouchMonitor mTouchMonitor; + private final SceneInteractor mSceneInteractor; private final CommunalInteractor mCommunalInteractor; private boolean mCommunalAvailable; @@ -378,6 +382,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ KeyguardUpdateMonitor keyguardUpdateMonitor, ScrimManager scrimManager, CommunalInteractor communalInteractor, + SceneInteractor sceneInteractor, SystemDialogsCloser systemDialogsCloser, UiEventLogger uiEventLogger, @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager, @@ -405,6 +410,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mDreamOverlayCallbackController = dreamOverlayCallbackController; mWindowTitle = windowTitle; mCommunalInteractor = communalInteractor; + mSceneInteractor = sceneInteractor; mSystemDialogsCloser = systemDialogsCloser; mGestureInteractor = gestureInteractor; mDreamOverlayComponentFactory = dreamOverlayComponentFactory; @@ -551,9 +557,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ @Override public void onWakeRequested() { mUiEventLogger.log(CommunalUiEvent.DREAM_TO_COMMUNAL_HUB_DREAM_AWAKE_START); - mCommunalInteractor.changeScene(CommunalScenes.Communal, - "dream wake requested", - null); + if (SceneContainerFlag.isEnabled()) { + // Scene interactor can only be modified on main thread. + mExecutor.execute(() -> mSceneInteractor.changeScene(Scenes.Communal, + "dream wake redirect to communal")); + } else { + mCommunalInteractor.changeScene(CommunalScenes.Communal, + "dream wake requested", + null); + } } private void updateGestureBlockingLocked() { @@ -617,7 +629,13 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mSystemDialogsCloser.closeSystemDialogs(); // Hide glanceable hub (this is a nop if glanceable hub is not open). - mCommunalInteractor.changeScene(CommunalScenes.Blank, "dream come to front", null); + if (SceneContainerFlag.isEnabled()) { + // Scene interactor can only be modified on main thread. + mExecutor.execute( + () -> mSceneInteractor.changeScene(Scenes.Dream, "closing hub to go to dream")); + } else { + mCommunalInteractor.changeScene(CommunalScenes.Blank, "dream come to front", null); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt index 8b6cc8cb4540..5a9e52ae5655 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt @@ -16,17 +16,66 @@ package com.android.systemui.dreams.ui.viewmodel +import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor +import com.android.systemui.scene.shared.model.SceneFamilies +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.shade.ui.viewmodel.dualShadeActions +import com.android.systemui.shade.ui.viewmodel.singleShadeActions +import com.android.systemui.shade.ui.viewmodel.splitShadeActions +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map /** Handles user input for the dream scene. */ -class DreamUserActionsViewModel @AssistedInject constructor() : UserActionsViewModel() { +class DreamUserActionsViewModel +@AssistedInject +constructor( + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, + private val shadeInteractor: ShadeInteractor, +) : UserActionsViewModel() { override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) { - setActions(emptyMap()) + shadeInteractor.isShadeTouchable + .flatMapLatestConflated { isShadeTouchable -> + if (!isShadeTouchable) { + flowOf(emptyMap()) + } else { + combine( + deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }, + shadeInteractor.shadeMode, + ) { isDeviceUnlocked, shadeMode -> + buildList { + add(Swipe.Start to Scenes.Communal) + + val bouncerOrGone = + if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer + add(Swipe.Up to bouncerOrGone) + + // "Home" is either Dream, Lockscreen, or Gone. + add(Swipe.End to SceneFamilies.Home) + + addAll( + when (shadeMode) { + ShadeMode.Single -> singleShadeActions() + ShadeMode.Split -> splitShadeActions() + ShadeMode.Dual -> dualShadeActions() + } + ) + } + .associate { it } + } + } + } + .collect { setActions(it) } } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt index ed7d1823648a..316964a47753 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt @@ -83,9 +83,6 @@ constructor( interactionState == TileInteractionState.LONG_CLICKED && animationState == TileAnimationState.ACTIVITY_LAUNCH -> TileHapticsState.LONG_PRESS - interactionState == TileInteractionState.LONG_CLICKED && - !tileViewModel.currentState.handlesLongClick -> - TileHapticsState.FAILED_LONGPRESS else -> TileHapticsState.NO_HAPTICS } } @@ -102,7 +99,6 @@ constructor( TileHapticsState.TOGGLE_ON -> MSDLToken.SWITCH_ON TileHapticsState.TOGGLE_OFF -> MSDLToken.SWITCH_OFF TileHapticsState.LONG_PRESS -> MSDLToken.LONG_PRESS - TileHapticsState.FAILED_LONGPRESS -> MSDLToken.FAILURE TileHapticsState.NO_HAPTICS -> null } tokenToPlay?.let { @@ -154,7 +150,6 @@ constructor( TOGGLE_ON, TOGGLE_OFF, LONG_PRESS, - FAILED_LONGPRESS, NO_HAPTICS, } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt index 7f8fbb5065aa..ec1d358b6bd2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt @@ -49,6 +49,7 @@ constructor( @Background private val backgroundScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val shortcutCategoriesUtils: ShortcutCategoriesUtils, + private val context: Context, ) : ShortcutCategoriesRepository { private val userContext: Context @@ -147,25 +148,23 @@ constructor( private fun fetchGroupLabelByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): String? { - return InputGestures.gestureToInternalKeyboardShortcutGroupLabelMap.getOrDefault( - keyGestureType, - null, - ) + InputGestures.gestureToInternalKeyboardShortcutGroupLabelMap[keyGestureType]?.let { + return context.getString(it) + } ?: return null } private fun fetchShortcutInfoLabelByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): String? { - return InputGestures.gestureToInternalKeyboardShortcutInfoLabelMap.getOrDefault( - keyGestureType, - null, - ) + InputGestures.gestureToInternalKeyboardShortcutInfoLabelMap[keyGestureType]?.let { + return context.getString(it) + } ?: return null } private fun fetchShortcutCategoryTypeByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): ShortcutCategoryType? { - return InputGestures.gestureToShortcutCategoryTypeMap.getOrDefault(keyGestureType, null) + return InputGestures.gestureToShortcutCategoryTypeMap[keyGestureType] } private data class InternalGroupsSource( diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt index 28134db4d588..90be9888e622 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt @@ -44,6 +44,7 @@ import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATI 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 +import com.android.systemui.res.R object InputGestures { val gestureToShortcutCategoryTypeMap = @@ -80,77 +81,97 @@ object InputGestures { KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to AppCategories, ) - // TODO move all string to to resources use the same resources as the original shortcuts - // - that way when the strings are translated there are no discrepancies val gestureToInternalKeyboardShortcutGroupLabelMap = mapOf( // System Category - KEY_GESTURE_TYPE_HOME to "System controls", - KEY_GESTURE_TYPE_RECENT_APPS to "System controls", - KEY_GESTURE_TYPE_BACK to "System controls", - KEY_GESTURE_TYPE_TAKE_SCREENSHOT to "System controls", - KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to "System controls", - KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to "System controls", - KEY_GESTURE_TYPE_LOCK_SCREEN to "System controls", - KEY_GESTURE_TYPE_ALL_APPS to "System controls", - KEY_GESTURE_TYPE_OPEN_NOTES to "System apps", - KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to "System apps", - KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to "System apps", - KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to "System apps", + KEY_GESTURE_TYPE_HOME to R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_RECENT_APPS to R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_BACK to R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_TAKE_SCREENSHOT to R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to + R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to + R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_LOCK_SCREEN to R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_ALL_APPS to R.string.shortcut_helper_category_system_controls, + KEY_GESTURE_TYPE_OPEN_NOTES to R.string.shortcut_helper_category_system_apps, + KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to + R.string.shortcut_helper_category_system_apps, + 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, // Multitasking Category - KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to "Recent apps", - KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to "Split screen", - KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to "Split screen", - KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to "Split screen", - KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to "Split screen", - KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to "Split screen", + KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.shortcutHelper_category_recent_apps, + KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to + R.string.shortcutHelper_category_split_screen, + KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to + R.string.shortcutHelper_category_split_screen, // App Category - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to "Applications", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to "Applications", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to "Applications", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to "Applications", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to "Applications", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to "Applications", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to "Applications", + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to + R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to + R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to + R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to + R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to R.string.keyboard_shortcut_group_applications, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to + R.string.keyboard_shortcut_group_applications, ) val gestureToInternalKeyboardShortcutInfoLabelMap = mapOf( // System Category - KEY_GESTURE_TYPE_HOME to "Go to home screen", - KEY_GESTURE_TYPE_RECENT_APPS to "View recent apps", - KEY_GESTURE_TYPE_BACK to "Go back", - KEY_GESTURE_TYPE_TAKE_SCREENSHOT to "Take screenshot", - KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to "Show shortcuts", - KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to "View notifications", - KEY_GESTURE_TYPE_LOCK_SCREEN to "Lock screen", - KEY_GESTURE_TYPE_ALL_APPS to "Open apps list", - KEY_GESTURE_TYPE_OPEN_NOTES to "Take a note", - KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to "Open settings", - KEY_GESTURE_TYPE_LAUNCH_ASSISTANT to "Open assistant", - KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT to "Open assistant", + KEY_GESTURE_TYPE_HOME to R.string.group_system_access_home_screen, + KEY_GESTURE_TYPE_RECENT_APPS to R.string.group_system_overview_open_apps, + KEY_GESTURE_TYPE_BACK to R.string.group_system_go_back, + KEY_GESTURE_TYPE_TAKE_SCREENSHOT to R.string.group_system_full_screenshot, + KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER to + R.string.group_system_access_system_app_shortcuts, + KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL to + R.string.group_system_access_notification_shade, + KEY_GESTURE_TYPE_LOCK_SCREEN to R.string.group_system_lock_screen, + KEY_GESTURE_TYPE_ALL_APPS to R.string.group_system_access_all_apps_search, + KEY_GESTURE_TYPE_OPEN_NOTES to R.string.group_system_quick_memo, + KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS to R.string.group_system_access_system_settings, + 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, // Multitasking Category - KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to "Cycle forward through recent apps", - KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to - "Use split screen with current app on the left", - KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to - "Use split screen with current app on the right", - KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to "Switch from split screen to full screen", + KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER to R.string.group_system_cycle_forward, + KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT to R.string.system_multitasking_lhs, + KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT to R.string.system_multitasking_rhs, + KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION to R.string.system_multitasking_full_screen, KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT to - "Switch to app on left or above while using split screen", + R.string.system_multitasking_splitscreen_focus_lhs, KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT to - "Switch to app on right or below while using split screen", + R.string.system_multitasking_splitscreen_focus_rhs, // App Category - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to "Calculator", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to "Calendar", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to "Chrome", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to "Contacts", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to "Gmail", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to "Maps", - KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to "Messages", + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR to + R.string.keyboard_shortcut_group_applications_calculator, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR to + R.string.keyboard_shortcut_group_applications_calendar, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER to + R.string.keyboard_shortcut_group_applications_browser, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS to + R.string.keyboard_shortcut_group_applications_contacts, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL to + R.string.keyboard_shortcut_group_applications_email, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS to + R.string.keyboard_shortcut_group_applications_maps, + KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to + R.string.keyboard_shortcut_group_applications_sms, ) } 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 e761c7313ff3..58ce194694df 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 @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.ui.composable import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.IndicationNodeFactory +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -126,8 +127,7 @@ fun SelectableShortcutSurface( .selectable( selected = selected, interactionSource = interactionSource, - indication = - ShortcutHelperIndication(interactionSource, interactionsConfig), + indication = ShortcutHelperIndication(interactionsConfig), enabled = enabled, onClick = onClick, ) @@ -181,8 +181,7 @@ fun ClickableShortcutSurface( ) .clickable( interactionSource = interactionSource, - indication = - ShortcutHelperIndication(interactionSource, interactionsConfig), + indication = ShortcutHelperIndication(interactionsConfig), enabled = enabled, onClick = onClick, ), @@ -507,10 +506,8 @@ private class ShortcutHelperInteractionsNode( } } -data class ShortcutHelperIndication( - private val interactionSource: InteractionSource, - private val interactionsConfig: InteractionsConfig, -) : IndicationNodeFactory { +data class ShortcutHelperIndication(private val interactionsConfig: InteractionsConfig) : + IndicationNodeFactory { override fun create(interactionSource: InteractionSource): DelegatableNode { return ShortcutHelperInteractionsNode(interactionSource, interactionsConfig) } @@ -529,3 +526,15 @@ data class InteractionsConfig( val hoverPadding: Dp = 0.dp, val pressedPadding: Dp = hoverPadding, ) + +@Composable +fun ProvideShortcutHelperIndication( + interactionsConfig: InteractionsConfig, + content: @Composable () -> Unit, +) { + CompositionLocalProvider( + LocalIndication provides ShortcutHelperIndication(interactionsConfig) + ) { + content() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index ec6a17b24989..21c45c550e08 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -51,6 +51,7 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -78,6 +79,7 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastRoundToInt import androidx.compose.ui.viewinterop.AndroidView @@ -102,6 +104,8 @@ import com.android.systemui.Dumpable import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dump.DumpManager +import com.android.systemui.keyboard.shortcut.ui.composable.InteractionsConfig +import com.android.systemui.keyboard.shortcut.ui.composable.ProvideShortcutHelperIndication import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.setSnapshotBinding import com.android.systemui.media.controls.ui.view.MediaHost @@ -240,51 +244,54 @@ constructor( @Composable private fun Content() { - PlatformTheme { - AnimatedVisibility( - visible = viewModel.isQsVisible, - modifier = - Modifier.graphicsLayer { alpha = viewModel.viewAlpha } - // Clipping before translation to match QSContainerImpl.onDraw - .offset { - IntOffset(x = 0, y = viewModel.viewTranslationY.fastRoundToInt()) - } - .thenIf(notificationScrimClippingParams.isEnabled) { - Modifier.notificationScrimClip { - notificationScrimClippingParams.params + PlatformTheme(isDarkTheme = true) { + ProvideShortcutHelperIndication(interactionsConfig = interactionsConfig()) { + AnimatedVisibility( + visible = viewModel.isQsVisible, + modifier = + Modifier.graphicsLayer { alpha = viewModel.viewAlpha } + // Clipping before translation to match QSContainerImpl.onDraw + .offset { + IntOffset(x = 0, y = viewModel.viewTranslationY.fastRoundToInt()) + } + .thenIf(notificationScrimClippingParams.isEnabled) { + Modifier.notificationScrimClip { + notificationScrimClippingParams.params + } } + // Disable touches in the whole composable while the mirror is showing. + // While the mirror is showing, an ancestor of the ComposeView is made + // alpha 0, but touches are still being captured by the composables. + .gesturesDisabled(viewModel.showingMirror), + ) { + val isEditing by + viewModel.containerViewModel.editModeViewModel.isEditing + .collectAsStateWithLifecycle() + val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS) + AnimatedContent( + targetState = isEditing, + transitionSpec = { + fadeIn(animationSpecEditMode) togetherWith + fadeOut(animationSpecEditMode) + }, + label = "EditModeAnimatedContent", + ) { editing -> + if (editing) { + val qqsPadding = viewModel.qqsHeaderHeight + EditMode( + viewModel = viewModel.containerViewModel.editModeViewModel, + modifier = + Modifier.fillMaxWidth() + .padding(top = { qqsPadding }) + .padding( + horizontal = { + QuickSettingsShade.Dimensions.Padding.roundToPx() + } + ), + ) + } else { + CollapsableQuickSettingsSTL() } - // Disable touches in the whole composable while the mirror is showing. - // While the mirror is showing, an ancestor of the ComposeView is made - // alpha 0, but touches are still being captured by the composables. - .gesturesDisabled(viewModel.showingMirror), - ) { - val isEditing by - viewModel.containerViewModel.editModeViewModel.isEditing - .collectAsStateWithLifecycle() - val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS) - AnimatedContent( - targetState = isEditing, - transitionSpec = { - fadeIn(animationSpecEditMode) togetherWith fadeOut(animationSpecEditMode) - }, - label = "EditModeAnimatedContent", - ) { editing -> - if (editing) { - val qqsPadding = viewModel.qqsHeaderHeight - EditMode( - viewModel = viewModel.containerViewModel.editModeViewModel, - modifier = - Modifier.fillMaxWidth() - .padding(top = { qqsPadding }) - .padding( - horizontal = { - QuickSettingsShade.Dimensions.Padding.roundToPx() - } - ), - ) - } else { - CollapsableQuickSettingsSTL() } } } @@ -1090,3 +1097,14 @@ private object ResIdTags { const val qsScroll = "expanded_qs_scroll_view" const val qsFooterActions = "qs_footer_actions" } + +@Composable +private fun interactionsConfig() = + InteractionsConfig( + hoverOverlayColor = MaterialTheme.colorScheme.onSurface, + hoverOverlayAlpha = 0.11f, + pressedOverlayColor = MaterialTheme.colorScheme.onSurface, + pressedOverlayAlpha = 0.15f, + // we are OK using this as our content is clipped and all corner radius are larger than this + surfaceCornerRadius = 28.dp, + ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 9029563b6321..e3de6d5152e4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -62,6 +62,7 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.asIndenting import com.android.systemui.util.kotlin.emitOnStart @@ -93,7 +94,7 @@ constructor( private val footerActionsController: FooterActionsController, private val sysuiStatusBarStateController: SysuiStatusBarStateController, deviceEntryInteractor: DeviceEntryInteractor, - disableFlagsRepository: DisableFlagsRepository, + DisableFlagsInteractor: DisableFlagsInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, @@ -182,8 +183,8 @@ constructor( val isQsEnabled by hydrator.hydratedStateOf( traceName = "isQsEnabled", - initialValue = disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(), - source = disableFlagsRepository.disableFlags.map { it.isQuickSettingsEnabled() }, + initialValue = DisableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(), + source = DisableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() }, ) var isInSplitShade by mutableStateOf(false) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 2efe500912cd..4e094cc77eae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -18,10 +18,13 @@ package com.android.systemui.qs.panels.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.Icon @@ -32,7 +35,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -41,6 +43,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType @@ -48,6 +51,7 @@ import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions. import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import javax.inject.Inject @@ -89,9 +93,24 @@ constructor( } Column { + val contentPaddingValue = + if (pages.size > 1) { + InterPageSpacing + } else { + 0.dp + } + val contentPadding = PaddingValues(horizontal = contentPaddingValue) + + /* Use negative padding equal with value equal to content padding. That way, each page + * layout extends to the sides, but the content is as if there was no padding. That + * way, the clipping bounds of the HorizontalPager extend beyond the tiles in each page. + */ HorizontalPager( state = pagerState, - modifier = Modifier.sysuiResTag("qs_pager"), + modifier = + Modifier.sysuiResTag("qs_pager") + .padding(horizontal = { -contentPaddingValue.roundToPx() }), + contentPadding = contentPadding, pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp, beyondViewportPageCount = 1, verticalAlignment = Alignment.Top, @@ -114,7 +133,13 @@ constructor( CompositionLocalProvider(value = LocalContentColor provides Color.White) { IconButton( onClick = editModeStart, - modifier = Modifier.align(Alignment.CenterEnd), + shape = RoundedCornerShape(CornerSize(28.dp)), + modifier = + Modifier.align(Alignment.CenterEnd) + .borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(FooterHeight / 2), + ), ) { Icon( imageVector = Icons.Default.Edit, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 177a5be35592..0e09ad29f4fd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -75,6 +74,7 @@ import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.SideIconWidth import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R private const val TEST_TAG_TOGGLE = "qs_tile_toggle_target" @@ -88,7 +88,7 @@ fun LargeTileContent( colors: TileColors, squishiness: () -> Float, accessibilityUiState: AccessibilityUiState? = null, - iconShape: Shape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), + iconShape: RoundedCornerShape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), toggleClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, ) { @@ -100,10 +100,12 @@ fun LargeTileContent( val longPressLabel = longPressLabel().takeIf { onLongClick != null } val animatedBackgroundColor by animateColorAsState(colors.iconBackground, label = "QSTileDualTargetBackgroundColor") + val focusBorderColor = MaterialTheme.colorScheme.secondary Box( modifier = Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) { - Modifier.clip(iconShape) + Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) + .clip(iconShape) .verticalSquish(squishiness) .drawBehind { drawRect(animatedBackgroundColor) } .combinedClickable( diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index fe59c4d36edc..cb57c6710553 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -49,6 +50,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalConfiguration @@ -59,6 +61,7 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -82,6 +85,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.panels.ui.viewmodel.toUiState import com.android.systemui.qs.tileimpl.QSTileImpl +import com.android.systemui.qs.ui.compose.borderOnFocus import com.android.systemui.res.R import java.util.function.Supplier import kotlinx.coroutines.CoroutineScope @@ -139,6 +143,7 @@ fun Tile( hapticsViewModel = hapticsViewModel, modifier = modifier + .borderOnFocus(color = MaterialTheme.colorScheme.secondary, tileShape.topEnd) .fillMaxWidth() .bounceable( bounceable = currentBounceableInfo.bounceable, @@ -381,7 +386,7 @@ private object TileDefaults { } @Composable - fun animateIconShape(state: Int): Shape { + fun animateIconShape(state: Int): RoundedCornerShape { return animateShape( state = state, activeCornerRadius = ActiveIconCornerRadius, @@ -390,7 +395,7 @@ private object TileDefaults { } @Composable - fun animateTileShape(state: Int): Shape { + fun animateTileShape(state: Int): RoundedCornerShape { return animateShape( state = state, activeCornerRadius = ActiveTileCornerRadius, @@ -399,7 +404,7 @@ private object TileDefaults { } @Composable - fun animateShape(state: Int, activeCornerRadius: Dp, label: String): Shape { + fun animateShape(state: Int, activeCornerRadius: Dp, label: String): RoundedCornerShape { val animatedCornerRadius by animateDpAsState( targetValue = @@ -410,7 +415,15 @@ private object TileDefaults { }, label = label, ) - return RoundedCornerShape(animatedCornerRadius) + + val corner = remember { + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } + } + } + } + return RoundedCornerShape(corner) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index 301ab2bcdd65..8f6c4e743269 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -1429,7 +1429,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi void makeOverlayToast(int stringId) { final Resources res = mContext.getResources(); - final SystemUIToast systemUIToast = mToastFactory.createToast(mContext, + final SystemUIToast systemUIToast = mToastFactory.createToast(mContext, mContext, res.getString(stringId), mContext.getPackageName(), UserHandle.myUserId(), res.getConfiguration().orientation); if (systemUIToast == null) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/compose/BorderOnFocus.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/compose/BorderOnFocus.kt new file mode 100644 index 000000000000..e6caa0d7520d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/compose/BorderOnFocus.kt @@ -0,0 +1,102 @@ +/* + * 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.qs.ui.compose + +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusEventModifierNode +import androidx.compose.ui.focus.FocusState +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Provides a rounded rect border when the element is focused. + * + * This should be used for elements that are themselves rounded rects. + */ +fun Modifier.borderOnFocus( + color: Color, + cornerSize: CornerSize, + strokeWidth: Dp = 3.dp, + padding: Dp = 2.dp, +) = this then BorderOnFocusElement(color, cornerSize, strokeWidth, padding) + +private class BorderOnFocusNode( + var color: Color, + var cornerSize: CornerSize, + var strokeWidth: Dp, + var padding: Dp, +) : FocusEventModifierNode, DrawModifierNode, Modifier.Node() { + + private var focused by mutableStateOf(false) + + override fun onFocusEvent(focusState: FocusState) { + focused = focusState.isFocused + } + + override fun ContentDrawScope.draw() { + drawContent() + val focusOutline = Rect(Offset.Zero, size).inflate(padding.toPx()) + if (focused) { + drawRoundRect( + color = color, + topLeft = focusOutline.topLeft, + size = focusOutline.size, + cornerRadius = CornerRadius(cornerSize.toPx(focusOutline.size, this)), + style = Stroke(strokeWidth.toPx()), + ) + } + } +} + +private data class BorderOnFocusElement( + val color: Color, + val cornerSize: CornerSize, + val strokeWidth: Dp, + val padding: Dp, +) : ModifierNodeElement<BorderOnFocusNode>() { + override fun create(): BorderOnFocusNode { + return BorderOnFocusNode(color, cornerSize, strokeWidth, padding) + } + + override fun update(node: BorderOnFocusNode) { + node.color = color + node.cornerSize = cornerSize + node.strokeWidth = strokeWidth + node.padding = padding + } + + override fun InspectorInfo.inspectableProperties() { + name = "borderOnFocus" + properties["color"] = color + properties["cornerSize"] = cornerSize + properties["strokeWidth"] = strokeWidth + properties["padding"] = padding + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt index 667827ac4724..c96ea03f057a 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -138,6 +138,7 @@ constructor( Overlays.QuickSettingsShade -> false Scenes.Bouncer -> false Scenes.Communal -> true + Scenes.Dream -> false Scenes.Gone -> true Scenes.Lockscreen -> true Scenes.QuickSettings -> false diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt index 41a3c8aff6cf..b89eb5c762e0 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/resolver/HomeSceneFamilyResolver.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import dagger.Binds @@ -46,6 +47,7 @@ class HomeSceneFamilyResolver constructor( @Application private val applicationScope: CoroutineScope, deviceEntryInteractor: DeviceEntryInteractor, + keyguardInteractor: KeyguardInteractor, keyguardEnabledInteractor: KeyguardEnabledInteractor, ) : SceneResolver { override val targetFamily: SceneKey = SceneFamilies.Home @@ -56,6 +58,7 @@ constructor( deviceEntryInteractor.canSwipeToEnter, deviceEntryInteractor.isDeviceEntered, deviceEntryInteractor.isUnlocked, + keyguardInteractor.isDreamingWithOverlay, transform = ::homeScene, ) .stateIn( @@ -67,7 +70,8 @@ constructor( canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value, isDeviceEntered = deviceEntryInteractor.isDeviceEntered.value, isUnlocked = deviceEntryInteractor.isUnlocked.value, - ) + isDreamingWithOverlay = false, + ), ) override fun includesScene(scene: SceneKey): Boolean = scene in homeScenes @@ -77,8 +81,11 @@ constructor( canSwipeToEnter: Boolean?, isDeviceEntered: Boolean, isUnlocked: Boolean, + isDreamingWithOverlay: Boolean, ): SceneKey = when { + // Dream can run even if Keyguard is disabled, thus it has the highest priority here. + isDreamingWithOverlay -> Scenes.Dream !isKeyguardEnabled -> Scenes.Gone canSwipeToEnter == true -> Scenes.Lockscreen !isDeviceEntered -> Scenes.Lockscreen @@ -91,6 +98,9 @@ constructor( setOf( Scenes.Gone, Scenes.Lockscreen, + // Dream is a home scene as the dream activity occludes keyguard and can show the + // shade on top. + Scenes.Dream, ) } } 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 daeaaa52fd94..9125d7e8bb0e 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 @@ -62,6 +62,7 @@ import com.android.systemui.scene.session.shared.SessionStorage import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.logger.SceneLogger import com.android.systemui.scene.shared.model.Overlays +import com.android.systemui.scene.shared.model.SceneFamilies import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.NotificationShadeWindowController @@ -217,7 +218,9 @@ constructor( sceneInteractor.transitionState.mapNotNull { state -> when (state) { is ObservableTransitionState.Idle -> { - if (state.currentScene != Scenes.Gone) { + if (state.currentScene == Scenes.Dream) { + false to "dream is showing" + } else if (state.currentScene != Scenes.Gone) { true to "scene is not Gone" } else if (state.currentOverlays.isNotEmpty()) { true to "overlay is shown" @@ -228,21 +231,30 @@ constructor( is ObservableTransitionState.Transition -> { if (state.fromContent == Scenes.Gone) { true to "scene transitioning away from Gone" + } else if (state.fromContent == Scenes.Dream) { + true to "scene transitioning away from dream" } else { null } } } }, + sceneInteractor.transitionState.map { state -> + state.isTransitioningFromOrTo(Scenes.Communal) || + state.isIdle(Scenes.Communal) + }, headsUpInteractor.isHeadsUpOrAnimatingAway, occlusionInteractor.invisibleDueToOcclusion, alternateBouncerInteractor.isVisible, ) { visibilityForTransitionState, + isCommunalShowing, isHeadsUpOrAnimatingAway, invisibleDueToOcclusion, isAlternateBouncerVisible -> when { + isCommunalShowing -> + true to "on or transitioning to/from communal" isHeadsUpOrAnimatingAway -> true to "showing a HUN" isAlternateBouncerVisible -> true to "showing alternate bouncer" invisibleDueToOcclusion -> false to "invisible due to occlusion" @@ -266,6 +278,7 @@ constructor( handleSimUnlock() handleDeviceUnlockStatus() handlePowerState() + handleDreamState() handleShadeTouchability() } @@ -506,6 +519,31 @@ constructor( } } + private fun handleDreamState() { + applicationScope.launch { + keyguardInteractor.isAbleToDream + .sample(sceneInteractor.transitionState, ::Pair) + .collect { (isAbleToDream, transitionState) -> + if (transitionState.isIdle(Scenes.Communal)) { + // The dream is automatically started underneath the hub, don't transition + // to dream when this is happening as communal is still visible on top. + return@collect + } + if (isAbleToDream) { + switchToScene( + targetSceneKey = Scenes.Dream, + loggingReason = "dream started", + ) + } else { + switchToScene( + targetSceneKey = SceneFamilies.Home, + loggingReason = "dream stopped", + ) + } + } + } + } + private fun handleShadeTouchability() { applicationScope.launch { shadeInteractor.isShadeTouchable diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java index 3a90d2b9df7b..503d0bfbc301 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -235,8 +235,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV if (mBrightnessWarningToast.isToastActive()) { return; } - mBrightnessWarningToast.show(mView.getContext(), - R.string.quick_settings_brightness_unable_adjust_msg); + mBrightnessWarningToast.show(mView.getContext(), resId); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt index dfbdaa62ec44..40260d0ca29f 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt @@ -37,11 +37,14 @@ constructor( private var toastView: View? = null fun show(viewContext: Context, @StringRes resId: Int) { + if (isToastActive()) { + return + } val res = viewContext.resources // Show the brightness warning toast with passing the toast inflation required context, // userId and resId from SystemUI package. val systemUIToast = toastFactory.createToast( - viewContext, + viewContext, viewContext, res.getString(resId), viewContext.packageName, viewContext.getUserId(), res.configuration.orientation ) @@ -79,13 +82,15 @@ constructor( val inAnimator = systemUIToast.inAnimation inAnimator?.start() - toastView!!.postDelayed({ + toastView?.postDelayed({ val outAnimator = systemUIToast.outAnimation if (outAnimator != null) { outAnimator.start() outAnimator.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animator: Animator) { - windowManager.removeViewImmediate(toastView) + if (isToastActive()) { + windowManager.removeViewImmediate(toastView) + } toastView = null } }) @@ -94,7 +99,7 @@ constructor( } fun isToastActive(): Boolean { - return toastView != null && toastView!!.isAttachedToWindow + return toastView?.isAttachedToWindow == true } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/shade/StatusBarLongPressGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt index ae36e81c7b1f..6fb3ca5f86d2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/StatusBarLongPressGestureDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt @@ -25,7 +25,7 @@ import javax.inject.Inject /** Accepts touch events, detects long press, and calls ShadeViewController#onStatusBarLongPress. */ @SysUISingleton -class StatusBarLongPressGestureDetector +class LongPressGestureDetector @Inject constructor(context: Context, val shadeViewController: ShadeViewController) { val gestureDetector = diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c15c8f946855..0e82bf82fdf9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2053,9 +2053,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } if (mQsController.getExpanded()) { mQsController.flingQs(0, FLING_COLLAPSE); - } else if (mBarState == KEYGUARD) { - mLockscreenShadeTransitionController.goToLockedShade( - /* expandedView= */null, /* needsQSAnimation= */false); } else { expand(true /* animate */); } @@ -3112,7 +3109,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump if (isTracking()) { onTrackingStopped(true); } - if (isExpanded() && mBarState != KEYGUARD && !mQsController.getExpanded()) { + if (isExpanded() && !mQsController.getExpanded()) { mShadeLog.d("Status Bar was long pressed. Expanding to QS."); expandToQs(); } else { @@ -5094,13 +5091,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } boolean handled = mHeadsUpTouchHelper.onTouchEvent(event); - // This touch session has already resulted in shade expansion. Ignore everything else. - if (ShadeExpandsOnStatusBarLongPress.isEnabled() - && event.getActionMasked() != MotionEvent.ACTION_DOWN - && event.getDownTime() == mStatusBarLongPressDowntime) { - mShadeLog.d("Touch has same down time as Status Bar long press. Ignoring."); - return false; - } if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && mQsController.handleTouch( event, isFullyCollapsed(), isShadeOrQsHeightAnimationRunning())) { if (event.getActionMasked() != MotionEvent.ACTION_MOVE) { @@ -5108,6 +5098,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } return true; } + // This touch session has already resulted in shade expansion. Ignore everything else. + if (ShadeExpandsOnStatusBarLongPress.isEnabled() + && event.getActionMasked() != MotionEvent.ACTION_DOWN + && event.getDownTime() == mStatusBarLongPressDowntime) { + mShadeLog.d("Touch has same down time as Status Bar long press. Ignoring."); + return false; + } if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); handled = true; diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index 460bfbbcb3ab..a653ca2f80a9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -25,7 +25,7 @@ import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor @@ -47,7 +47,7 @@ class ShadeInteractorImpl constructor( @Application val scope: CoroutineScope, deviceProvisioningInteractor: DeviceProvisioningInteractor, - disableFlagsRepository: DisableFlagsRepository, + disableFlagsInteractor: DisableFlagsInteractor, dozeParams: DozeParameters, keyguardRepository: KeyguardRepository, keyguardTransitionInteractor: KeyguardTransitionInteractor, @@ -61,13 +61,13 @@ constructor( BaseShadeInteractor by baseShadeInteractor, ShadeModeInteractor by shadeModeInteractor { override val isShadeEnabled: StateFlow<Boolean> = - disableFlagsRepository.disableFlags + disableFlagsInteractor.disableFlags .map { it.isShadeEnabled() } .flowName("isShadeEnabled") .stateIn(scope, SharingStarted.Eagerly, initialValue = false) override val isQsEnabled: StateFlow<Boolean> = - disableFlagsRepository.disableFlags + disableFlagsInteractor.disableFlags .map { it.isQuickSettingsEnabled() } .flowName("isQsEnabled") .stateIn(scope, SharingStarted.Eagerly, initialValue = false) @@ -114,7 +114,7 @@ constructor( override val isExpandToQsEnabled: Flow<Boolean> = combine( - disableFlagsRepository.disableFlags, + disableFlagsInteractor.disableFlags, isShadeEnabled, keyguardRepository.isDozing, userSetupRepository.isUserSetUp, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index da04f6edf9e2..b2ca33a4aecf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -705,6 +705,7 @@ public class StatusBarStateControllerImpl implements final boolean onBouncer = currentScene.equals(Scenes.Bouncer); final boolean onCommunal = currentScene.equals(Scenes.Communal); final boolean onGone = currentScene.equals(Scenes.Gone); + final boolean onDream = currentScene.equals(Scenes.Dream); final boolean onLockscreen = currentScene.equals(Scenes.Lockscreen); final boolean onQuickSettings = currentScene.equals(Scenes.QuickSettings); final boolean onShade = currentScene.equals(Scenes.Shade); @@ -765,6 +766,8 @@ public class StatusBarStateControllerImpl implements // We get here if deviceUnlockStatus.isUnlocked is false but we are no longer on or over // a keyguardish scene; we want to return SHADE_LOCKED until isUnlocked is also true. newState = StatusBarState.SHADE_LOCKED; + } else if (onDream) { + newState = StatusBarState.SHADE_LOCKED; } else { throw new IllegalArgumentException( "unhandled input to calculateStateFromSceneFramework: " + inputLogString); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipLogTags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipLogTags.kt new file mode 100644 index 000000000000..6c1d6c52088f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipLogTags.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.statusbar.chips + +/** Helper class to ensure all tags used in [StatusBarChipsLog] are exactly the same length. */ +object StatusBarChipLogTags { + private const val TAG_LENGTH = 20 + + fun String.pad(): String { + return this.padEnd(TAG_LENGTH) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt index eaefc111ae3d..bb0467f10e16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel @@ -47,6 +48,6 @@ constructor( .stateIn(scope, SharingStarted.Lazily, OngoingCallModel.NoCall) companion object { - private const val TAG = "OngoingCall" + private val TAG = "OngoingCall".pad() } } 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 e82525810c64..b8cdd2587774 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 @@ -28,6 +28,7 @@ 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.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor import com.android.systemui.statusbar.chips.ui.model.ColorsModel @@ -112,7 +113,7 @@ constructor( ActivityTransitionAnimator.Controller.fromView( backgroundView, InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, - ) + ), ) } } @@ -121,10 +122,8 @@ constructor( private val phoneIcon = Icon.Resource( com.android.internal.R.drawable.ic_phone, - ContentDescription.Resource( - R.string.ongoing_phone_call_content_description, - ), + ContentDescription.Resource(R.string.ongoing_phone_call_content_description), ) - private const val TAG = "CallVM" + private val TAG = "CallVM".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt index 7c95f1e42080..b3dbf299e7cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.mediarouter.data.repository.MediaRouterRepository +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel import com.android.systemui.statusbar.policy.CastDevice @@ -68,6 +69,6 @@ constructor( } companion object { - private const val TAG = "MediaRouter" + private val TAG = "MediaRouter".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt index 11072068bef9..3422337523f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt @@ -28,6 +28,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor.MediaRouterChipInteractor import com.android.systemui.statusbar.chips.casttootherdevice.domain.model.MediaRouterCastModel @@ -255,6 +256,6 @@ constructor( companion object { @DrawableRes val CAST_TO_OTHER_DEVICE_ICON = R.drawable.ic_cast_connected - private const val TAG = "CastToOtherVM" + private val TAG = "CastToOtherVM".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt index 27b2465d52b3..af238f697a18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor import android.content.pm.PackageManager +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -25,6 +26,7 @@ import com.android.systemui.log.core.LogLevel import com.android.systemui.mediaprojection.MediaProjectionUtils.packageHasCastingCapabilities import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel import javax.inject.Inject @@ -33,7 +35,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** * Interactor for media projection events, used to show chips in the status bar for share-to-app and @@ -108,6 +109,6 @@ constructor( } companion object { - private const val TAG = "MediaProjection" + private val TAG = "MediaProjection".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt index e3dc70af5fe6..f5952f4804fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import javax.inject.Inject @@ -143,6 +144,6 @@ constructor( } companion object { - private const val TAG = "ScreenRecord" + private val TAG = "ScreenRecord".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt index eb735211a970..0065593c7b73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt @@ -30,6 +30,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.res.R import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor @@ -84,7 +85,7 @@ constructor( Icon.Resource( ICON, ContentDescription.Resource( - R.string.screenrecord_ongoing_screen_only, + R.string.screenrecord_ongoing_screen_only ), ) ), @@ -153,6 +154,6 @@ constructor( companion object { @DrawableRes val ICON = R.drawable.ic_screenrecord - private const val TAG = "ScreenRecordVM" + private val TAG = "ScreenRecordVM".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt index 11d077fc09f1..2af86a51cf70 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt @@ -28,6 +28,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.res.R +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel @@ -179,6 +180,6 @@ constructor( companion object { @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all - private const val TAG = "ShareToAppVM" + private val TAG = "ShareToAppVM".pad() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt index ed325970ebb2..45efc57685f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt @@ -20,6 +20,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.call.ui.viewmodel.CallChipViewModel import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel @@ -347,7 +348,7 @@ constructor( } companion object { - private const val TAG = "ChipsViewModel" + private val TAG = "ChipsViewModel".pad() private val DEFAULT_INTERNAL_HIDDEN_MODEL = InternalChipModel.Hidden( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt index 9004e5d12663..aeeb0427d24b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt @@ -22,7 +22,7 @@ import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.DisableFlagsRepositoryLog import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.disableflags.DisableFlagsLogger -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler import javax.inject.Inject import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt new file mode 100644 index 000000000000..4f1b97841fde --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.disableflags.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +@SysUISingleton +class DisableFlagsInteractor @Inject constructor(repository: DisableFlagsRepository) { + /** A model of the disable flags last received from [IStatusBar]. */ + val disableFlags: StateFlow<DisableFlagsModel> = repository.disableFlags +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt index ce25cf5895c1..6507237cfc24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt @@ -1,18 +1,20 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * 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. + * 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.disableflags.data.model +package com.android.systemui.statusbar.disableflags.shared.model import android.app.StatusBarManager.DISABLE2_NONE import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt index 8079ce540e1b..4ea597a12ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt @@ -17,17 +17,15 @@ package com.android.systemui.statusbar.notification.domain.interactor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor import javax.inject.Inject /** Interactor for notification alerting. */ @SysUISingleton class NotificationAlertsInteractor @Inject -constructor( - private val disableFlagsRepository: DisableFlagsRepository, -) { +constructor(private val disableFlagsInteractor: DisableFlagsInteractor) { /** Returns true if notification alerts are allowed. */ fun areNotificationAlertsEnabled(): Boolean = - disableFlagsRepository.disableFlags.value.areNotificationAlertsEnabled() + disableFlagsInteractor.disableFlags.value.areNotificationAlertsEnabled() } 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 80c8e8b2a109..db294934c9ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -171,14 +171,12 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; -import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionListener; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shade.ShadeLogger; import com.android.systemui.shade.ShadeSurface; import com.android.systemui.shade.ShadeViewController; -import com.android.systemui.shade.StatusBarLongPressGestureDetector; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.statusbar.phone.BarTransitions; import com.android.systemui.statusbar.AutoHideUiElement; @@ -368,7 +366,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private PhoneStatusBarViewController mPhoneStatusBarViewController; private PhoneStatusBarTransitions mStatusBarTransitions; - private final Provider<StatusBarLongPressGestureDetector> mStatusBarLongPressGestureDetector; private final AuthRippleController mAuthRippleController; @WindowVisibleState private int mStatusBarWindowState = WINDOW_STATE_SHOWING; private final NotificationShadeWindowController mNotificationShadeWindowController; @@ -674,7 +671,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { ShadeController shadeController, WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor, StatusBarKeyguardViewManager statusBarKeyguardViewManager, - Provider<StatusBarLongPressGestureDetector> statusBarLongPressGestureDetector, ViewMediatorCallback viewMediatorCallback, InitController initController, @Named(TIME_TICK_HANDLER_NAME) Handler timeTickHandler, @@ -782,7 +778,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mShadeController = shadeController; mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; - mStatusBarLongPressGestureDetector = statusBarLongPressGestureDetector; mKeyguardViewMediatorCallback = viewMediatorCallback; mInitController = initController; mPluginDependencyProvider = pluginDependencyProvider; @@ -1532,11 +1527,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // to touch outside the customizer to close it, such as on the status or nav bar. mShadeController.onStatusBarTouch(event); } - if (ShadeExpandsOnStatusBarLongPress.isEnabled() - && mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { - mStatusBarLongPressGestureDetector.get().handleTouch(event); - } - return getNotificationShadeWindowView().onTouchEvent(event); }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 176dd8de6cd4..91c43ddf1ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -39,8 +39,8 @@ import com.android.systemui.Dependency; import com.android.systemui.Flags; import com.android.systemui.Gefingerpoken; import com.android.systemui.res.R; +import com.android.systemui.shade.LongPressGestureDetector; import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress; -import com.android.systemui.shade.StatusBarLongPressGestureDetector; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; @@ -69,7 +69,7 @@ public class PhoneStatusBarView extends FrameLayout { private InsetsFetcher mInsetsFetcher; private int mDensity; private float mFontScale; - private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector; + private LongPressGestureDetector mLongPressGestureDetector; /** * Draw this many pixels into the left/right side of the cutout to optimally use the space @@ -81,10 +81,9 @@ public class PhoneStatusBarView extends FrameLayout { mStatusBarWindowControllerStore = Dependency.get(StatusBarWindowControllerStore.class); } - void setLongPressGestureDetector( - StatusBarLongPressGestureDetector statusBarLongPressGestureDetector) { + void setLongPressGestureDetector(LongPressGestureDetector longPressGestureDetector) { if (ShadeExpandsOnStatusBarLongPress.isEnabled()) { - mStatusBarLongPressGestureDetector = statusBarLongPressGestureDetector; + mLongPressGestureDetector = longPressGestureDetector; } } @@ -208,9 +207,8 @@ public class PhoneStatusBarView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { - if (ShadeExpandsOnStatusBarLongPress.isEnabled() - && mStatusBarLongPressGestureDetector != null) { - mStatusBarLongPressGestureDetector.handleTouch(event); + if (ShadeExpandsOnStatusBarLongPress.isEnabled() && mLongPressGestureDetector != null) { + mLongPressGestureDetector.handleTouch(event); } if (mTouchEventHandler == null) { Log.w( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 16e023ce17fd..a94db490df0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -34,11 +34,11 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.ui.view.WindowRootView +import com.android.systemui.shade.LongPressGestureDetector import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController -import com.android.systemui.shade.StatusBarLongPressGestureDetector import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore @@ -69,7 +69,7 @@ private constructor( private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, private val panelExpansionInteractor: PanelExpansionInteractor, - private val statusBarLongPressGestureDetector: Provider<StatusBarLongPressGestureDetector>, + private val longPressGestureDetector: Provider<LongPressGestureDetector>, private val windowRootView: Provider<WindowRootView>, private val shadeLogger: ShadeLogger, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, @@ -119,7 +119,7 @@ private constructor( addCursorSupportToIconContainers() if (ShadeExpandsOnStatusBarLongPress.isEnabled) { - mView.setLongPressGestureDetector(statusBarLongPressGestureDetector.get()) + mView.setLongPressGestureDetector(longPressGestureDetector.get()) } progressProvider?.setReadyToHandleTransition(true) @@ -336,7 +336,7 @@ private constructor( private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, private val panelExpansionInteractor: PanelExpansionInteractor, - private val statusBarLongPressGestureDetector: Provider<StatusBarLongPressGestureDetector>, + private val longPressGestureDetector: Provider<LongPressGestureDetector>, private val windowRootView: Provider<WindowRootView>, private val shadeLogger: ShadeLogger, private val viewUtil: ViewUtil, @@ -361,7 +361,7 @@ private constructor( shadeController, shadeViewController, panelExpansionInteractor, - statusBarLongPressGestureDetector, + longPressGestureDetector, windowRootView, shadeLogger, statusBarMoveFromCenterAnimationController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt index 9164da721e3a..b2a0272c06d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.pipeline.shared.domain.interactor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor import com.android.systemui.statusbar.pipeline.shared.domain.model.StatusBarDisableFlagsVisibilityModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -30,13 +30,13 @@ import kotlinx.coroutines.flow.map @SysUISingleton class CollapsedStatusBarInteractor @Inject -constructor(disableFlagsRepository: DisableFlagsRepository) { +constructor(DisableFlagsInteractor: DisableFlagsInteractor) { /** * The visibilities of various status bar child views, based only on the information we received * from disable flags. */ val visibilityViaDisableFlags: Flow<StatusBarDisableFlagsVisibilityModel> = - disableFlagsRepository.disableFlags.map { + DisableFlagsInteractor.disableFlags.map { StatusBarDisableFlagsVisibilityModel( isClockAllowed = it.isClockEnabled, areNotificationIconsAllowed = it.areNotificationIconsEnabled, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index c7b6be3fc4ac..a1b56d6688d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -112,8 +112,7 @@ constructor( selectedUserContext .flatMapLatest { currentContext -> // flatMapLatest because when selectedUserContext emits a new value, we want - // to - // re-create a whole flow + // to re-create a whole flow callbackFlow { val callback = object : WifiPickerTracker.WifiPickerTrackerCallback { @@ -132,20 +131,16 @@ constructor( .map { it.toWifiNetworkModel() } // [WifiPickerTracker.connectedWifiEntry] will return the - // same - // instance but with updated internals. For example, when - // its - // validation status changes from false to true, the same - // instance is re-used but with the validated field updated. + // same instance but with updated internals. For example, + // when its validation status changes from false to true, + // the same instance is re-used but with the validated + // field updated. // // Because it's the same instance, the flow won't re-emit - // the - // value (even though the internals have changed). So, we - // need - // to transform it into our internal model immediately. - // [toWifiNetworkModel] always returns a new instance, so - // the - // flow is guaranteed to emit. + // the value (even though the internals have changed). So, + // we need to transform it into our internal model + // immediately. [toWifiNetworkModel] always returns a new + // instance, so the flow is guaranteed to emit. send( newPrimaryNetwork = connectedEntry?.toPrimaryWifiNetworkModel() @@ -235,20 +230,15 @@ constructor( .map { it.toWifiNetworkModel() } // [WifiPickerTracker.connectedWifiEntry] will return the same - // instance - // but with updated internals. For example, when its validation - // status - // changes from false to true, the same instance is re-used but - // with the - // validated field updated. + // instance but with updated internals. For example, when its + // validation status changes from false to true, the same + // instance is re-used but with the validated field updated. // // Because it's the same instance, the flow won't re-emit the - // value - // (even though the internals have changed). So, we need to - // transform it - // into our internal model immediately. [toWifiNetworkModel] - // always - // returns a new instance, so the flow is guaranteed to emit. + // value (even though the internals have changed). So, we need + // to transform it into our internal model immediately. + // [toWifiNetworkModel] always returns a new instance, so the + // flow is guaranteed to emit. send( newPrimaryNetwork = connectedEntry?.toPrimaryWifiNetworkModel() @@ -292,12 +282,10 @@ constructor( .create(applicationContext, lifecycle, callback, "WifiRepository") .apply { // By default, [WifiPickerTracker] will scan to see all - // available wifi - // networks in the area. Because SysUI only needs to display the - // **connected** network, we don't need scans to be running (and - // in fact, - // running scans is costly and should be avoided whenever - // possible). + // available wifi networks in the area. Because SysUI only + // needs to display the **connected** network, we don't + // need scans to be running (and in fact, running scans is + // costly and should be avoided whenever possible). this?.disableScanning() } // The lifecycle must be STARTED in order for the callback to receive @@ -382,16 +370,11 @@ constructor( private fun MergedCarrierEntry.convertCarrierMergedToModel(): WifiNetworkModel { // WifiEntry instance values aren't guaranteed to be stable between method calls - // because - // WifiPickerTracker is continuously updating the same object. Save the level in a - // local - // variable so that checking the level validity here guarantees that the level will - // still be - // valid when we create the `WifiNetworkModel.Active` instance later. Otherwise, the - // level - // could be valid here but become invalid later, and `WifiNetworkModel.Active` will - // throw - // an exception. See b/362384551. + // because WifiPickerTracker is continuously updating the same object. Save the + // level in a local variable so that checking the level validity here guarantees + // that the level will still be valid when we create the `WifiNetworkModel.Active` + // instance later. Otherwise, the level could be valid here but become invalid + // later, and `WifiNetworkModel.Active` will throw an exception. See b/362384551. return WifiNetworkModel.CarrierMerged.of( subscriptionId = this.subscriptionId, diff --git a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java index d97cae2a99e3..d367455d26c7 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java +++ b/packages/SystemUI/src/com/android/systemui/toast/SystemUIToast.java @@ -50,6 +50,7 @@ import com.android.systemui.plugins.ToastPlugin; public class SystemUIToast implements ToastPlugin.Toast { static final String TAG = "SystemUIToast"; final Context mContext; + final Context mDisplayContext; final CharSequence mText; final ToastPlugin.Toast mPluginToast; @@ -68,17 +69,18 @@ public class SystemUIToast implements ToastPlugin.Toast { @Nullable private final Animator mInAnimator; @Nullable private final Animator mOutAnimator; - SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, - String packageName, int userId, int orientation) { - this(layoutInflater, context, text, null, packageName, userId, + SystemUIToast(LayoutInflater layoutInflater, Context applicationContext, Context displayContext, + CharSequence text, String packageName, int userId, int orientation) { + this(layoutInflater, applicationContext, displayContext, text, null, packageName, userId, orientation); } - SystemUIToast(LayoutInflater layoutInflater, Context context, CharSequence text, - ToastPlugin.Toast pluginToast, String packageName, @UserIdInt int userId, - int orientation) { + SystemUIToast(LayoutInflater layoutInflater, Context applicationContext, Context displayContext, + CharSequence text, ToastPlugin.Toast pluginToast, String packageName, + @UserIdInt int userId, int orientation) { mLayoutInflater = layoutInflater; - mContext = context; + mContext = applicationContext; + mDisplayContext = displayContext; mText = text; mPluginToast = pluginToast; mPackageName = packageName; @@ -221,9 +223,9 @@ public class SystemUIToast implements ToastPlugin.Toast { mPluginToast.onOrientationChange(orientation); } - mDefaultY = mContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset); + mDefaultY = mDisplayContext.getResources().getDimensionPixelSize(R.dimen.toast_y_offset); mDefaultGravity = - mContext.getResources().getInteger(R.integer.config_toastDefaultGravity); + mDisplayContext.getResources().getInteger(R.integer.config_toastDefaultGravity); } private Animator createInAnimator() { diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java index 9ae66749aa0a..388d4bd6780b 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastFactory.java @@ -65,15 +65,16 @@ public class ToastFactory implements Dumpable { /** * Create a toast to be shown by ToastUI. */ - public SystemUIToast createToast(Context context, CharSequence text, String packageName, - int userId, int orientation) { - LayoutInflater layoutInflater = LayoutInflater.from(context); + public SystemUIToast createToast(Context applicationContext, Context displayContext, + CharSequence text, String packageName, int userId, int orientation) { + LayoutInflater layoutInflater = LayoutInflater.from(displayContext); if (isPluginAvailable()) { - return new SystemUIToast(layoutInflater, context, text, mPlugin.createToast(text, - packageName, userId), packageName, userId, orientation); + return new SystemUIToast(layoutInflater, applicationContext, displayContext, text, + mPlugin.createToast(text, packageName, userId), packageName, userId, + orientation); } - return new SystemUIToast(layoutInflater, context, text, packageName, userId, - orientation); + return new SystemUIToast(layoutInflater, applicationContext, displayContext, text, + packageName, userId, orientation); } private boolean isPluginAvailable() { diff --git a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java index 32a4f12777ac..12f73b8a887d 100644 --- a/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java +++ b/packages/SystemUI/src/com/android/systemui/toast/ToastUI.java @@ -135,8 +135,8 @@ public class ToastUI implements return; } Context displayContext = context.createDisplayContext(display); - mToast = mToastFactory.createToast(displayContext /* sysuiContext */, text, packageName, - userHandle.getIdentifier(), mOrientation); + mToast = mToastFactory.createToast(mContext, displayContext /* sysuiContext */, text, + packageName, userHandle.getIdentifier(), mOrientation); if (mToast.getInAnimation() != null) { mToast.getInAnimation().start(); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index 65b62737b692..2aa6e7b18154 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -105,8 +105,7 @@ class ClockEventControllerTest : SysuiTestCase() { private val mainExecutor = ImmediateExecutor() private lateinit var repository: FakeKeyguardRepository - private val messageBuffer = LogcatOnlyMessageBuffer(LogLevel.DEBUG) - private val clockBuffers = ClockMessageBuffers(messageBuffer, messageBuffer, messageBuffer) + private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG)) private lateinit var underTest: ClockEventController @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher 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 a940bc9b3e20..425aad2bd43c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt @@ -16,10 +16,12 @@ import android.view.RemoteAnimationAdapter import android.view.RemoteAnimationTarget import android.view.SurfaceControl import android.view.ViewGroup +import android.view.WindowManager.TRANSIT_NONE import android.widget.FrameLayout import android.widget.LinearLayout import android.window.RemoteTransition import android.window.TransitionFilter +import android.window.WindowAnimationState import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -34,6 +36,10 @@ import junit.framework.Assert.assertTrue import junit.framework.AssertionFailedError import kotlin.concurrent.thread import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before @@ -258,7 +264,6 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() { - val controller = object : DelegateTransitionAnimatorController(controller) { override val transitionCookie = @@ -273,7 +278,6 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) @Test fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() { - // No TransitionCookie val controllerWithoutCookie = object : DelegateTransitionAnimatorController(controller) { @@ -348,7 +352,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { fun doesNotStartIfAnimationIsCancelled() { val runner = activityTransitionAnimator.createRunner(controller) runner.onAnimationCancelled() - runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback) + runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() verify(controller).onTransitionAnimationCancelled() @@ -361,7 +365,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun cancelsIfNoOpeningWindowIsFound() { val runner = activityTransitionAnimator.createRunner(controller) - runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback) + runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback) waitForIdleSync() verify(controller).onTransitionAnimationCancelled() @@ -374,7 +378,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { @Test fun startsAnimationIfWindowIsOpening() { val runner = activityTransitionAnimator.createRunner(controller) - runner.onAnimationStart(0, arrayOf(fakeWindow()), emptyArray(), emptyArray(), iCallback) + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) waitForIdleSync() verify(listener).onTransitionAnimationStart() verify(controller).onTransitionAnimationStart(anyBoolean()) @@ -387,6 +397,113 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { } } + @DisableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) + @Test + fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() { + assertThrows(IllegalStateException::class.java) { + activityTransitionAnimator.createRunner(controller, initializeLazily = true) + } + } + + @EnableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) + @Test + fun runnerCreatesDelegateLazily_whenPostingTimeouts() { + val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = true) + assertNull(runner.delegate) + runner.postTimeouts() + assertNotNull(runner.delegate) + } + + @EnableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) + @Test + fun runnerCreatesDelegateLazily_onAnimationStart() { + val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = true) + assertNull(runner.delegate) + + // The delegate is cleaned up after execution (which happens in another thread), so what we + // do instead is check if it becomes non-null at any point with a 1 second timeout. This + // will tell us that takeOverWithAnimation() triggered the lazy initialization. + var delegateInitialized = false + runBlocking { + val initChecker = launch { + withTimeout(1.seconds) { + while (runner.delegate == null) continue + delegateInitialized = true + } + } + runner.onAnimationStart( + TRANSIT_NONE, + arrayOf(fakeWindow()), + emptyArray(), + emptyArray(), + iCallback, + ) + initChecker.join() + } + assertTrue(delegateInitialized) + } + + @EnableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) + @Test + fun runnerCreatesDelegateLazily_onAnimationTakeover() { + val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = true) + assertNull(runner.delegate) + + // The delegate is cleaned up after execution (which happens in another thread), so what we + // do instead is check if it becomes non-null at any point with a 1 second timeout. This + // will tell us that takeOverWithAnimation() triggered the lazy initialization. + var delegateInitialized = false + runBlocking { + val initChecker = launch { + withTimeout(1.seconds) { + while (runner.delegate == null) continue + delegateInitialized = true + } + } + runner.takeOverAnimation( + arrayOf(fakeWindow()), + arrayOf(WindowAnimationState()), + SurfaceControl.Transaction(), + iCallback, + ) + initChecker.join() + } + assertTrue(delegateInitialized) + } + + @DisableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) + @Test + fun animationTakeoverThrows_whenTheFlagsAreDisabled() { + val runner = activityTransitionAnimator.createRunner(controller, initializeLazily = false) + assertThrows(IllegalStateException::class.java) { + runner.takeOverAnimation( + arrayOf(fakeWindow()), + emptyArray(), + SurfaceControl.Transaction(), + iCallback, + ) + } + } + + @DisableFlags( + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY, + Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED, + ) @Test fun disposeRunner_delegateDereferenced() { val runner = activityTransitionAnimator.createRunner(controller) @@ -409,7 +526,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() { false, Rect(), Rect(), - 0, + 1, Point(), Rect(), bounds, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java index 0b9c06f2dbe2..5ada2f3fd63d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java @@ -222,7 +222,7 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { when(SubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(SUB_ID); SubscriptionInfo info = mock(SubscriptionInfo.class); when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(info); - when(mToastFactory.createToast(any(), anyString(), anyString(), anyInt(), anyInt())) + when(mToastFactory.createToast(any(), any(), anyString(), anyString(), anyInt(), anyInt())) .thenReturn(mSystemUIToast); when(mSystemUIToast.getView()).thenReturn(mToastView); when(mSystemUIToast.getGravity()).thenReturn(GRAVITY_FLAGS); @@ -275,8 +275,8 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { mInternetDialogController.connectCarrierNetwork(); verify(mMergedCarrierEntry).connect(null /* callback */, false /* showToast */); - verify(mToastFactory).createToast(any(), eq(TOAST_MESSAGE_STRING), anyString(), anyInt(), - anyInt()); + verify(mToastFactory).createToast(any(), any(), eq(TOAST_MESSAGE_STRING), anyString(), + anyInt(), anyInt()); } @Test @@ -288,7 +288,7 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { mInternetDialogController.connectCarrierNetwork(); verify(mMergedCarrierEntry, never()).connect(null /* callback */, false /* showToast */); - verify(mToastFactory, never()).createToast(any(), anyString(), anyString(), anyInt(), + verify(mToastFactory, never()).createToast(any(), any(), anyString(), anyString(), anyInt(), anyInt()); } @@ -302,7 +302,7 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { mInternetDialogController.connectCarrierNetwork(); verify(mMergedCarrierEntry, never()).connect(null /* callback */, false /* showToast */); - verify(mToastFactory, never()).createToast(any(), anyString(), anyString(), anyInt(), + verify(mToastFactory, never()).createToast(any(), any(), anyString(), anyString(), anyInt(), anyInt()); } @@ -321,7 +321,7 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { mInternetDialogController.connectCarrierNetwork(); verify(mMergedCarrierEntry, never()).connect(null /* callback */, false /* showToast */); - verify(mToastFactory, never()).createToast(any(), anyString(), anyString(), anyInt(), + verify(mToastFactory, never()).createToast(any(), any(), anyString(), anyString(), anyInt(), anyInt()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt index 8bd8b72e5527..2812bd334b0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt @@ -15,11 +15,16 @@ */ package systemui.shared.clocks.view +import android.graphics.Typeface import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.clocks.ClockMessageBuffers +import com.android.systemui.plugins.clocks.ClockSettings +import com.android.systemui.shared.clocks.ClockContext import com.android.systemui.shared.clocks.FontTextStyle import com.android.systemui.shared.clocks.LogUtil +import com.android.systemui.shared.clocks.TypefaceCache import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView import org.junit.Assert.assertEquals import org.junit.Before @@ -38,7 +43,23 @@ class SimpleDigitalClockTextViewTest : SysuiTestCase() { @Before fun setup() { - underTest = SimpleDigitalClockTextView(context, messageBuffer) + underTest = + SimpleDigitalClockTextView( + ClockContext( + context, + context.resources, + ClockSettings(), + TypefaceCache(messageBuffer) { + // TODO(b/364680873): Move constant to config_clockFontFamily when shipping + return@TypefaceCache Typeface.create( + "google-sans-flex-clock", + Typeface.NORMAL, + ) + }, + ClockMessageBuffers(messageBuffer), + messageBuffer, + ) + ) underTest.textStyle = FontTextStyle() underTest.aodStyle = FontTextStyle() underTest.text = "0" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index a8618eb544d4..3a46d038f946 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -20,9 +20,8 @@ import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel -import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout @@ -31,7 +30,6 @@ import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController -import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.statusbar.policy.fakeConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -144,7 +142,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { context = context, configurationController = configurationController, dumpManager = mock(), - splitShadeStateController = ResourcesSplitShadeStateController() + splitShadeStateController = ResourcesSplitShadeStateController(), ), keyguardTransitionControllerFactory = { notificationPanelController -> LockscreenShadeKeyguardTransitionController( @@ -153,7 +151,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { context = context, configurationController = configurationController, dumpManager = mock(), - splitShadeStateController = ResourcesSplitShadeStateController() + splitShadeStateController = ResourcesSplitShadeStateController(), ) }, depthController = depthController, @@ -171,7 +169,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { splitShadeStateController = ResourcesSplitShadeStateController(), shadeLockscreenInteractorLazy = { shadeLockscreenInteractor }, naturalScrollingSettingObserver = naturalScrollingSettingObserver, - lazyQSSceneAdapter = { qsSceneAdapter } + lazyQSSceneAdapter = { qsSceneAdapter }, ) transitionController.addCallback(transitionControllerCallback) @@ -229,7 +227,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED) assertFalse( "Waking to shade locked when not dozing", - transitionController.isWakingToShadeLocked + transitionController.isWakingToShadeLocked, ) } @@ -247,9 +245,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { fun testDontGoWhenShadeDisabled() = testScope.runTest { disableFlagsRepository.disableFlags.value = - DisableFlagsModel( - disable2 = DISABLE2_NOTIFICATION_SHADE, - ) + DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE) testScope.runCurrent() transitionController.goToLockedShade(null) verify(statusbarStateController, never()).setState(anyInt()) @@ -454,7 +450,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { val distance = 10 context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_scrim_transition_distance, - distance + distance, ) configurationController.notifyConfigurationChanged() @@ -463,7 +459,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController) .transitionToFullShadeProgress( progress = eq(0.5f), - lockScreenNotificationsProgress = anyFloat() + lockScreenNotificationsProgress = anyFloat(), ) } @@ -474,11 +470,11 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { val delay = 10 context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_notifications_scrim_transition_distance, - distance + distance, ) context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_notifications_scrim_transition_delay, - delay + delay, ) configurationController.notifyConfigurationChanged() @@ -487,7 +483,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController) .transitionToFullShadeProgress( progress = anyFloat(), - lockScreenNotificationsProgress = eq(0.1f) + lockScreenNotificationsProgress = eq(0.1f), ) } @@ -498,11 +494,11 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { val delay = 50 context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_notifications_scrim_transition_distance, - distance + distance, ) context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_notifications_scrim_transition_delay, - delay + delay, ) configurationController.notifyConfigurationChanged() @@ -511,7 +507,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController) .transitionToFullShadeProgress( progress = anyFloat(), - lockScreenNotificationsProgress = eq(0f) + lockScreenNotificationsProgress = eq(0f), ) } @@ -522,11 +518,11 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { val delay = 50 context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_notifications_scrim_transition_distance, - distance + distance, ) context.orCreateTestableResources.addOverride( R.dimen.lockscreen_shade_notifications_scrim_transition_delay, - delay + delay, ) configurationController.notifyConfigurationChanged() @@ -535,7 +531,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { verify(scrimController) .transitionToFullShadeProgress( progress = anyFloat(), - lockScreenNotificationsProgress = eq(1f) + lockScreenNotificationsProgress = eq(1f), ) } @@ -627,7 +623,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { */ private fun ScrimController.transitionToFullShadeProgress( progress: Float, - lockScreenNotificationsProgress: Float + lockScreenNotificationsProgress: Float, ) { setTransitionToFullShadeProgress(progress, lockScreenNotificationsProgress) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index b142fc2deea9..c9ada7e7f5ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -155,7 +155,6 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeControllerImpl; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.shade.ShadeLogger; -import com.android.systemui.shade.StatusBarLongPressGestureDetector; import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.KeyboardShortcutListSearch; @@ -175,6 +174,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.core.StatusBarInitializerImpl; +import com.android.systemui.statusbar.core.StatusBarOrchestrator; import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository; import com.android.systemui.statusbar.data.repository.StatusBarModePerDisplayRepository; import com.android.systemui.statusbar.notification.NotifPipelineFlags; @@ -372,7 +372,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private EmergencyGestureIntentFactory mEmergencyGestureIntentFactory; @Mock private NotificationSettingsInteractor mNotificationSettingsInteractor; @Mock private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager; - @Mock private StatusBarLongPressGestureDetector mStatusBarLongPressGestureDetector; + @Mock private StatusBarOrchestrator mStatusBarOrchestrator; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); private final FakeGlobalSettings mFakeGlobalSettings = new FakeGlobalSettings(); @@ -607,7 +607,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mShadeController, mWindowRootViewVisibilityInteractor, mStatusBarKeyguardViewManager, - () -> mStatusBarLongPressGestureDetector, mViewMediatorCallback, mInitController, new Handler(TestableLooper.get(this).getLooper()), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 69efa87a9cac..638f195df00c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -40,13 +40,14 @@ import com.android.systemui.flags.Flags import com.android.systemui.plugins.fakeDarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView +import com.android.systemui.shade.LongPressGestureDetector import com.android.systemui.shade.ShadeControllerImpl import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController -import com.android.systemui.shade.StatusBarLongPressGestureDetector import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.data.repository.statusBarContentInsetsProviderStore import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.window.StatusBarWindowStateController @@ -97,7 +98,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var windowRootView: Provider<WindowRootView> @Mock private lateinit var shadeLogger: ShadeLogger @Mock private lateinit var viewUtil: ViewUtil - @Mock private lateinit var mStatusBarLongPressGestureDetector: StatusBarLongPressGestureDetector + @Mock private lateinit var longPressGestureDetector: LongPressGestureDetector private lateinit var statusBarWindowStateController: StatusBarWindowStateController private lateinit var view: PhoneStatusBarView @@ -394,7 +395,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { shadeControllerImpl, shadeViewController, panelExpansionInteractor, - { mStatusBarLongPressGestureDetector }, + { longPressGestureDetector }, windowRootView, shadeLogger, viewUtil, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt new file mode 100644 index 000000000000..b24b3ad05117 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.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.dreams.ui.viewmodel + +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.dreamUserActionsViewModel by + Kosmos.Fixture { + DreamUserActionsViewModel( + deviceUnlockedInteractor = deviceUnlockedInteractor, + shadeInteractor = shadeInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt index 32469b6cfc30..ad38bbeba1a8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt @@ -23,13 +23,13 @@ import java.io.PrintWriter class FakeFeatureFlagsClassic : FakeFeatureFlags() +val FeatureFlagsClassic.fake + get() = this as FakeFeatureFlagsClassic + @Deprecated( message = "Use FakeFeatureFlagsClassic instead.", replaceWith = - ReplaceWith( - "FakeFeatureFlagsClassic", - "com.android.systemui.flags.FakeFeatureFlagsClassic", - ), + ReplaceWith("FakeFeatureFlagsClassic", "com.android.systemui.flags.FakeFeatureFlagsClassic"), ) open class FakeFeatureFlags : FeatureFlagsClassic { private val booleanFlags = mutableMapOf<String, Boolean>() @@ -105,6 +105,7 @@ open class FakeFeatureFlags : FeatureFlagsClassic { listener.onFlagChanged( object : FlagListenable.FlagEvent { override val flagName = flag.name + override fun requestNoRestart() {} } ) @@ -165,7 +166,7 @@ open class FakeFeatureFlags : FeatureFlagsClassic { @Module(includes = [FakeFeatureFlagsClassicModule.Bindings::class]) class FakeFeatureFlagsClassicModule( - @get:Provides val fakeFeatureFlagsClassic: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic(), + @get:Provides val fakeFeatureFlagsClassic: FakeFeatureFlagsClassic = FakeFeatureFlagsClassic() ) { constructor( @@ -175,7 +176,9 @@ class FakeFeatureFlagsClassicModule( @Module interface Bindings { @Binds fun bindFake(fake: FakeFeatureFlagsClassic): FeatureFlagsClassic + @Binds fun bindClassic(classic: FeatureFlagsClassic): FeatureFlags + @Binds fun bindFakeClassic(fake: FakeFeatureFlagsClassic): FakeFeatureFlags } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index f52f039b6758..903bc8ebf42d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -109,6 +109,7 @@ val Kosmos.customShortcutCategoriesRepository by applicationCoroutineScope, testDispatcher, shortcutCategoriesUtils, + applicationContext, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 3cd613b21f4b..3d60abf59d62 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -71,6 +71,8 @@ import com.android.systemui.shade.shadeController import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel import com.android.systemui.statusbar.data.repository.fakeStatusBarModePerDisplayRepository +import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor @@ -78,7 +80,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.heads import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.phone.keyguardBypassController import com.android.systemui.statusbar.phone.scrimController -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.fakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.wifiInteractor import com.android.systemui.statusbar.policy.configurationController @@ -126,7 +128,7 @@ class KosmosJavaAdapter() { val keyguardStatusBarViewModel by lazy { kosmos.keyguardStatusBarViewModel } val powerRepository by lazy { kosmos.fakePowerRepository } val clock by lazy { kosmos.systemClock } - val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository } + val mobileConnectionsRepository by lazy { kosmos.mobileConnectionsRepository } val simBouncerInteractor by lazy { kosmos.simBouncerInteractor } val statusBarStateController by lazy { kosmos.statusBarStateController } val statusBarModePerDisplayRepository by lazy { kosmos.fakeStatusBarModePerDisplayRepository } @@ -183,4 +185,6 @@ class KosmosJavaAdapter() { val lockscreenToGlanceableHubTransitionViewModel by lazy { kosmos.lockscreenToGlanceableHubTransitionViewModel } + val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor } + val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt index 4ed491233f3c..45d5b387fea0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt @@ -33,7 +33,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.mediaInRowInLandscapeViewMode import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.transition.largeScreenShadeInterpolator -import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor import com.android.systemui.statusbar.sysuiStatusBarStateController import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -51,7 +51,7 @@ val Kosmos.qsFragmentComposeViewModelFactory by footerActionsController, sysuiStatusBarStateController, deviceEntryInteractor, - disableFlagsRepository, + disableFlagsInteractor, keyguardTransitionInteractor, largeScreenShadeInterpolator, configurationInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt index 8b124258909a..a4a63ec6ca21 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/resolver/SceneResolverKosmos.kt @@ -21,6 +21,7 @@ package com.android.systemui.scene.domain.resolver import com.android.compose.animation.scene.SceneKey import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.scene.shared.model.SceneFamilies @@ -34,6 +35,7 @@ val Kosmos.homeSceneFamilyResolver by HomeSceneFamilyResolver( applicationScope = applicationCoroutineScope, deviceEntryInteractor = deviceEntryInteractor, + keyguardInteractor = keyguardInteractor, keyguardEnabledInteractor = keyguardEnabledInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt index 39f58aea82ef..af6d6249b4a8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt @@ -25,7 +25,7 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.ShadeModule import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.policy.data.repository.userSetupRepository import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor @@ -60,7 +60,7 @@ val Kosmos.shadeInteractorImpl by ShadeInteractorImpl( scope = applicationCoroutineScope, deviceProvisioningInteractor = deviceProvisioningInteractor, - disableFlagsRepository = disableFlagsRepository, + disableFlagsInteractor = disableFlagsInteractor, dozeParams = dozeParameters, keyguardRepository = fakeKeyguardRepository, keyguardTransitionInteractor = keyguardTransitionInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt index 466a3eb83e95..9dbb547a434d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt @@ -15,7 +15,7 @@ package com.android.systemui.statusbar.disableflags.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel +import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel import dagger.Binds import dagger.Module import javax.inject.Inject diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt new file mode 100644 index 000000000000..7b4b047c130a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt @@ -0,0 +1,25 @@ +/* + * 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.disableflags.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository + +val Kosmos.disableFlagsInteractor by Fixture { + DisableFlagsInteractor(repository = disableFlagsRepository) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt index d76defef3c97..99ed4f0db64d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/airplane/domain/interactor/AirplaneModeInteractorKosmos.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar.pipeline.airplane.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository val Kosmos.airplaneModeInteractor: AirplaneModeInteractor by @@ -26,6 +26,6 @@ val Kosmos.airplaneModeInteractor: AirplaneModeInteractor by AirplaneModeInteractor( FakeAirplaneModeRepository(), FakeConnectivityRepository(), - fakeMobileConnectionsRepository, + mobileConnectionsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index de73d3397db3..bfd46b664242 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -77,11 +77,7 @@ class FakeMobileConnectionsRepository( override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] - ?: FakeMobileConnectionRepository( - subId, - tableLogBuffer, - ) - .also { subIdRepos[subId] = it } + ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it } } override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config()) @@ -135,3 +131,6 @@ class FakeMobileConnectionsRepository( const val LTE_ADVANCED_PRO = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO } } + +val MobileConnectionsRepository.fake + get() = this as FakeMobileConnectionsRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt index cd22f1dd6acc..b952d71b157e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt @@ -19,12 +19,20 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.log.table.logcatTableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy + +val Kosmos.mobileMappingsProxy: MobileMappingsProxy by Fixture { FakeMobileMappingsProxy() } + +var Kosmos.mobileConnectionsRepositoryLogbufferName by Fixture { "FakeMobileConnectionsRepository" } val Kosmos.fakeMobileConnectionsRepository by Fixture { FakeMobileConnectionsRepository( - tableLogBuffer = logcatTableLogBuffer(this, "FakeMobileConnectionsRepository"), + mobileMappings = mobileMappingsProxy, + tableLogBuffer = logcatTableLogBuffer(this, mobileConnectionsRepositoryLogbufferName), ) } -val Kosmos.mobileConnectionsRepository by - Fixture<MobileConnectionsRepository> { fakeMobileConnectionsRepository } +val Kosmos.mobileConnectionsRepository: MobileConnectionsRepository by Fixture { + fakeMobileConnectionsRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt index 8e656cf002ce..00bfa994aabd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt @@ -18,7 +18,5 @@ package com.android.systemui.statusbar.pipeline.shared.data.repository import com.android.systemui.kosmos.Kosmos -val Kosmos.fakeConnectivityRepository: FakeConnectivityRepository by - Kosmos.Fixture { FakeConnectivityRepository() } val Kosmos.connectivityRepository: ConnectivityRepository by - Kosmos.Fixture { fakeConnectivityRepository } + Kosmos.Fixture { FakeConnectivityRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt index 331e2fad19cb..c69d9a29a9b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt @@ -42,10 +42,7 @@ class FakeConnectivityRepository : ConnectivityRepository { * validated */ @JvmOverloads - fun setMobileConnected( - default: Boolean = true, - validated: Boolean = true, - ) { + fun setMobileConnected(default: Boolean = true, validated: Boolean = true) { defaultConnections.value = DefaultConnectionModel( mobile = DefaultConnectionModel.Mobile(default), @@ -55,10 +52,7 @@ class FakeConnectivityRepository : ConnectivityRepository { /** Similar convenience method for ethernet */ @JvmOverloads - fun setEthernetConnected( - default: Boolean = true, - validated: Boolean = true, - ) { + fun setEthernetConnected(default: Boolean = true, validated: Boolean = true) { defaultConnections.value = DefaultConnectionModel( ethernet = DefaultConnectionModel.Ethernet(default), @@ -67,10 +61,7 @@ class FakeConnectivityRepository : ConnectivityRepository { } @JvmOverloads - fun setWifiConnected( - default: Boolean = true, - validated: Boolean = true, - ) { + fun setWifiConnected(default: Boolean = true, validated: Boolean = true) { defaultConnections.value = DefaultConnectionModel( wifi = DefaultConnectionModel.Wifi(default), @@ -78,3 +69,6 @@ class FakeConnectivityRepository : ConnectivityRepository { ) } } + +val ConnectivityRepository.fake + get() = this as FakeConnectivityRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt index 385a813996ff..13fde9608017 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.pipeline.shared.domain.interactor import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository +import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor val Kosmos.collapsedStatusBarInteractor: CollapsedStatusBarInteractor by - Kosmos.Fixture { CollapsedStatusBarInteractor(fakeDisableFlagsRepository) } + Kosmos.Fixture { CollapsedStatusBarInteractor(disableFlagsInteractor) } diff --git a/ravenwood/scripts/run-ravenwood-tests.sh b/ravenwood/scripts/run-ravenwood-tests.sh index fe2269a8dc38..27c5ea1bd0d7 100755 --- a/ravenwood/scripts/run-ravenwood-tests.sh +++ b/ravenwood/scripts/run-ravenwood-tests.sh @@ -33,7 +33,7 @@ include_re="" exclude_re="" smoke_exclude_re="" dry_run="" -while getopts "sx:f:dt" opt; do +while getopts "sx:f:dtb" opt; do case "$opt" in s) # Remove slow tests. @@ -52,8 +52,13 @@ case "$opt" in dry_run="echo" ;; t) + # Redirect log to terminal export RAVENWOOD_LOG_OUT=$(tty) ;; + b) + # Build only + ATEST=m + ;; '?') exit 1 ;; @@ -99,11 +104,16 @@ done # Calculate the removed tests. -diff="$(diff <(echo "${all_tests[@]}" | tr ' ' '\n') <(echo "${targets[@]}" | tr ' ' '\n') )" +diff="$(diff <(echo "${all_tests[@]}" | tr ' ' '\n') <(echo "${targets[@]}" | tr ' ' '\n') | grep -v [0-9] )" if [[ "$diff" != "" ]]; then echo "Excluded tests:" echo "$diff" fi -$dry_run ${ATEST:-atest} "${targets[@]}" +run() { + echo "Running: ${@}" + "${@}" +} + +run $dry_run ${ATEST:-atest} "${targets[@]}" diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index cd2a535aa2c5..e59bb42fd666 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -28,8 +28,11 @@ import android.app.ActivityManager; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.WindowNode; +import android.app.slice.Slice; +import android.app.slice.SliceItem; import android.content.ComponentName; import android.content.Context; +import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.metrics.LogMaker; import android.os.UserHandle; @@ -97,11 +100,12 @@ public final class Helper { @UserIdInt int userId, @NonNull RemoteViews rView) { final AtomicBoolean permissionsOk = new AtomicBoolean(true); - rView.visitUris(uri -> { - int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri); - boolean allowed = uriOwnerId == userId; - permissionsOk.set(allowed & permissionsOk.get()); - }); + rView.visitUris( + uri -> { + int uriOwnerId = android.content.ContentProvider.getUserIdFromUri(uri, userId); + boolean allowed = uriOwnerId == userId; + permissionsOk.set(allowed & permissionsOk.get()); + }); return permissionsOk.get(); } @@ -150,6 +154,47 @@ public final class Helper { return (ok ? rView : null); } + /** + * Checks the URI permissions of the icon in the slice, to see if the current userId is able to + * access it. + * + * <p>Returns null if slice contains user inaccessible icons + * + * <p>TODO: instead of returning a null Slice when the current userId cannot access an icon, + * return a reconstructed Slice without the icons. This is currently non-trivial since there are + * no public methods to generically add SliceItems to Slices + */ + public static @Nullable Slice sanitizeSlice(Slice slice) { + if (slice == null) { + return null; + } + + int userId = ActivityManager.getCurrentUser(); + + // Recontruct the Slice, filtering out bad icons + for (SliceItem sliceItem : slice.getItems()) { + if (!sliceItem.getFormat().equals(SliceItem.FORMAT_IMAGE)) { + // Not an image slice + continue; + } + + Icon icon = sliceItem.getIcon(); + if (icon.getType() != Icon.TYPE_URI + && icon.getType() != Icon.TYPE_URI_ADAPTIVE_BITMAP) { + // No URIs to sanitize + continue; + } + + int iconUriId = android.content.ContentProvider.getUserIdFromUri(icon.getUri(), userId); + + if (iconUriId != userId) { + Slog.w(TAG, "sanitizeSlice() user: " + userId + " cannot access icons in Slice"); + return null; + } + } + + return slice; + } @Nullable static AutofillId[] toArray(@Nullable ArraySet<AutofillId> set) { diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java index 38a412fa063d..50a26b355537 100644 --- a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java @@ -27,6 +27,7 @@ import android.service.autofill.InlinePresentation; import android.util.Slog; import com.android.server.LocalServices; +import com.android.server.autofill.Helper; import com.android.server.autofill.RemoteInlineSuggestionRenderService; import com.android.server.inputmethod.InputMethodManagerInternal; @@ -83,6 +84,10 @@ final class RemoteInlineSuggestionViewConnector { */ public boolean renderSuggestion(int width, int height, @NonNull IInlineSuggestionUiCallback callback) { + if (Helper.sanitizeSlice(mInlinePresentation.getSlice()) == null) { + if (sDebug) Slog.d(TAG, "Skipped rendering inline suggestion."); + return false; + } if (mRemoteRenderService != null) { if (sDebug) Slog.d(TAG, "Request to recreate the UI"); mRemoteRenderService.renderSuggestion(callback, mInlinePresentation, width, height, diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index 446b3671a94a..c6cb67f4efa8 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -553,7 +553,7 @@ class BroadcastController { } BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId, receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps, - exported, mService.mPlatformCompat); + exported, callerApp.info, mService.mPlatformCompat); if (rl.containsFilter(filter)) { Slog.w(TAG, "Receiver with filter " + filter + " already registered for pid " + rl.pid diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java index 3c7fb52b11b4..e20c46cebc5a 100644 --- a/services/core/java/com/android/server/am/BroadcastFilter.java +++ b/services/core/java/com/android/server/am/BroadcastFilter.java @@ -20,6 +20,8 @@ import android.annotation.Nullable; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.os.Binder; import android.os.UserHandle; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -40,7 +42,7 @@ public final class BroadcastFilter extends IntentFilter { @ChangeId @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) @VisibleForTesting - static final long CHANGE_RESTRICT_PRIORITY_VALUES = 371309185L; + static final long RESTRICT_PRIORITY_VALUES = 371309185L; // Back-pointer to the list this filter is in. final ReceiverList receiverList; @@ -58,7 +60,7 @@ public final class BroadcastFilter extends IntentFilter { BroadcastFilter(IntentFilter _filter, ReceiverList _receiverList, String _packageName, String _featureId, String _receiverId, String _requiredPermission, int _owningUid, int _userId, boolean _instantApp, boolean _visibleToInstantApp, - boolean _exported, PlatformCompat platformCompat) { + boolean _exported, ApplicationInfo applicationInfo, PlatformCompat platformCompat) { super(_filter); receiverList = _receiverList; packageName = _packageName; @@ -71,7 +73,8 @@ public final class BroadcastFilter extends IntentFilter { visibleToInstantApp = _visibleToInstantApp; exported = _exported; initialPriority = getPriority(); - setPriority(calculateAdjustedPriority(owningUid, initialPriority, platformCompat)); + setPriority(calculateAdjustedPriority(owningUid, initialPriority, + applicationInfo, platformCompat)); } public @Nullable String getReceiverClassName() { @@ -125,13 +128,18 @@ public final class BroadcastFilter extends IntentFilter { @VisibleForTesting static int calculateAdjustedPriority(int owningUid, int priority, - PlatformCompat platformCompat) { + ApplicationInfo applicationInfo, PlatformCompat platformCompat) { if (!Flags.restrictPriorityValues()) { return priority; } - if (!platformCompat.isChangeEnabledByUidInternalNoLogging( - CHANGE_RESTRICT_PRIORITY_VALUES, owningUid)) { - return priority; + final long token = Binder.clearCallingIdentity(); + try { + if (!platformCompat.isChangeEnabledInternalNoLogging( + RESTRICT_PRIORITY_VALUES, applicationInfo)) { + return priority; + } + } finally { + Binder.restoreCallingIdentity(token); } if (!UserHandle.isCore(owningUid)) { if (priority >= SYSTEM_HIGH_PRIORITY) { diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 38df10a0bc8c..e8ce1731f739 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -88,7 +88,7 @@ final class BroadcastRecord extends Binder { @ChangeId @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) @VisibleForTesting - static final long CHANGE_LIMIT_PRIORITY_SCOPE = 371307720L; + static final long LIMIT_PRIORITY_SCOPE = 371307720L; final @NonNull Intent intent; // the original intent that generated us final @Nullable ComponentName targetComp; // original component name set on the intent @@ -781,7 +781,7 @@ final class BroadcastRecord extends Binder { } else { if (Flags.limitPriorityScope()) { final boolean[] changeEnabled = calculateChangeStateForReceivers( - receivers, CHANGE_LIMIT_PRIORITY_SCOPE, platformCompat); + receivers, LIMIT_PRIORITY_SCOPE, platformCompat); // Priority of the previous tranche int lastTranchePriority = 0; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 5a2610b00772..abb756b294a3 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -2460,6 +2460,15 @@ public final class DisplayManagerService extends SystemService { DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED); } + private void handleLogicalDisplayRefreshRateChangedLocked(@NonNull LogicalDisplay display) { + sendDisplayEventIfEnabledLocked(display, + DisplayManagerGlobal.EVENT_DISPLAY_REFRESH_RATE_CHANGED); + } + + private void handleLogicalDisplayStateChangedLocked(@NonNull LogicalDisplay display) { + sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED); + } + private void notifyDefaultDisplayDeviceUpdated(LogicalDisplay display) { mDisplayModeDirector.defaultDisplayDeviceUpdated(display.getPrimaryDisplayDeviceLocked() .mDisplayDeviceConfig); @@ -3991,6 +4000,12 @@ public final class DisplayManagerService extends SystemService { case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED: handleLogicalDisplayDisconnectedLocked(display); break; + case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED: + handleLogicalDisplayRefreshRateChangedLocked(display); + break; + case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED: + handleLogicalDisplayStateChangedLocked(display); + break; } } @@ -4198,6 +4213,13 @@ public final class DisplayManagerService extends SystemService { return (mask & DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0; + case DisplayManagerGlobal.EVENT_DISPLAY_REFRESH_RATE_CHANGED: + return (mask + & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE) != 0; + case DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED: + return (mask & DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_STATE) != 0; default: // This should never happen. Slog.e(TAG, "Unknown display event " + event); diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 09fa4e6aa628..c0903a9bafac 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -79,15 +79,18 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // 'adb shell setprop persist.log.tag.LogicalDisplayMapper DEBUG && adb reboot' private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); - public static final int LOGICAL_DISPLAY_EVENT_ADDED = 1; - public static final int LOGICAL_DISPLAY_EVENT_CHANGED = 2; - public static final int LOGICAL_DISPLAY_EVENT_REMOVED = 3; - public static final int LOGICAL_DISPLAY_EVENT_SWAPPED = 4; - public static final int LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED = 5; - public static final int LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION = 6; - public static final int LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED = 7; - public static final int LOGICAL_DISPLAY_EVENT_CONNECTED = 8; - public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 9; + public static final int LOGICAL_DISPLAY_EVENT_BASE = 0; + public static final int LOGICAL_DISPLAY_EVENT_ADDED = 1 << 0; + public static final int LOGICAL_DISPLAY_EVENT_CHANGED = 1 << 1; + public static final int LOGICAL_DISPLAY_EVENT_REMOVED = 1 << 2; + public static final int LOGICAL_DISPLAY_EVENT_SWAPPED = 1 << 3; + public static final int LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED = 1 << 4; + public static final int LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION = 1 << 5; + public static final int LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED = 1 << 6; + public static final int LOGICAL_DISPLAY_EVENT_CONNECTED = 1 << 7; + public static final int LOGICAL_DISPLAY_EVENT_DISCONNECTED = 1 << 8; + public static final int LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED = 1 << 9; + public static final int LOGICAL_DISPLAY_EVENT_STATE_CHANGED = 1 << 10; public static final int DISPLAY_GROUP_EVENT_ADDED = 1; public static final int DISPLAY_GROUP_EVENT_CHANGED = 2; @@ -804,6 +807,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final boolean wasPreviouslyUpdated = updateState != UPDATE_STATE_NEW; final boolean wasPreviouslyEnabled = mDisplaysEnabledCache.get(displayId); final boolean isCurrentlyEnabled = display.isEnabledLocked(); + int logicalDisplayEventMask = mLogicalDisplaysToUpdate + .get(displayId, LOGICAL_DISPLAY_EVENT_BASE); // The display is no longer valid and needs to be removed. if (!display.isValidLocked()) { @@ -821,20 +826,20 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (mDisplaysEnabledCache.get(displayId)) { // We still need to send LOGICAL_DISPLAY_EVENT_DISCONNECTED reloop = true; - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_REMOVED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; } else { mUpdatedLogicalDisplays.delete(displayId); - mLogicalDisplaysToUpdate.put(displayId, - LOGICAL_DISPLAY_EVENT_DISCONNECTED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DISCONNECTED; } } else { mUpdatedLogicalDisplays.delete(displayId); - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_REMOVED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_REMOVED; } } else { // This display never left this class, safe to remove without notification mLogicalDisplays.removeAt(i); } + mLogicalDisplaysToUpdate.put(displayId, logicalDisplayEventMask); continue; // The display is new. @@ -842,38 +847,40 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (mFlags.isConnectedDisplayManagementEnabled()) { // We still need to send LOGICAL_DISPLAY_EVENT_ADDED reloop = true; - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_CONNECTED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CONNECTED; } else { - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_ADDED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_ADDED; } // Underlying displays device has changed to a different one. } else if (!TextUtils.equals(mTempDisplayInfo.uniqueId, newDisplayInfo.uniqueId)) { - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_SWAPPED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_SWAPPED; // Something about the display device has changed. } else if (mFlags.isConnectedDisplayManagementEnabled() && wasPreviouslyEnabled != isCurrentlyEnabled) { int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED : LOGICAL_DISPLAY_EVENT_REMOVED; - mLogicalDisplaysToUpdate.put(displayId, event); + logicalDisplayEventMask |= event; } else if (wasDirty || !mTempDisplayInfo.equals(newDisplayInfo)) { // If only the hdr/sdr ratio changed, then send just the event for that case if ((diff == DisplayDeviceInfo.DIFF_HDR_SDR_RATIO)) { - mLogicalDisplaysToUpdate.put(displayId, - LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED; } else { - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_CHANGED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CHANGED; } - // The display is involved in a display layout transition + if (mFlags.isDisplayListenerPerformanceImprovementsEnabled()) { + logicalDisplayEventMask + |= updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo); + } + + // The display is involved in a display layout transition } else if (updateState == UPDATE_STATE_TRANSITION) { - mLogicalDisplaysToUpdate.put(displayId, - LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION; // Display frame rate overrides changed. } else if (!display.getPendingFrameRateOverrideUids().isEmpty()) { - mLogicalDisplaysToUpdate.put( - displayId, LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED; // Non-override display values changed. } else { @@ -882,10 +889,10 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // things like display cutouts. display.getNonOverrideDisplayInfoLocked(mTempDisplayInfo); if (!mTempNonOverrideDisplayInfo.equals(mTempDisplayInfo)) { - mLogicalDisplaysToUpdate.put(displayId, LOGICAL_DISPLAY_EVENT_CHANGED); + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CHANGED; } } - + mLogicalDisplaysToUpdate.put(displayId, logicalDisplayEventMask); mUpdatedLogicalDisplays.put(displayId, UPDATE_STATE_UPDATED); } @@ -922,6 +929,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); } sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_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()) { @@ -944,13 +953,25 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } } + @VisibleForTesting + int updateAndGetMaskForDisplayPropertyChanges(DisplayInfo newDisplayInfo) { + int mask = LOGICAL_DISPLAY_EVENT_BASE; + if (mTempDisplayInfo.getRefreshRate() != newDisplayInfo.getRefreshRate()) { + mask |= LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; + } + + if (mTempDisplayInfo.state != newDisplayInfo.state) { + mask |= LOGICAL_DISPLAY_EVENT_STATE_CHANGED; + } + return mask; + } /** * Send the specified message for all relevant displays in the specified display-to-message map. */ - private void sendUpdatesForDisplaysLocked(int msg) { + private void sendUpdatesForDisplaysLocked(int logicalDisplayEvent) { for (int i = mLogicalDisplaysToUpdate.size() - 1; i >= 0; --i) { - final int currMsg = mLogicalDisplaysToUpdate.valueAt(i); - if (currMsg != msg) { + final int logicalDisplayEventMask = mLogicalDisplaysToUpdate.valueAt(i); + if ((logicalDisplayEventMask & logicalDisplayEvent) == 0) { continue; } @@ -959,25 +980,25 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (DEBUG) { final DisplayDevice device = display.getPrimaryDisplayDeviceLocked(); final String uniqueId = device == null ? "null" : device.getUniqueId(); - Slog.d(TAG, "Sending " + displayEventToString(msg) + " for display=" + id - + " with device=" + uniqueId); + Slog.d(TAG, "Sending " + displayEventToString(logicalDisplayEvent) + " for " + + "display=" + id + " with device=" + uniqueId); } if (mFlags.isConnectedDisplayManagementEnabled()) { - if (msg == LOGICAL_DISPLAY_EVENT_ADDED) { + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_ADDED) { mDisplaysEnabledCache.put(id, true); - } else if (msg == LOGICAL_DISPLAY_EVENT_REMOVED) { + } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { mDisplaysEnabledCache.delete(id); } } - mListener.onLogicalDisplayEventLocked(display, msg); + mListener.onLogicalDisplayEventLocked(display, logicalDisplayEvent); if (mFlags.isConnectedDisplayManagementEnabled()) { - if (msg == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { + if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_DISCONNECTED) { mLogicalDisplays.delete(id); } - } else if (msg == LOGICAL_DISPLAY_EVENT_REMOVED) { + } else if (logicalDisplayEvent == LOGICAL_DISPLAY_EVENT_REMOVED) { // We wait until we sent the EVENT_REMOVED event before actually removing the // display. mLogicalDisplays.delete(id); @@ -1348,6 +1369,10 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { return "connected"; case LOGICAL_DISPLAY_EVENT_DISCONNECTED: return "disconnected"; + case LOGICAL_DISPLAY_EVENT_STATE_CHANGED: + return "state_changed"; + case LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED: + return "refresh_rate_changed"; } return null; } 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 71f17d1f411e..1a7d74ae1713 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -246,6 +246,10 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_PLUGIN_MANAGER, Flags::enablePluginManager ); + private final FlagState mDisplayListenerPerformanceImprovementsFlagState = new FlagState( + Flags.FLAG_DISPLAY_LISTENER_PERFORMANCE_IMPROVEMENTS, + Flags::displayListenerPerformanceImprovements + ); /** * @return {@code true} if 'port' is allowed in display layout configuration file. @@ -527,6 +531,13 @@ public class DisplayManagerFlags { } /** + * @return {@code true} if the flag for display listener performance improvements is enabled + */ + public boolean isDisplayListenerPerformanceImprovementsEnabled() { + return mDisplayListenerPerformanceImprovementsFlagState.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ @@ -578,6 +589,7 @@ public class DisplayManagerFlags { pw.println(" " + mHasArrSupport); pw.println(" " + mAutoBrightnessModeBedtimeWearFlagState); pw.println(" " + mEnablePluginManagerFlagState); + pw.println(" " + mDisplayListenerPerformanceImprovementsFlagState); } 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 b509544c72bc..586d59492f57 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 @@ -440,6 +440,14 @@ flag { } flag { + name: "display_listener_performance_improvements" + namespace: "display_manager" + description: "Feature flag for an API to let the apps subscribe to a specific property change of the Display." + bug: "372700957" + is_fixed_read_only: true +} + +flag { name: "enable_get_supported_refresh_rates" namespace: "core_graphics" description: "Flag to use the surfaceflinger rates for getSupportedRefreshRates" diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index f611c57dab03..88a7d08415dc 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -31,6 +31,7 @@ import android.database.ContentObserver; import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManagerInternal; import android.hardware.contexthub.ErrorCode; +import android.hardware.contexthub.HubEndpointInfo; import android.hardware.contexthub.MessageDeliveryStatus; import android.hardware.location.ContextHubInfo; import android.hardware.location.ContextHubMessage; @@ -739,6 +740,14 @@ public class ContextHubService extends IContextHubService.Stub { return mHubInfoRegistry.getHubs(); } + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + @Override + public List<HubEndpointInfo> findEndpoints(long endpointId) { + super.findEndpoints_enforcePermission(); + // TODO(b/375487784): connect this with mHubInfoRegistry + return Collections.emptyList(); + } + /** * Creates an internal load transaction callback to be used for old API clients * diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java index 5914dbe44b0b..7fd962003ce9 100644 --- a/services/core/java/com/android/server/notification/GroupHelper.java +++ b/services/core/java/com/android/server/notification/GroupHelper.java @@ -92,10 +92,16 @@ public class GroupHelper { static final int REGROUP_REASON_CHANNEL_UPDATE = 0; // Regrouping needed because of notification bundling static final int REGROUP_REASON_BUNDLE = 1; + // Regrouping needed because of notification unbundling + static final int REGROUP_REASON_UNBUNDLE = 2; + // Regrouping needed because of notification unbundling + the original group summary exists + static final int REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP = 3; @IntDef(prefix = { "REGROUP_REASON_" }, value = { REGROUP_REASON_CHANNEL_UPDATE, REGROUP_REASON_BUNDLE, + REGROUP_REASON_UNBUNDLE, + REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP, }) @Retention(RetentionPolicy.SOURCE) @interface RegroupingReason {} @@ -103,7 +109,6 @@ public class GroupHelper { private final Callback mCallback; private final int mAutoGroupAtCount; private final int mAutogroupSparseGroupsAtCount; - private final int mAutoGroupRegroupingAtCount; private final Context mContext; private final PackageManager mPackageManager; private boolean mIsTestHarnessExempted; @@ -190,11 +195,6 @@ public class GroupHelper { mContext = context; mPackageManager = packageManager; mAutogroupSparseGroupsAtCount = autoGroupSparseGroupsAtCount; - if (notificationRegroupOnClassification()) { - mAutoGroupRegroupingAtCount = 1; - } else { - mAutoGroupRegroupingAtCount = mAutoGroupAtCount; - } NOTIFICATION_SHADE_SECTIONS = getNotificationShadeSections(); } @@ -797,7 +797,8 @@ public class GroupHelper { Slog.v(TAG, "isGroupChildInDifferentBundleThanSummary: " + record); } moveNotificationsToNewSection(record.getUserId(), pkgName, - List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey))); + List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)), + REGROUP_REASON_BUNDLE); return; } } @@ -945,6 +946,27 @@ public class GroupHelper { } } + /** + * Called when a notification that was classified (bundled) is restored to its original channel. + * The notification will be restored to its original group, if any/if summary still exists. + * Otherwise it will be moved to the appropriate section as an ungrouped notification. + * + * @param record the notification which had its channel updated + * @param originalSummaryExists the original group summary exists + */ + @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING) + public void onNotificationUnbundled(final NotificationRecord record, + final boolean originalSummaryExists) { + synchronized (mAggregatedNotifications) { + ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>(); + notificationsToCheck.put(record.getKey(), record); + regroupNotifications(record.getUserId(), record.getSbn().getPackageName(), + notificationsToCheck, + originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP + : REGROUP_REASON_UNBUNDLE); + } + } + @GuardedBy("mAggregatedNotifications") private void regroupNotifications(int userId, String pkgName, ArrayMap<String, NotificationRecord> notificationsToCheck, @@ -973,7 +995,7 @@ public class GroupHelper { // Batch move to new section if (!notificationsToMove.isEmpty()) { - moveNotificationsToNewSection(userId, pkgName, notificationsToMove); + moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupingReason); } } @@ -1093,7 +1115,7 @@ public class GroupHelper { @GuardedBy("mAggregatedNotifications") private void moveNotificationsToNewSection(final int userId, final String pkgName, - final List<NotificationMoveOp> notificationsToMove) { + final List<NotificationMoveOp> notificationsToMove, int regroupingReason) { record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record, boolean hasSummary) { } // Bundled operations to apply to groups affected by the channel update @@ -1111,7 +1133,8 @@ public class GroupHelper { if (DEBUG) { Log.i(TAG, "moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey - + " from: " + oldFullAggregateGroupKey); + + " from: " + oldFullAggregateGroupKey + " regroupingReason: " + + regroupingReason); } // Update/remove aggregate summary for old group @@ -1140,28 +1163,35 @@ public class GroupHelper { // Add moved notifications to the ungrouped list for new group and do grouping // after all notifications have been handled if (newFullAggregateGroupKey != null) { - final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs = + if (notificationRegroupOnClassification() + && regroupingReason == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) { + // Just reset override group key, original summary exists + // => will be grouped back to its original group + record.setOverrideGroupKey(null); + } else { + final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs = mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey, new ArrayMap<>()); - boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty(); - ArrayMap<String, NotificationAttributes> ungrouped = + boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty(); + ArrayMap<String, NotificationAttributes> ungrouped = mUngroupedAbuseNotifications.getOrDefault(newFullAggregateGroupKey, new ArrayMap<>()); - ungrouped.put(record.getKey(), new NotificationAttributes( + ungrouped.put(record.getKey(), new NotificationAttributes( record.getFlags(), record.getNotification().getSmallIcon(), record.getNotification().color, record.getNotification().visibility, record.getNotification().getGroupAlertBehavior(), record.getChannel().getId())); - mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped); + mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped); - record.setOverrideGroupKey(null); + record.setOverrideGroupKey(null); - // Only add once, for triggering notification - if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) { - groupsToUpdate.put(newFullAggregateGroupKey, - new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary)); + // Only add once, for triggering notification + if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) { + groupsToUpdate.put(newFullAggregateGroupKey, + new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary)); + } } } } @@ -1176,7 +1206,7 @@ public class GroupHelper { NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record; boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary; //Group needs to be created/updated - if (ungrouped.size() >= mAutoGroupRegroupingAtCount + if (ungrouped.size() >= mAutoGroupAtCount || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) { NotificationSectioner sectioner = getSection(triggeringNotification); if (sectioner == null) { @@ -1436,7 +1466,8 @@ public class GroupHelper { } } - private ArrayMap<String, NotificationRecord> getSparseGroups( + @VisibleForTesting + protected ArrayMap<String, NotificationRecord> getSparseGroups( final FullyQualifiedGroupKey fullAggregateGroupKey, final List<NotificationRecord> notificationList, final Map<String, NotificationRecord> summaryByGroupKey, @@ -1448,8 +1479,8 @@ public class GroupHelper { && summary.getUserId() == fullAggregateGroupKey.userId && summary.getSbn().isAppGroup() && !summary.getGroupKey().equals(fullAggregateGroupKey.toString())) { - int numChildren = getNumChildrenForGroup(summary.getSbn().getGroup(), - notificationList); + int numChildren = getNumChildrenForGroupWithSection(summary.getSbn().getGroup(), + notificationList, sectioner); if (numChildren > 0 && numChildren < MIN_CHILD_COUNT_TO_AVOID_FORCE_GROUPING) { sparseGroups.put(summary.getGroupKey(), summary); } @@ -1459,6 +1490,43 @@ public class GroupHelper { return sparseGroups; } + /** + * Get the number of children of a group if all match a certain section. + * Used for force grouping sparse groups, where the summary may match a section but the + * child notifications do not: ie. conversations + * + * @param groupKey the group key (name) + * @param notificationList all notifications list + * @param sectioner the section to match + * @return number of children in that group or -1 if section does not match + */ + private int getNumChildrenForGroupWithSection(final String groupKey, + final List<NotificationRecord> notificationList, + final NotificationSectioner sectioner) { + int numChildren = 0; + for (NotificationRecord r : notificationList) { + if (!r.getNotification().isGroupSummary() && groupKey.equals(r.getSbn().getGroup())) { + NotificationSectioner childSection = getSection(r); + if (childSection == null || childSection != sectioner) { + if (DEBUG) { + Slog.i(TAG, + "getNumChildrenForGroupWithSection skip because invalid section: " + + groupKey + " r: " + r); + } + return -1; + } else { + numChildren++; + } + } + } + + if (DEBUG) { + Slog.i(TAG, + "getNumChildrenForGroupWithSection " + groupKey + " numChild: " + numChildren); + } + return numChildren; + } + @GuardedBy("mAggregatedNotifications") private void cacheCanceledSummary(NotificationRecord record) { final FullyQualifiedGroupKey groupKey = new FullyQualifiedGroupKey(record.getUserId(), diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 0d8880af2256..5182dfe60fcc 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -7180,13 +7180,16 @@ public class NotificationManagerService extends SystemService { Slog.i(TAG, "Removing app summary (all children bundled): " + groupSummary); } - canceledSummary = groupSummary; - mSummaryByGroupKey.remove(oldGroupKey); - cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), + if (convertSummaryToNotificationLocked(groupSummary.getKey())) { + groupSummary.isCanceled = true; + canceledSummary = groupSummary; + mSummaryByGroupKey.remove(oldGroupKey); + cancelNotification(Binder.getCallingUid(), Binder.getCallingPid(), groupSummary.getSbn().getPackageName(), groupSummary.getSbn().getTag(), groupSummary.getSbn().getId(), 0, 0, false, groupSummary.getUserId(), NotificationListenerService.REASON_GROUP_OPTIMIZATION, null); + } } } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 06e29c2c1408..b2b8aaf7dd2a 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -4984,7 +4984,10 @@ public class UserManagerService extends IUserManager.Stub { res.getValue(com.android.internal.R.string.owner_name, mOwnerNameTypedValue, true); final CharSequence ownerName = mOwnerNameTypedValue.coerceToString(); mOwnerName.set(ownerName != null ? ownerName.toString() : null); + // Invalidate when owners name changes due to config change. + UserManager.invalidateCacheOnUserDataChanged(); } + } private void scheduleWriteUserList() { @@ -4997,6 +5000,8 @@ public class UserManagerService extends IUserManager.Stub { Message msg = mHandler.obtainMessage(WRITE_USER_LIST_MSG); mHandler.sendMessageDelayed(msg, WRITE_USER_DELAY); } + // Invalidate cache when {@link UserData} changed, but write was scheduled for later. + UserManager.invalidateCacheOnUserDataChanged(); } private void scheduleWriteUser(@UserIdInt int userId) { @@ -5009,6 +5014,8 @@ public class UserManagerService extends IUserManager.Stub { Message msg = mHandler.obtainMessage(WRITE_USER_MSG, userId); mHandler.sendMessageDelayed(msg, WRITE_USER_DELAY); } + // Invalidate cache when {@link Data} changed, but write was scheduled for later. + UserManager.invalidateCacheOnUserDataChanged(); } private ResilientAtomicFile getUserFile(int userId) { @@ -5032,6 +5039,9 @@ public class UserManagerService extends IUserManager.Stub { if (DBG) { debug("writeUserLP " + userData); } + // invalidate caches related to any {@link UserData} change. + UserManager.invalidateCacheOnUserDataChanged(); + try (ResilientAtomicFile userFile = getUserFile(userData.info.id)) { FileOutputStream fos = null; try { @@ -5196,6 +5206,8 @@ public class UserManagerService extends IUserManager.Stub { if (DBG) { debug("writeUserList"); } + // invalidate caches related to any {@link UserData} change. + UserManager.invalidateCacheOnUserDataChanged(); try (ResilientAtomicFile file = getUserListFile()) { FileOutputStream fos = null; @@ -7958,7 +7970,7 @@ public class UserManagerService extends IUserManager.Stub { Settings.Secure.getIntForUser(mContext.getContentResolver(), HIDE_PRIVATESPACE_ENTRY_POINT, parentId) == 1); } catch (Settings.SettingNotFoundException e) { - throw new RuntimeException(e); + config.putBoolean(PRIVATE_SPACE_ENTRYPOINT_HIDDEN, false); } } return new LauncherUserInfo.Builder(userDetails.getName(), diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 0acfe92f578d..37883f594227 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -2332,6 +2332,8 @@ public final class PowerManagerService extends SystemService Trace.traceBegin(Trace.TRACE_TAG_POWER, traceMethodName); try { // Phase 2: Handle wakefulness change and bookkeeping. + // Under lock, invalidate before set ensures caches won't return stale values. + mInjector.invalidateIsInteractiveCaches(); mWakefulnessRaw = newWakefulness; mWakefulnessChanging = true; mDirty |= DIRTY_WAKEFULNESS; @@ -2429,7 +2431,6 @@ public final class PowerManagerService extends SystemService void onPowerGroupEventLocked(int event, PowerGroup powerGroup) { mWakefulnessChanging = true; mDirty |= DIRTY_WAKEFULNESS; - mInjector.invalidateIsInteractiveCaches(); final int groupId = powerGroup.getGroupId(); if (event == DisplayGroupPowerChangeListener.DISPLAY_GROUP_REMOVED) { mPowerGroups.delete(groupId); 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 2c0ce252df18..17459df2bc1a 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -33,11 +33,15 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.hardware.power.ChannelConfig; +import android.hardware.power.CpuHeadroomParams; +import android.hardware.power.GpuHeadroomParams; import android.hardware.power.IPower; import android.hardware.power.SessionConfig; import android.hardware.power.SessionTag; import android.hardware.power.WorkDuration; import android.os.Binder; +import android.os.CpuHeadroomParamsInternal; +import android.os.GpuHeadroomParamsInternal; import android.os.Handler; import android.os.IBinder; import android.os.IHintManager; @@ -90,6 +94,10 @@ public final class HintManagerService extends SystemService { private static final int EVENT_CLEAN_UP_UID = 3; @VisibleForTesting static final int CLEAN_UP_UID_DELAY_MILLIS = 1000; + private static final int DEFAULT_GPU_HEADROOM_INTERVAL_MILLIS = 1000; + private static final int DEFAULT_CPU_HEADROOM_INTERVAL_MILLIS = 1000; + private static final int HEADROOM_INTERVAL_UNSUPPORTED = -1; + @VisibleForTesting static final int DEFAULT_HEADROOM_PID = -1; @VisibleForTesting final long mHintSessionPreferredRate; @@ -160,10 +168,76 @@ public final class HintManagerService extends SystemService { private static final String PROPERTY_SF_ENABLE_CPU_HINT = "debug.sf.enable_adpf_cpu_hint"; private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager"; + private static final String PROPERTY_USE_HAL_HEADROOMS = "persist.hms.use_hal_headrooms"; private Boolean mFMQUsesIntegratedEventFlag = false; - @VisibleForTesting final IHintManager.Stub mService = new BinderService(); + private final Object mCpuHeadroomLock = new Object(); + + private static class CpuHeadroomCacheItem { + long mExpiredTimeMillis; + CpuHeadroomParamsInternal mParams; + float[] mHeadroom; + long mPid; + + CpuHeadroomCacheItem(long expiredTimeMillis, CpuHeadroomParamsInternal params, + float[] headroom, long pid) { + mExpiredTimeMillis = expiredTimeMillis; + mParams = params; + mPid = pid; + mHeadroom = headroom; + } + + private boolean match(CpuHeadroomParamsInternal params, long pid) { + if (mParams == null && params == null) return true; + if (mParams != null) { + return mParams.equals(params) && pid == mPid; + } + return false; + } + + private boolean isExpired() { + return System.currentTimeMillis() > mExpiredTimeMillis; + } + } + + @GuardedBy("mCpuHeadroomLock") + private final List<CpuHeadroomCacheItem> mCpuHeadroomCache; + private final long mCpuHeadroomIntervalMillis; + + private final Object mGpuHeadroomLock = new Object(); + + private static class GpuHeadroomCacheItem { + long mExpiredTimeMillis; + GpuHeadroomParamsInternal mParams; + float mHeadroom; + + GpuHeadroomCacheItem(long expiredTimeMillis, GpuHeadroomParamsInternal params, + float headroom) { + mExpiredTimeMillis = expiredTimeMillis; + mParams = params; + mHeadroom = headroom; + } + + private boolean match(GpuHeadroomParamsInternal params) { + if (mParams == null && params == null) return true; + if (mParams != null) { + return mParams.equals(params); + } + return false; + } + + private boolean isExpired() { + return System.currentTimeMillis() > mExpiredTimeMillis; + } + } + + @GuardedBy("mGpuHeadroomLock") + private final List<GpuHeadroomCacheItem> mGpuHeadroomCache; + private final long mGpuHeadroomIntervalMillis; + + @VisibleForTesting + final IHintManager.Stub mService = new BinderService(); public HintManagerService(Context context) { this(context, new Injector()); @@ -197,13 +271,72 @@ public final class HintManagerService extends SystemService { mPowerHal = injector.createIPower(); mPowerHalVersion = 0; mUsesFmq = false; + long cpuHeadroomIntervalMillis = HEADROOM_INTERVAL_UNSUPPORTED; + long gpuHeadroomIntervalMillis = HEADROOM_INTERVAL_UNSUPPORTED; if (mPowerHal != null) { try { mPowerHalVersion = mPowerHal.getInterfaceVersion(); + if (mPowerHal.getInterfaceVersion() >= 6) { + if (SystemProperties.getBoolean(PROPERTY_USE_HAL_HEADROOMS, true)) { + cpuHeadroomIntervalMillis = checkCpuHeadroomSupport(); + gpuHeadroomIntervalMillis = checkGpuHeadroomSupport(); + } + } } catch (RemoteException e) { throw new IllegalStateException("Could not contact PowerHAL!", e); } } + mCpuHeadroomIntervalMillis = cpuHeadroomIntervalMillis; + mGpuHeadroomIntervalMillis = gpuHeadroomIntervalMillis; + if (mCpuHeadroomIntervalMillis > 0) { + mCpuHeadroomCache = new ArrayList<>(4); + } else { + mCpuHeadroomCache = null; + } + if (mGpuHeadroomIntervalMillis > 0) { + mGpuHeadroomCache = new ArrayList<>(2); + } else { + mGpuHeadroomCache = null; + } + } + + private long checkCpuHeadroomSupport() { + try { + synchronized (mCpuHeadroomLock) { + final CpuHeadroomParams defaultParams = new CpuHeadroomParams(); + defaultParams.pid = Process.myPid(); + float[] ret = mPowerHal.getCpuHeadroom(defaultParams); + if (ret != null && ret.length > 0) { + return Math.max( + DEFAULT_CPU_HEADROOM_INTERVAL_MILLIS, + mPowerHal.getCpuHeadroomMinIntervalMillis()); + } + } + + } catch (UnsupportedOperationException e) { + Slog.w(TAG, "getCpuHeadroom HAL API is not supported", e); + } catch (RemoteException e) { + Slog.e(TAG, "getCpuHeadroom HAL API fails, disabling the API", e); + } + return HEADROOM_INTERVAL_UNSUPPORTED; + } + + private long checkGpuHeadroomSupport() { + try { + synchronized (mGpuHeadroomLock) { + float ret = mPowerHal.getGpuHeadroom(new GpuHeadroomParams()); + if (!Float.isNaN(ret)) { + return Math.max( + DEFAULT_GPU_HEADROOM_INTERVAL_MILLIS, + mPowerHal.getGpuHeadroomMinIntervalMillis()); + } + } + } catch (UnsupportedOperationException e) { + Slog.w(TAG, "getGpuHeadroom HAL API is not supported", e); + } catch (RemoteException e) { + Slog.e(TAG, "getGpuHeadroom HAL API fails, disabling the API", e); + } + return HEADROOM_INTERVAL_UNSUPPORTED; } private ServiceThread createCleanUpThread() { @@ -738,7 +871,7 @@ public final class HintManagerService extends SystemService { mLinked = false; } if (mConfig != null) { - try { + try { mPowerHal.closeSessionChannel(mTgid, mUid); } catch (RemoteException e) { throw new IllegalStateException("Failed to close session channel!", e); @@ -982,13 +1115,13 @@ public final class HintManagerService extends SystemService { } // returns the first invalid tid or null if not found - private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) { + private Integer checkTidValid(int uid, int tgid, int[] tids, IntArray nonIsolated) { // Make sure all tids belongs to the same UID (including isolated UID), // tids can belong to different application processes. List<Integer> isolatedPids = null; for (int i = 0; i < tids.length; i++) { int tid = tids[i]; - final String[] procStatusKeys = new String[] { + final String[] procStatusKeys = new String[]{ "Uid:", "Tgid:" }; @@ -1058,7 +1191,7 @@ public final class HintManagerService extends SystemService { Slogf.w(TAG, errMsg); throw new SecurityException(errMsg); } - if (resetOnForkEnabled()){ + if (resetOnForkEnabled()) { try { for (int tid : tids) { int policy = Process.getThreadScheduler(tid); @@ -1214,6 +1347,124 @@ public final class HintManagerService extends SystemService { } @Override + public float[] getCpuHeadroom(@Nullable CpuHeadroomParamsInternal params) { + if (mCpuHeadroomIntervalMillis <= 0) { + throw new UnsupportedOperationException(); + } + CpuHeadroomParams halParams = new CpuHeadroomParams(); + halParams.pid = Binder.getCallingPid(); + if (params != null) { + halParams.calculationType = params.calculationType; + halParams.selectionType = params.selectionType; + if (params.usesDeviceHeadroom) { + halParams.pid = DEFAULT_HEADROOM_PID; + } + } + synchronized (mCpuHeadroomLock) { + while (!mCpuHeadroomCache.isEmpty()) { + if (mCpuHeadroomCache.getFirst().isExpired()) { + mCpuHeadroomCache.removeFirst(); + } else { + break; + } + } + for (int i = 0; i < mCpuHeadroomCache.size(); ++i) { + final CpuHeadroomCacheItem item = mCpuHeadroomCache.get(i); + if (item.match(params, halParams.pid)) { + item.mExpiredTimeMillis = + System.currentTimeMillis() + mCpuHeadroomIntervalMillis; + mCpuHeadroomCache.remove(i); + mCpuHeadroomCache.add(item); + return item.mHeadroom; + } + } + } + // return from HAL directly + try { + float[] headroom = mPowerHal.getCpuHeadroom(halParams); + if (headroom == null || headroom.length == 0) { + Slog.wtf(TAG, + "CPU headroom from Power HAL is invalid: " + Arrays.toString(headroom)); + return new float[]{Float.NaN}; + } + synchronized (mCpuHeadroomLock) { + mCpuHeadroomCache.add(new CpuHeadroomCacheItem( + System.currentTimeMillis() + mCpuHeadroomIntervalMillis, + params, headroom, halParams.pid + )); + } + return headroom; + + } catch (RemoteException e) { + return new float[]{Float.NaN}; + } + } + + @Override + public float getGpuHeadroom(@Nullable GpuHeadroomParamsInternal params) { + if (mGpuHeadroomIntervalMillis <= 0) { + throw new UnsupportedOperationException(); + } + GpuHeadroomParams halParams = new GpuHeadroomParams(); + if (params != null) { + halParams.calculationType = params.calculationType; + } + synchronized (mGpuHeadroomLock) { + while (!mGpuHeadroomCache.isEmpty()) { + if (mGpuHeadroomCache.getFirst().isExpired()) { + mGpuHeadroomCache.removeFirst(); + } else { + break; + } + } + for (int i = 0; i < mGpuHeadroomCache.size(); ++i) { + final GpuHeadroomCacheItem item = mGpuHeadroomCache.get(i); + if (item.match(params)) { + item.mExpiredTimeMillis = + System.currentTimeMillis() + mGpuHeadroomIntervalMillis; + mGpuHeadroomCache.remove(i); + mGpuHeadroomCache.add(item); + return item.mHeadroom; + } + } + } + // return from HAL directly + try { + float headroom = mPowerHal.getGpuHeadroom(halParams); + if (Float.isNaN(headroom)) { + Slog.wtf(TAG, + "GPU headroom from Power HAL is NaN"); + return Float.NaN; + } + synchronized (mGpuHeadroomLock) { + mGpuHeadroomCache.add(new GpuHeadroomCacheItem( + System.currentTimeMillis() + mGpuHeadroomIntervalMillis, + params, headroom + )); + } + return headroom; + } catch (RemoteException e) { + return Float.NaN; + } + } + + @Override + public long getCpuHeadroomMinIntervalMillis() throws RemoteException { + if (mCpuHeadroomIntervalMillis <= 0) { + throw new UnsupportedOperationException(); + } + return mCpuHeadroomIntervalMillis; + } + + @Override + public long getGpuHeadroomMinIntervalMillis() throws RemoteException { + if (mGpuHeadroomIntervalMillis <= 0) { + throw new UnsupportedOperationException(); + } + return mGpuHeadroomIntervalMillis; + } + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) { return; @@ -1235,6 +1486,25 @@ public final class HintManagerService extends SystemService { } } } + pw.println("CPU Headroom Interval: " + mCpuHeadroomIntervalMillis); + pw.println("GPU Headroom Interval: " + mGpuHeadroomIntervalMillis); + try { + CpuHeadroomParamsInternal params = new CpuHeadroomParamsInternal(); + params.selectionType = CpuHeadroomParams.SelectionType.ALL; + params.usesDeviceHeadroom = true; + pw.println("CPU headroom: " + Arrays.toString(getCpuHeadroom(params))); + params = new CpuHeadroomParamsInternal(); + params.selectionType = CpuHeadroomParams.SelectionType.PER_CORE; + params.usesDeviceHeadroom = true; + pw.println("CPU headroom per core: " + Arrays.toString(getCpuHeadroom(params))); + } catch (Exception e) { + pw.println("CPU headroom: N/A"); + } + try { + pw.println("GPU headroom: " + getGpuHeadroom(null)); + } catch (Exception e) { + pw.println("GPU headroom: N/A"); + } } private void logPerformanceHintSessionAtom(int uid, long sessionId, @@ -1467,7 +1737,7 @@ public final class HintManagerService extends SystemService { Slogf.w(TAG, errMsg); throw new SecurityException(errMsg); } - if (resetOnForkEnabled()){ + if (resetOnForkEnabled()) { try { for (int tid : tids) { int policy = Process.getThreadScheduler(tid); diff --git a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java index 1260eeec098f..e780be490181 100644 --- a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java +++ b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java @@ -46,6 +46,7 @@ import com.android.server.SystemService; import com.android.server.pm.UserManagerInternal; import com.android.server.security.advancedprotection.features.AdvancedProtectionHook; import com.android.server.security.advancedprotection.features.AdvancedProtectionProvider; +import com.android.server.security.advancedprotection.features.DisallowInstallUnknownSourcesAdvancedProtectionHook; import java.io.FileDescriptor; import java.util.ArrayList; @@ -76,10 +77,9 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub } private void initFeatures(boolean enabled) { - // Empty until features are added. - // Examples: - // mHooks.add(new SideloadingAdvancedProtectionHook(mContext, enabled)); - // mProviders.add(new WifiAdvancedProtectionProvider()); + if (android.security.Flags.aapmFeatureDisableInstallUnknownSources()) { + mHooks.add(new DisallowInstallUnknownSourcesAdvancedProtectionHook(mContext, enabled)); + } } // Only for tests diff --git a/services/core/java/com/android/server/security/advancedprotection/features/DisallowInstallUnknownSourcesAdvancedProtectionHook.java b/services/core/java/com/android/server/security/advancedprotection/features/DisallowInstallUnknownSourcesAdvancedProtectionHook.java new file mode 100644 index 000000000000..21752e524619 --- /dev/null +++ b/services/core/java/com/android/server/security/advancedprotection/features/DisallowInstallUnknownSourcesAdvancedProtectionHook.java @@ -0,0 +1,73 @@ +/* + * 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.security.advancedprotection.features; + +import static android.security.advancedprotection.AdvancedProtectionManager.ADVANCED_PROTECTION_SYSTEM_ENTITY; +import static android.security.advancedprotection.AdvancedProtectionManager.FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES; + +import android.annotation.NonNull; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserManager; +import android.security.advancedprotection.AdvancedProtectionFeature; +import android.util.Slog; + +/** @hide */ +public final class DisallowInstallUnknownSourcesAdvancedProtectionHook + extends AdvancedProtectionHook { + private static final String TAG = "AdvancedProtectionDisallowInstallUnknown"; + + private final AdvancedProtectionFeature mFeature = new AdvancedProtectionFeature( + FEATURE_ID_DISALLOW_INSTALL_UNKNOWN_SOURCES); + private final DevicePolicyManager mDevicePolicyManager; + + public DisallowInstallUnknownSourcesAdvancedProtectionHook(@NonNull Context context, + boolean enabled) { + super(context, enabled); + mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); + onAdvancedProtectionChanged(enabled); + } + + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return mFeature; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public void onAdvancedProtectionChanged(boolean enabled) { + if (enabled) { + Slog.d(TAG, "Setting DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY restriction"); + mDevicePolicyManager.addUserRestrictionGlobally(ADVANCED_PROTECTION_SYSTEM_ENTITY, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY); + return; + } + Slog.d(TAG, "Clearing DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY restriction"); + mDevicePolicyManager.clearUserRestrictionGlobally(ADVANCED_PROTECTION_SYSTEM_ENTITY, + UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY); + + // TODO(b/369361373): + // 1. After clearing the restriction, set AppOpsManager.OP_REQUEST_INSTALL_PACKAGES to + // disabled. + // 2. Update dialog strings. + } +} diff --git a/services/core/java/com/android/server/security/forensic/BackupTransportConnection.java b/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java index caca011b6549..b85199ed9218 100644 --- a/services/core/java/com/android/server/security/forensic/BackupTransportConnection.java +++ b/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java @@ -16,15 +16,19 @@ package com.android.server.security.forensic; +import static android.Manifest.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.security.forensic.ForensicEvent; -import android.security.forensic.IBackupTransport; +import android.security.forensic.IForensicEventTransport; import android.text.TextUtils; import android.util.Slog; @@ -36,20 +40,20 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -public class BackupTransportConnection implements ServiceConnection { - private static final String TAG = "BackupTransportConnection"; +public class ForensicEventTransportConnection implements ServiceConnection { + private static final String TAG = "ForensicEventTransportConnection"; private static final long FUTURE_TIMEOUT_MILLIS = 60 * 1000; // 1 mins private final Context mContext; - private String mForensicBackupTransportConfig; - volatile IBackupTransport mService; + private String mForensicEventTransportConfig; + volatile IForensicEventTransport mService; - public BackupTransportConnection(Context context) { + public ForensicEventTransportConnection(Context context) { mContext = context; mService = null; } /** - * Initialize the BackupTransport binder service. + * Initialize the ForensicEventTransport binder service. * @return Whether the initialization succeed. */ public boolean initialize() { @@ -74,7 +78,7 @@ public class BackupTransportConnection implements ServiceConnection { } /** - * Add data to the BackupTransport binder service. + * Add data to the ForensicEventTransport binder service. * @param data List of ForensicEvent. * @return Whether the data is added to the binder service. */ @@ -109,21 +113,37 @@ public class BackupTransportConnection implements ServiceConnection { return future.get(FUTURE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException | CancellationException e) { - Slog.w(TAG, "Failed to get result from transport:", e); + Slog.e(TAG, "Failed to get result from transport:", e); return null; } } private boolean bindService() { - mForensicBackupTransportConfig = mContext.getString( - com.android.internal.R.string.config_forensicBackupTransport); - if (TextUtils.isEmpty(mForensicBackupTransportConfig)) { + mForensicEventTransportConfig = mContext.getString( + com.android.internal.R.string.config_forensicEventTransport); + if (TextUtils.isEmpty(mForensicEventTransportConfig)) { + Slog.e(TAG, "config_forensicEventTransport is empty"); return false; } ComponentName serviceComponent = - ComponentName.unflattenFromString(mForensicBackupTransportConfig); + ComponentName.unflattenFromString(mForensicEventTransportConfig); if (serviceComponent == null) { + Slog.e(TAG, "Can't get serviceComponent name"); + return false; + } + + try { + ServiceInfo serviceInfo = mContext.getPackageManager().getServiceInfo(serviceComponent, + 0 /* flags */); + if (!BIND_FORENSIC_EVENT_TRANSPORT_SERVICE.equals(serviceInfo.permission)) { + Slog.e(TAG, serviceComponent.flattenToShortString() + + " is not declared with the permission " + + "\"" + BIND_FORENSIC_EVENT_TRANSPORT_SERVICE + "\""); + return false; + } + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Unable to find serviceComponent"); return false; } @@ -143,7 +163,7 @@ public class BackupTransportConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { - mService = IBackupTransport.Stub.asInterface(service); + mService = IForensicEventTransport.Stub.asInterface(service); } @Override diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java index 01f630b60ff5..2be068fa2f83 100644 --- a/services/core/java/com/android/server/security/forensic/ForensicService.java +++ b/services/core/java/com/android/server/security/forensic/ForensicService.java @@ -16,11 +16,16 @@ package com.android.server.security.forensic; +import static android.Manifest.permission.MANAGE_FORENSIC_STATE; +import static android.Manifest.permission.READ_FORENSIC_STATE; + +import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.PermissionEnforcer; import android.os.RemoteException; import android.security.forensic.ForensicEvent; import android.security.forensic.IForensicService; @@ -41,16 +46,15 @@ import java.util.List; public class ForensicService extends SystemService { private static final String TAG = "ForensicService"; - private static final int MSG_MONITOR_STATE = 0; - private static final int MSG_MAKE_VISIBLE = 1; - private static final int MSG_MAKE_INVISIBLE = 2; - private static final int MSG_ENABLE = 3; - private static final int MSG_DISABLE = 4; - private static final int MSG_BACKUP = 5; + private static final int MAX_STATE_CALLBACK_NUM = 16; + private static final int MSG_ADD_STATE_CALLBACK = 0; + private static final int MSG_REMOVE_STATE_CALLBACK = 1; + private static final int MSG_ENABLE = 2; + private static final int MSG_DISABLE = 3; + private static final int MSG_TRANSPORT = 4; private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN; - private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE; - private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE; + private static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED; private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED; private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN; @@ -58,19 +62,19 @@ public class ForensicService extends SystemService { IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED; private static final int ERROR_INVALID_STATE_TRANSITION = IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION; - private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE = - IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE; + private static final int ERROR_TRANSPORT_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE; private static final int ERROR_DATA_SOURCE_UNAVAILABLE = IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; private final Context mContext; private final Handler mHandler; - private final BackupTransportConnection mBackupTransportConnection; + private final ForensicEventTransportConnection mForensicEventTransportConnection; private final DataAggregator mDataAggregator; private final BinderService mBinderService; - private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>(); - private volatile int mState = STATE_INVISIBLE; + private final ArrayList<IForensicServiceStateCallback> mStateCallbacks = new ArrayList<>(); + private volatile int mState = STATE_DISABLED; public ForensicService(@NonNull Context context) { this(new InjectorImpl(context)); @@ -81,9 +85,9 @@ public class ForensicService extends SystemService { super(injector.getContext()); mContext = injector.getContext(); mHandler = new EventHandler(injector.getLooper(), this); - mBackupTransportConnection = injector.getBackupTransportConnection(); + mForensicEventTransportConnection = injector.getForensicEventransportConnection(); mDataAggregator = injector.getDataAggregator(this); - mBinderService = new BinderService(this); + mBinderService = new BinderService(this, injector.getPermissionEnforcer()); } @VisibleForTesting @@ -94,32 +98,36 @@ public class ForensicService extends SystemService { private static final class BinderService extends IForensicService.Stub { final ForensicService mService; - BinderService(ForensicService service) { + BinderService(ForensicService service, @NonNull PermissionEnforcer permissionEnforcer) { + super(permissionEnforcer); mService = service; } @Override - public void monitorState(IForensicServiceStateCallback callback) { - mService.mHandler.obtainMessage(MSG_MONITOR_STATE, callback).sendToTarget(); - } - - @Override - public void makeVisible(IForensicServiceCommandCallback callback) { - mService.mHandler.obtainMessage(MSG_MAKE_VISIBLE, callback).sendToTarget(); + @EnforcePermission(READ_FORENSIC_STATE) + public void addStateCallback(IForensicServiceStateCallback callback) { + addStateCallback_enforcePermission(); + mService.mHandler.obtainMessage(MSG_ADD_STATE_CALLBACK, callback).sendToTarget(); } @Override - public void makeInvisible(IForensicServiceCommandCallback callback) { - mService.mHandler.obtainMessage(MSG_MAKE_INVISIBLE, callback).sendToTarget(); + @EnforcePermission(READ_FORENSIC_STATE) + public void removeStateCallback(IForensicServiceStateCallback callback) { + removeStateCallback_enforcePermission(); + mService.mHandler.obtainMessage(MSG_REMOVE_STATE_CALLBACK, callback).sendToTarget(); } @Override + @EnforcePermission(MANAGE_FORENSIC_STATE) public void enable(IForensicServiceCommandCallback callback) { + enable_enforcePermission(); mService.mHandler.obtainMessage(MSG_ENABLE, callback).sendToTarget(); } @Override + @EnforcePermission(MANAGE_FORENSIC_STATE) public void disable(IForensicServiceCommandCallback callback) { + disable_enforcePermission(); mService.mHandler.obtainMessage(MSG_DISABLE, callback).sendToTarget(); } } @@ -135,24 +143,18 @@ public class ForensicService extends SystemService { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_MONITOR_STATE: + case MSG_ADD_STATE_CALLBACK: try { - mService.monitorState( + mService.addStateCallback( (IForensicServiceStateCallback) msg.obj); } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); } break; - case MSG_MAKE_VISIBLE: + case MSG_REMOVE_STATE_CALLBACK: try { - mService.makeVisible((IForensicServiceCommandCallback) msg.obj); - } catch (RemoteException e) { - Slog.e(TAG, "RemoteException", e); - } - break; - case MSG_MAKE_INVISIBLE: - try { - mService.makeInvisible((IForensicServiceCommandCallback) msg.obj); + mService.removeStateCallback( + (IForensicServiceStateCallback) msg.obj); } catch (RemoteException e) { Slog.e(TAG, "RemoteException", e); } @@ -171,8 +173,8 @@ public class ForensicService extends SystemService { Slog.e(TAG, "RemoteException", e); } break; - case MSG_BACKUP: - mService.backup((List<ForensicEvent>) msg.obj); + case MSG_TRANSPORT: + mService.transport((List<ForensicEvent>) msg.obj); break; default: Slog.w(TAG, "Unknown message: " + msg.what); @@ -180,103 +182,83 @@ public class ForensicService extends SystemService { } } - private void monitorState(IForensicServiceStateCallback callback) throws RemoteException { - for (int i = 0; i < mStateMonitors.size(); i++) { - if (mStateMonitors.get(i).asBinder() == callback.asBinder()) { + private void addStateCallback(IForensicServiceStateCallback callback) throws RemoteException { + for (int i = 0; i < mStateCallbacks.size(); i++) { + if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) { return; } } - mStateMonitors.add(callback); + mStateCallbacks.add(callback); callback.onStateChange(mState); } - private void notifyStateMonitors() throws RemoteException { - for (int i = 0; i < mStateMonitors.size(); i++) { - mStateMonitors.get(i).onStateChange(mState); + private void removeStateCallback(IForensicServiceStateCallback callback) + throws RemoteException { + for (int i = 0; i < mStateCallbacks.size(); i++) { + if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) { + mStateCallbacks.remove(i); + return; + } } } - private void makeVisible(IForensicServiceCommandCallback callback) throws RemoteException { - switch (mState) { - case STATE_INVISIBLE: - if (!mDataAggregator.initialize()) { - callback.onFailure(ERROR_DATA_SOURCE_UNAVAILABLE); - break; - } - mState = STATE_VISIBLE; - notifyStateMonitors(); - callback.onSuccess(); - break; - case STATE_VISIBLE: - callback.onSuccess(); - break; - default: - callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + private void notifyStateMonitors() { + if (mStateCallbacks.size() >= MAX_STATE_CALLBACK_NUM) { + mStateCallbacks.removeFirst(); } - } - private void makeInvisible(IForensicServiceCommandCallback callback) throws RemoteException { - switch (mState) { - case STATE_VISIBLE: - case STATE_ENABLED: - mState = STATE_INVISIBLE; - notifyStateMonitors(); - callback.onSuccess(); - break; - case STATE_INVISIBLE: - callback.onSuccess(); - break; - default: - callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + for (int i = 0; i < mStateCallbacks.size(); i++) { + try { + mStateCallbacks.get(i).onStateChange(mState); + } catch (RemoteException e) { + mStateCallbacks.remove(i); + } } } private void enable(IForensicServiceCommandCallback callback) throws RemoteException { - switch (mState) { - case STATE_VISIBLE: - if (!mBackupTransportConnection.initialize()) { - callback.onFailure(ERROR_BACKUP_TRANSPORT_UNAVAILABLE); - break; - } - mDataAggregator.enable(); - mState = STATE_ENABLED; - notifyStateMonitors(); - callback.onSuccess(); - break; - case STATE_ENABLED: - callback.onSuccess(); - break; - default: - callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + if (mState == STATE_ENABLED) { + callback.onSuccess(); + return; } + + // TODO: temporarily disable the following for the CTS ForensicManagerTest. + // Enable it when the transport component is ready. + // if (!mForensicEventTransportConnection.initialize()) { + // callback.onFailure(ERROR_TRANSPORT_UNAVAILABLE); + // return; + // } + + mDataAggregator.enable(); + mState = STATE_ENABLED; + notifyStateMonitors(); + callback.onSuccess(); } private void disable(IForensicServiceCommandCallback callback) throws RemoteException { - switch (mState) { - case STATE_ENABLED: - mBackupTransportConnection.release(); - mDataAggregator.disable(); - mState = STATE_VISIBLE; - notifyStateMonitors(); - callback.onSuccess(); - break; - case STATE_VISIBLE: - callback.onSuccess(); - break; - default: - callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + if (mState == STATE_DISABLED) { + callback.onSuccess(); + return; } + + // TODO: temporarily disable the following for the CTS ForensicManagerTest. + // Enable it when the transport component is ready. + // mForensicEventTransportConnection.release(); + mDataAggregator.disable(); + mState = STATE_DISABLED; + notifyStateMonitors(); + callback.onSuccess(); } /** * Add a list of ForensicEvent. */ public void addNewData(List<ForensicEvent> events) { - mHandler.obtainMessage(MSG_BACKUP, events).sendToTarget(); + mHandler.obtainMessage(MSG_TRANSPORT, events).sendToTarget(); } - private void backup(List<ForensicEvent> events) { - mBackupTransportConnection.addData(events); + private void transport(List<ForensicEvent> events) { + mForensicEventTransportConnection.addData(events); } @Override @@ -296,9 +278,11 @@ public class ForensicService extends SystemService { interface Injector { Context getContext(); + PermissionEnforcer getPermissionEnforcer(); + Looper getLooper(); - BackupTransportConnection getBackupTransportConnection(); + ForensicEventTransportConnection getForensicEventransportConnection(); DataAggregator getDataAggregator(ForensicService forensicService); } @@ -315,6 +299,10 @@ public class ForensicService extends SystemService { return mContext; } + @Override + public PermissionEnforcer getPermissionEnforcer() { + return PermissionEnforcer.fromContext(mContext); + } @Override public Looper getLooper() { @@ -326,8 +314,8 @@ public class ForensicService extends SystemService { } @Override - public BackupTransportConnection getBackupTransportConnection() { - return new BackupTransportConnection(mContext); + public ForensicEventTransportConnection getForensicEventransportConnection() { + return new ForensicEventTransportConnection(mContext); } @Override diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index 4b2d45430bb4..cf145f94f787 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -228,13 +228,11 @@ class InsetsStateController { changed |= provider.updateClientVisibility(caller, isImeProvider ? statsToken : null); } - if (!android.view.inputmethod.Flags.refactorInsetsController()) { - if (changed) { - notifyInsetsChanged(); - mDisplayContent.updateSystemGestureExclusion(); + if (changed) { + notifyInsetsChanged(); + mDisplayContent.updateSystemGestureExclusion(); - mDisplayContent.getDisplayPolicy().updateSystemBarAttributes(); - } + mDisplayContent.getDisplayPolicy().updateSystemBarAttributes(); } } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 6707a27d83c8..f50417d6659e 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2856,7 +2856,10 @@ class RootWindowContainer extends WindowContainer<DisplayContent> void prepareForShutdown() { for (int i = 0; i < getChildCount(); i++) { - createSleepToken("shutdown", getChildAt(i).mDisplayId); + final int displayId = getChildAt(i).mDisplayId; + mWindowManager.mSnapshotController.mTaskSnapshotController + .snapshotForShutdown(displayId); + createSleepToken("shutdown", displayId); } } diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java index 1c8c245f7640..bd8e8f4008de 100644 --- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java +++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java @@ -64,6 +64,7 @@ class SnapshotPersistQueue { private boolean mStarted; private final Object mLock = new Object(); private final UserManagerInternal mUserManagerInternal; + private boolean mShutdown; SnapshotPersistQueue() { mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -101,6 +102,16 @@ class SnapshotPersistQueue { } } + /** + * Write out everything in the queue because of shutdown. + */ + void shutdown() { + synchronized (mLock) { + mShutdown = true; + mLock.notifyAll(); + } + } + @VisibleForTesting void waitForQueueEmpty() { while (true) { @@ -193,7 +204,9 @@ class SnapshotPersistQueue { if (isReadyToWrite) { next.write(); } - SystemClock.sleep(DELAY_MS); + if (!mShutdown) { + SystemClock.sleep(DELAY_MS); + } } synchronized (mLock) { final boolean writeQueueEmpty = mWriteQueue.isEmpty(); diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index 1f82cdb70b91..9fe3f7563902 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -307,6 +307,28 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot } /** + * Record task snapshots before shutdown. + */ + void snapshotForShutdown(int displayId) { + if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) { + return; + } + final DisplayContent displayContent = mService.mRoot.getDisplayContent(displayId); + if (displayContent == null) { + return; + } + displayContent.forAllLeafTasks(task -> { + if (task.isVisible() && !task.isActivityTypeHome()) { + final TaskSnapshot snapshot = captureSnapshot(task); + if (snapshot != null) { + mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); + } + } + }, true /* traverseTopToBottom */); + mPersister.mSnapshotPersistQueue.shutdown(); + } + + /** * Called when screen is being turned off. */ void screenTurningOff(int displayId, ScreenOffListener listener) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index ead12826c263..091896590b6b 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -788,7 +788,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub deferResume = false; // Already calls ensureActivityConfig mService.mRootWindowContainer.ensureActivitiesVisible(); - mService.mRootWindowContainer.resumeFocusedTasksTopActivities(); + if (!mService.mRootWindowContainer.resumeFocusedTasksTopActivities()) { + mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT-effects"); + } } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) { for (int i = haveConfigChanges.size() - 1; i >= 0; --i) { haveConfigChanges.valueAt(i).forAllActivities(r -> { @@ -886,7 +888,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (windowingMode > -1) { if (mService.isInLockTaskMode() - && WindowConfiguration.inMultiWindowMode(windowingMode)) { + && WindowConfiguration.inMultiWindowMode(windowingMode) + && !container.isEmbedded()) { Slog.w(TAG, "Dropping unsupported request to set multi-window windowing mode" + " during locked task mode."); return effects; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index c19c58e4ba13..cb333f02757e 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -1236,6 +1236,8 @@ final class DevicePolicyEngine { } } for (EnforcingAdmin admin : admins) { + // No need to make changes to system enforcing admins. + if (admin.isSystemAuthority()) break; if (updatedPackage == null || updatedPackage.equals(admin.getPackageName())) { if (!isPackageInstalled(admin.getPackageName(), userId)) { Slogf.i(TAG, String.format( diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 8ad878627804..90c3dff86280 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -376,6 +376,7 @@ import android.app.backup.IBackupManager; import android.app.compat.CompatChanges; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; +import android.app.supervision.SupervisionManagerInternal; import android.app.trust.TrustManager; import android.app.usage.UsageStatsManagerInternal; import android.compat.annotation.ChangeId; @@ -926,6 +927,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { final UsageStatsManagerInternal mUsageStatsManagerInternal; final TelephonyManager mTelephonyManager; final RoleManager mRoleManager; + final SupervisionManagerInternal mSupervisionManagerInternal; + private final LockPatternUtils mLockPatternUtils; private final LockSettingsInternal mLockSettingsInternal; private final DeviceAdminServiceController mDeviceAdminServiceController; @@ -2082,6 +2085,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { boolean isAdminInstalledCaCertAutoApproved() { return false; } + + @Nullable + SupervisionManagerInternal getSupervisionManager() { + return LocalServices.getService(SupervisionManagerInternal.class); + } } /** @@ -2113,6 +2121,11 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { mIPermissionManager = Objects.requireNonNull(injector.getIPermissionManager()); mTelephonyManager = Objects.requireNonNull(injector.getTelephonyManager()); mRoleManager = Objects.requireNonNull(injector.getRoleManager()); + if (Flags.secondaryLockscreenApiEnabled()) { + mSupervisionManagerInternal = injector.getSupervisionManager(); + } else { + mSupervisionManagerInternal = null; + } mLocalService = new LocalService(); mLockPatternUtils = injector.newLockPatternUtils(); @@ -2234,7 +2247,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return Collections.unmodifiableSet(packageNames); } - private @Nullable String getDefaultRoleHolderPackageName(int resId) { String packageNameAndSignature = mContext.getString(resId); @@ -13362,10 +13374,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Objects.requireNonNull(systemEntity); CallerIdentity caller = getCallerIdentity(); - if (caller.getUid() != Process.SYSTEM_UID) { + if (!isSystemUid(caller)) { throw new SecurityException("Only system services can call setUserRestrictionForUser" + " on a target user: " + targetUser); } + if (!UserRestrictionsUtils.isValidRestriction(key)) { + throw new IllegalArgumentException("Invalid restriction key: " + key); + } if (VERBOSE_LOG) { Slogf.v(LOG_TAG, "Creating SystemEnforcingAdmin %s for calling package %s", systemEntity, caller.getPackageName()); @@ -13498,6 +13513,31 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { logUserRestrictionCall(key, /* enabled= */ true, /* parent= */ false, caller, UserHandle.USER_ALL); } + + @Override + public void setUserRestrictionGloballyFromSystem(@NonNull String systemEntity, String key, + boolean enabled) { + Objects.requireNonNull(systemEntity); + + CallerIdentity caller = getCallerIdentity(); + if (!isSystemUid(caller)) { + throw new SecurityException("Only system services can call" + + " setUserRestrictionGloballyFromSystem"); + } + if (!UserRestrictionsUtils.isValidRestriction(key)) { + throw new IllegalArgumentException("Invalid restriction key: " + key); + } + if (VERBOSE_LOG) { + Slogf.v(LOG_TAG, "Creating SystemEnforcingAdmin %s for calling package %s", + systemEntity, caller.getPackageName()); + } + EnforcingAdmin admin = EnforcingAdmin.createSystemEnforcingAdmin(systemEntity); + + setGlobalUserRestrictionInternal(admin, key, enabled); + + logUserRestrictionCall(key, enabled, /* parent= */ false, caller, UserHandle.USER_ALL); + } + private void setLocalUserRestrictionInternal( EnforcingAdmin admin, String key, boolean enabled, int userId) { PolicyDefinition<Boolean> policyDefinition = @@ -13515,6 +13555,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { userId); } } + private void setGlobalUserRestrictionInternal( EnforcingAdmin admin, String key, boolean enabled) { PolicyDefinition<Boolean> policyDefinition = @@ -14556,34 +14597,76 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private boolean hasActiveSupervisionTestAdminLocked(@UserIdInt int userId) { + ensureLocked(); + if (mConstants.USE_TEST_ADMIN_AS_SUPERVISION_COMPONENT) { + final DevicePolicyData policy = getUserData(userId); + for (ActiveAdmin admin : policy.mAdminMap.values()) { + if (admin != null && admin.testOnlyAdmin) { + return true; + } + } + } + return false; + } + @Override public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled, PersistableBundle options) { - Objects.requireNonNull(who, "ComponentName is null"); - - // Check can set secondary lockscreen enabled - final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isProfileOwner(caller)); - Preconditions.checkCallAuthorization(!isManagedProfile(caller.getUserId()), - "User %d is not allowed to call setSecondaryLockscreenEnabled", + if (Flags.secondaryLockscreenApiEnabled()) { + final CallerIdentity caller = getCallerIdentity(); + final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller); + synchronized (getLockObject()) { + // TODO(b/378102594): Remove access for test admins. + final boolean isTestAdmin = hasActiveSupervisionTestAdminLocked(caller.getUserId()); + Preconditions.checkCallAuthorization(isRoleHolder || isTestAdmin, + "Caller (%d) is not the SYSTEM_SUPERVISION role holder", caller.getUserId()); + } - synchronized (getLockObject()) { - // Allow testOnly admins to bypass supervision config requirement. - Preconditions.checkCallAuthorization(isAdminTestOnlyLocked(who, caller.getUserId()) - || isSupervisionComponentLocked(caller.getComponentName()), "Admin %s is not " - + "the default supervision component", caller.getComponentName()); - DevicePolicyData policy = getUserData(caller.getUserId()); - policy.mSecondaryLockscreenEnabled = enabled; - saveSettingsLocked(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()); + } + } + } else { + Objects.requireNonNull(who, "ComponentName is null"); + + // Check can set secondary lockscreen enabled + final CallerIdentity caller = getCallerIdentity(who); + Preconditions.checkCallAuthorization( + isDefaultDeviceOwner(caller) || isProfileOwner(caller)); + Preconditions.checkCallAuthorization(!isManagedProfile(caller.getUserId()), + "User %d is not allowed to call setSecondaryLockscreenEnabled", + caller.getUserId()); + + synchronized (getLockObject()) { + // Allow testOnly admins to bypass supervision config requirement. + Preconditions.checkCallAuthorization(isAdminTestOnlyLocked(who, caller.getUserId()) + || isSupervisionComponentLocked(caller.getComponentName()), + "Admin %s is not the default supervision component", + caller.getComponentName()); + DevicePolicyData policy = getUserData(caller.getUserId()); + policy.mSecondaryLockscreenEnabled = enabled; + saveSettingsLocked(caller.getUserId()); + } } } @Override public boolean isSecondaryLockscreenEnabled(@NonNull UserHandle userHandle) { - synchronized (getLockObject()) { - return getUserData(userHandle.getIdentifier()).mSecondaryLockscreenEnabled; + if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) { + return mSupervisionManagerInternal.isSupervisionLockscreenEnabledForUser( + userHandle.getIdentifier()); + } else { + synchronized (getLockObject()) { + return getUserData(userHandle.getIdentifier()).mSecondaryLockscreenEnabled; + } } } @@ -19119,6 +19202,14 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } } + private boolean isAnyResetPasswordTokenActiveForUser(int userId) { + return mDevicePolicyEngine + .getLocalPoliciesSetByAdmins(PolicyDefinition.RESET_PASSWORD_TOKEN, userId) + .values() + .stream() + .anyMatch((p) -> isResetPasswordTokenActiveForUserLocked(p.getValue(), userId)); + } + private boolean isResetPasswordTokenActiveForUserLocked( long passwordTokenHandle, int userHandle) { if (passwordTokenHandle != 0) { @@ -20974,6 +21065,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { Preconditions.checkCallAuthorization(isSystemUid(getCallerIdentity()), String.format(NOT_SYSTEM_CALLER_MSG, "call canProfileOwnerResetPasswordWhenLocked")); + if (Flags.resetPasswordWithTokenCoexistence()) { + return isAnyResetPasswordTokenActiveForUser(userId); + } synchronized (getLockObject()) { final ActiveAdmin poAdmin = getProfileOwnerAdminLocked(userId); DevicePolicyData policy = getUserData(userId); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java index 634f1bc97772..58e3a7d236b4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java @@ -280,6 +280,10 @@ final class EnforcingAdmin { return getAuthorities().contains(authority); } + boolean isSystemAuthority() { + return mIsSystemAuthority; + } + @NonNull String getPackageName() { return mPackageName; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 19b03437292f..3e7c4ef4019f 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -251,6 +251,7 @@ import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService; import com.android.server.security.advancedprotection.AdvancedProtectionService; +import com.android.server.security.forensic.ForensicService; import com.android.server.security.rkp.RemoteProvisioningService; import com.android.server.selinux.SelinuxAuditLogsService; import com.android.server.sensorprivacy.SensorPrivacyService; @@ -1760,6 +1761,13 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(LogcatManagerService.class); t.traceEnd(); + if (!isWatch && !isTv && !isAutomotive + && android.security.Flags.aflApi()) { + t.traceBegin("StartForensicService"); + mSystemServiceManager.startService(ForensicService.class); + t.traceEnd(); + } + if (AppFunctionManagerConfiguration.isSupported(context)) { t.traceBegin("StartAppFunctionManager"); mSystemServiceManager.startService(AppFunctionManagerService.class); @@ -2761,8 +2769,9 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(WEAR_MODE_SERVICE_CLASS); t.traceEnd(); - boolean enableWristOrientationService = SystemProperties.getBoolean( - "config.enable_wristorientation", false); + boolean enableWristOrientationService = + !android.server.Flags.migrateWristOrientation() + && SystemProperties.getBoolean("config.enable_wristorientation", false); if (enableWristOrientationService) { t.traceBegin("StartWristOrientationService"); mSystemServiceManager.startService(WRIST_ORIENTATION_SERVICE_CLASS); diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig index e2ac22de29a4..4412968999e5 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -39,6 +39,14 @@ flag { } flag { + name: "migrate_wrist_orientation" + namespace: "wear_frameworks" + description: "Migrate wrist orientation service functionality to wear settings service" + bug: "352725980" + is_fixed_read_only: true +} + +flag { name: "allow_network_time_update_service" namespace: "wear_systems" description: "Allow NetworkTimeUpdateService on Wear" diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java index 67e254782f6d..53a25dd454ef 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionService.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java @@ -21,10 +21,11 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.admin.DevicePolicyManagerInternal; import android.app.supervision.ISupervisionManager; +import android.app.supervision.SupervisionManagerInternal; import android.content.ComponentName; import android.content.Context; import android.content.pm.UserInfo; -import android.os.Bundle; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; @@ -179,8 +180,15 @@ public class SupervisionService extends ISupervisionManager.Stub { } @Override + public boolean isSupervisionLockscreenEnabledForUser(@UserIdInt int userId) { + synchronized (getLockObject()) { + return getUserDataLocked(userId).supervisionLockScreenEnabled; + } + } + + @Override public void setSupervisionLockscreenEnabledForUser( - @UserIdInt int userId, boolean enabled, @Nullable Bundle options) { + @UserIdInt int userId, boolean enabled, @Nullable PersistableBundle options) { synchronized (getLockObject()) { SupervisionUserData data = getUserDataLocked(userId); data.supervisionLockScreenEnabled = enabled; diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java index 56162372f740..1dd48f581bf4 100644 --- a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java +++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java @@ -19,7 +19,7 @@ package com.android.server.supervision; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.os.Bundle; +import android.os.PersistableBundle; import android.util.IndentingPrintWriter; /** User specific data, used internally by the {@link SupervisionService}. */ @@ -27,7 +27,7 @@ public class SupervisionUserData { public final @UserIdInt int userId; public boolean supervisionEnabled; public boolean supervisionLockScreenEnabled; - @Nullable public Bundle supervisionLockScreenOptions; + @Nullable public PersistableBundle supervisionLockScreenOptions; public SupervisionUserData(@UserIdInt int userId) { this.userId = userId; diff --git a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java index 08155c7b3f98..9772ef929eae 100644 --- a/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java +++ b/services/tests/VpnTests/java/com/android/server/connectivity/VpnTest.java @@ -2380,10 +2380,14 @@ public class VpnTest extends VpnTestBase { @Test public void doTestMigrateIkeSession_Vcn() throws Exception { final int expectedKeepalive = 2097; // Any unlikely number will do - final NetworkCapabilities vcnNc = new NetworkCapabilities.Builder() - .addTransportType(TRANSPORT_CELLULAR) - .setTransportInfo(new VcnTransportInfo(TEST_SUB_ID, expectedKeepalive)) - .build(); + final NetworkCapabilities vcnNc = + new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .setTransportInfo( + new VcnTransportInfo.Builder() + .setMinUdpPort4500NatTimeoutSeconds(expectedKeepalive) + .build()) + .build(); final Ikev2VpnProfile ikev2VpnProfile = makeIkeV2VpnProfile( true /* isAutomaticIpVersionSelectionEnabled */, true /* isAutomaticNattKeepaliveTimerEnabled */, 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 80e5ee39c13d..759976f79371 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -226,6 +226,9 @@ public class DisplayManagerServiceTest { "EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED"; private static final String EVENT_DISPLAY_CONNECTED = "EVENT_DISPLAY_CONNECTED"; private static final String EVENT_DISPLAY_DISCONNECTED = "EVENT_DISPLAY_DISCONNECTED"; + private static final String EVENT_DISPLAY_REFRESH_RATE_CHANGED = + "EVENT_DISPLAY_REFRESH_RATE_CHANGED"; + private static final String EVENT_DISPLAY_STATE_CHANGED = "EVENT_DISPLAY_STATE_CHANGED"; private static final String DISPLAY_GROUP_EVENT_ADDED = "DISPLAY_GROUP_EVENT_ADDED"; private static final String DISPLAY_GROUP_EVENT_REMOVED = "DISPLAY_GROUP_EVENT_REMOVED"; private static final String DISPLAY_GROUP_EVENT_CHANGED = "DISPLAY_GROUP_EVENT_CHANGED"; @@ -4234,6 +4237,10 @@ public class DisplayManagerServiceTest { return EVENT_DISPLAY_CONNECTED; case DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED: return EVENT_DISPLAY_DISCONNECTED; + case DisplayManagerGlobal.EVENT_DISPLAY_REFRESH_RATE_CHANGED: + return EVENT_DISPLAY_REFRESH_RATE_CHANGED; + case DisplayManagerGlobal.EVENT_DISPLAY_STATE_CHANGED: + return EVENT_DISPLAY_STATE_CHANGED; default: return "UNKNOWN: " + eventType; } 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 b6da3ae6a5cd..ff652a2727de 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -35,7 +35,9 @@ import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DE import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED; +import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED; +import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED; import static com.android.server.display.layout.Layout.Display.POSITION_REAR; import static com.android.server.display.layout.Layout.Display.POSITION_UNKNOWN; import static com.android.server.utils.FoldSettingProvider.SETTING_VALUE_SELECTIVE_STAY_AWAKE; @@ -1158,6 +1160,29 @@ public class LogicalDisplayMapperTest { mLogicalDisplayMapper.getDisplayLocked(device2).getDevicePositionLocked()); } + @Test + public void updateAndGetMaskForDisplayPropertyChanges_getsPropertyChangedFlags() { + // Change the display state + DisplayInfo newDisplayInfo = new DisplayInfo(); + newDisplayInfo.state = STATE_OFF; + assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED, + mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); + + // Change the refresh rate override + newDisplayInfo = new DisplayInfo(); + newDisplayInfo.refreshRateOverride = 30; + assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED, + mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); + + // Change multiple properties + newDisplayInfo = new DisplayInfo(); + newDisplayInfo.refreshRateOverride = 30; + newDisplayInfo.state = STATE_OFF; + assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED + | LOGICAL_DISPLAY_EVENT_STATE_CHANGED, + mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); + } + ///////////////// // Helper Methods ///////////////// diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 95acd75f32cc..993569fb17fe 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -78,7 +78,7 @@ android_test { "am_flags_lib", "device_policy_aconfig_flags_lib", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { - "true": ["service-crashrecovery.impl"], + "true": ["service-crashrecovery-pre-jarjar"], default: [], }), diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index 30de0e8c7981..8dc8c14f8948 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -67,7 +67,6 @@ import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHARGING import static com.android.server.alarm.AlarmManagerService.AlarmHandler.CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REFRESH_EXACT_ALARM_CANDIDATES; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_ALARMS; -import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_FOR_CANCELED; import static com.android.server.alarm.AlarmManagerService.AlarmHandler.TEMPORARY_QUOTA_CHANGED; import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_COMPAT_QUOTA; @@ -152,7 +151,6 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.flag.util.FlagSetException; @@ -436,8 +434,7 @@ public final class AlarmManagerServiceTest { */ private void disableFlagsNotSetByAnnotation() { try { - mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS, - Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS); + mSetFlagsRule.disableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS); } catch (FlagSetException fse) { // Expected if the test about to be run requires this enabled. } @@ -523,13 +520,11 @@ public final class AlarmManagerServiceTest { mService.onStart(); - if (Flags.useFrozenStateToDropListenerAlarms()) { - final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor = - ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class); - verify(mActivityManager).registerUidFrozenStateChangedCallback( - any(HandlerExecutor.class), frozenCaptor.capture()); - mUidFrozenStateCallback = frozenCaptor.getValue(); - } + final ArgumentCaptor<ActivityManager.UidFrozenStateChangedCallback> frozenCaptor = + ArgumentCaptor.forClass(ActivityManager.UidFrozenStateChangedCallback.class); + verify(mActivityManager).registerUidFrozenStateChangedCallback( + any(HandlerExecutor.class), frozenCaptor.capture()); + mUidFrozenStateCallback = frozenCaptor.getValue(); // Unable to mock mMockContext to return a mock stats manager. // So just mocking the whole MetricsHelper instance. @@ -3744,79 +3739,11 @@ public final class AlarmManagerServiceTest { testTemporaryQuota_bumpedBeforeDeferral(STANDBY_BUCKET_RARE); } - @Test - public void exactListenerAlarmsRemovedOnCached() { - mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true); - - setTestAlarmWithListener(ELAPSED_REALTIME, 31, getNewListener(() -> {}), WINDOW_EXACT, - TEST_CALLING_UID); - setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID); - setTestAlarm(ELAPSED_REALTIME, 54, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0, - TEST_CALLING_UID, null); - setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null); - - setTestAlarmWithListener(ELAPSED_REALTIME, 21, getNewListener(() -> {}), WINDOW_EXACT, - TEST_CALLING_UID_2); - setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2); - setTestAlarm(ELAPSED_REALTIME, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0, - TEST_CALLING_UID_2, null); - setTestAlarm(RTC, 549, 234, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID_2, null); - - assertEquals(8, mService.mAlarmStore.size()); - - mListener.handleUidCachedChanged(TEST_CALLING_UID, true); - assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED); - assertEquals(7, mService.mAlarmStore.size()); - - mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true); - assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED); - assertEquals(6, mService.mAlarmStore.size()); - } - - @Test - public void alarmCountOnListenerCached() { - mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true); - - // Set some alarms for TEST_CALLING_UID. - final int numExactListenerUid1 = 14; - for (int i = 0; i < numExactListenerUid1; i++) { - setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i, - getNewListener(() -> {})); - } - setTestAlarmWithListener(RTC, 42, getNewListener(() -> {}), 56, TEST_CALLING_UID); - setTestAlarm(ELAPSED_REALTIME, 54, getNewMockPendingIntent()); - setTestAlarm(RTC, 49, 154, getNewMockPendingIntent(), 0, 0, TEST_CALLING_UID, null); - - // Set some alarms for TEST_CALLING_UID_2. - final int numExactListenerUid2 = 9; - for (int i = 0; i < numExactListenerUid2; i++) { - setTestAlarmWithListener(ALARM_TYPES[i % 4], mNowElapsedTest + i, - getNewListener(() -> {}), WINDOW_EXACT, TEST_CALLING_UID_2); - } - setTestAlarmWithListener(RTC, 412, getNewListener(() -> {}), 561, TEST_CALLING_UID_2); - setTestAlarm(RTC_WAKEUP, 26, WINDOW_EXACT, getNewMockPendingIntent(), 0, 0, - TEST_CALLING_UID_2, null); - - assertEquals(numExactListenerUid1 + 3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); - assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); - - mListener.handleUidCachedChanged(TEST_CALLING_UID, true); - assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED); - assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); - assertEquals(numExactListenerUid2 + 2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); - - mListener.handleUidCachedChanged(TEST_CALLING_UID_2, true); - assertAndHandleMessageSync(REMOVE_EXACT_LISTENER_ALARMS_ON_CACHED); - assertEquals(3, mService.mAlarmsPerUid.get(TEST_CALLING_UID)); - assertEquals(2, mService.mAlarmsPerUid.get(TEST_CALLING_UID_2)); - } - private void executeUidFrozenStateCallback(int[] uids, int[] frozenStates) { assertNotNull(mUidFrozenStateCallback); mUidFrozenStateCallback.onUidFrozenStateChanged(uids, frozenStates); } - @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS) @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS) @Test public void exactListenerAlarmsRemovedOnFrozen() { @@ -3848,7 +3775,6 @@ public final class AlarmManagerServiceTest { assertEquals(6, mService.mAlarmStore.size()); } - @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS) @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS) @Test public void alarmCountOnListenerFrozen() { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java index a1a8b0ec7d2f..1caa02a6dff2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java @@ -185,10 +185,10 @@ public abstract class BaseBroadcastQueueTest { doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker(); + doReturn(true).when(mPlatformCompat).isChangeEnabledInternalNoLogging( + eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), any(ApplicationInfo.class)); doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); - doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt()); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), anyInt()); } public void tearDown() throws Exception { @@ -298,7 +298,7 @@ public abstract class BaseBroadcastQueueTest { filter.setPriority(priority); final BroadcastFilter res = new BroadcastFilter(filter, receiverList, receiverList.app.info.packageName, null, null, null, receiverList.uid, - receiverList.userId, false, false, true, + receiverList.userId, false, false, true, receiverList.app.info, mock(PlatformCompat.class)); receiverList.add(res); return res; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java index e977a7d46f30..5d106ace71dd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastFilterTest.java @@ -20,10 +20,13 @@ import static android.content.IntentFilter.SYSTEM_LOW_PRIORITY; import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import android.content.pm.ApplicationInfo; import android.os.Process; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; @@ -53,14 +56,14 @@ public class BroadcastFilterTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + + doReturn(true).when(mPlatformCompat).isChangeEnabledInternalNoLogging( + eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), any(ApplicationInfo.class)); } @Test @EnableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES) public void testCalculateAdjustedPriority() { - doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); - { // Pairs of {initial-priority, expected-adjusted-priority} final Pair<Integer, Integer>[] priorities = new Pair[] { @@ -95,8 +98,8 @@ public class BroadcastFilterTest { @Test @EnableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES) public void testCalculateAdjustedPriority_withChangeIdDisabled() { - doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); + doReturn(false).when(mPlatformCompat).isChangeEnabledInternalNoLogging( + eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), any(ApplicationInfo.class)); { // Pairs of {initial-priority, expected-adjusted-priority} @@ -132,9 +135,6 @@ public class BroadcastFilterTest { @Test @DisableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES) public void testCalculateAdjustedPriority_withFlagDisabled() { - doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); - { // Pairs of {initial-priority, expected-adjusted-priority} final Pair<Integer, Integer>[] priorities = new Pair[] { @@ -170,7 +170,7 @@ public class BroadcastFilterTest { @DisableFlags(Flags.FLAG_RESTRICT_PRIORITY_VALUES) public void testCalculateAdjustedPriority_withFlagDisabled_withChangeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastFilter.CHANGE_RESTRICT_PRIORITY_VALUES), anyInt()); + eq(BroadcastFilter.RESTRICT_PRIORITY_VALUES), anyInt()); { // Pairs of {initial-priority, expected-adjusted-priority} @@ -215,6 +215,7 @@ public class BroadcastFilterTest { final String errorMsg = String.format("owner=%d; actualPriority=%d; expectedPriority=%d", owningUid, priority, expectedAdjustedPriority); assertWithMessage(errorMsg).that(BroadcastFilter.calculateAdjustedPriority( - owningUid, priority, mPlatformCompat)).isEqualTo(expectedAdjustedPriority); + owningUid, priority, mock(ApplicationInfo.class), mPlatformCompat)) + .isEqualTo(expectedAdjustedPriority); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 1481085c5f71..88caaa61eacf 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -717,7 +717,7 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { @Test public void testRunnableAt_Cached_Prioritized_NonDeferrable_changeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getUidForPackage(PACKAGE_GREEN))); final List receivers = List.of( withPriority(makeManifestReceiver(PACKAGE_RED, PACKAGE_RED), 10), @@ -1289,7 +1289,7 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { @Test public void testDeliveryGroupPolicy_prioritized_diffReceivers_changeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getUidForPackage(PACKAGE_GREEN))); final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON); @@ -1824,7 +1824,7 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { @Test public void testDeliveryDeferredForCached_changeIdDisabled() throws Exception { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getUidForPackage(PACKAGE_GREEN))); final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); @@ -2028,7 +2028,7 @@ public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest { public void testDeliveryDeferredForCached_withInfiniteDeferred_changeIdDisabled() throws Exception { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getUidForPackage(PACKAGE_GREEN))); final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN)); final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED)); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 9d92d5fe4f60..a38ef78f8c64 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -1660,7 +1660,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { final ProcessRecord receiverYellowApp = makeActiveProcessRecord(PACKAGE_YELLOW); doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); // Enqueue a normal broadcast that will go to several processes, and // then enqueue a foreground broadcast that risks reordering @@ -2472,7 +2472,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN); doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(receiverBlueApp.uid)); mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java index a424bfdb8df4..f9f3790cae10 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastRecordTest.java @@ -19,7 +19,7 @@ package com.android.server.am; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.server.am.BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE; +import static com.android.server.am.BroadcastRecord.LIMIT_PRIORITY_SCOPE; import static com.android.server.am.BroadcastRecord.DELIVERY_DEFERRED; import static com.android.server.am.BroadcastRecord.DELIVERY_DELIVERED; import static com.android.server.am.BroadcastRecord.DELIVERY_PENDING; @@ -109,7 +109,7 @@ public class BroadcastRecordTest { MockitoAnnotations.initMocks(this); doReturn(true).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), anyInt()); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), anyInt()); } @Test @@ -223,7 +223,7 @@ public class BroadcastRecordTest { @Test public void testIsPrioritized_withDifferentPriorities_withFirstUidChangeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); assertTrue(isPrioritized(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), @@ -257,7 +257,7 @@ public class BroadcastRecordTest { @Test public void testIsPrioritized_withDifferentPriorities_withLastUidChangeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); assertTrue(isPrioritized(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), @@ -295,7 +295,7 @@ public class BroadcastRecordTest { @Test public void testIsPrioritized_withDifferentPriorities_withUidChangeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); assertTrue(isPrioritized(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), @@ -329,9 +329,9 @@ public class BroadcastRecordTest { @Test public void testIsPrioritized_withDifferentPriorities_withMultipleUidChangeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); assertTrue(isPrioritized(List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), @@ -593,7 +593,7 @@ public class BroadcastRecordTest { @Test public void testSetDeliveryState_DeferUntilActive_changeIdDisabled() { doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); final BroadcastRecord r = createBroadcastRecord( new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of( createResolveInfo(PACKAGE1, getAppId(1), 10), @@ -961,7 +961,7 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE3, getAppId(3))))); doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(1))); assertArrayEquals(new boolean[] {false, true, true}, calculateChangeState( List.of(createResolveInfo(PACKAGE1, getAppId(1)), createResolveInfo(PACKAGE2, getAppId(2)), @@ -973,7 +973,7 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE3, getAppId(3))))); doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(2))); assertArrayEquals(new boolean[] {false, false, true}, calculateChangeState( List.of(createResolveInfo(PACKAGE1, getAppId(1)), createResolveInfo(PACKAGE2, getAppId(2)), @@ -988,7 +988,7 @@ public class BroadcastRecordTest { createResolveInfo(PACKAGE3, getAppId(3))))); doReturn(false).when(mPlatformCompat).isChangeEnabledByUidInternalNoLogging( - eq(BroadcastRecord.CHANGE_LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); + eq(BroadcastRecord.LIMIT_PRIORITY_SCOPE), eq(getAppId(3))); assertArrayEquals(new boolean[] {false, false, false}, calculateChangeState( List.of(createResolveInfo(PACKAGE1, getAppId(1)), createResolveInfo(PACKAGE2, getAppId(2)), @@ -1005,7 +1005,7 @@ public class BroadcastRecordTest { private boolean[] calculateChangeState(List<Object> receivers) { return BroadcastRecord.calculateChangeStateForReceivers(receivers, - CHANGE_LIMIT_PRIORITY_SCOPE, mPlatformCompat); + LIMIT_PRIORITY_SCOPE, mPlatformCompat); } private static void cleanupDisabledPackageReceivers(BroadcastRecord record, diff --git a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp index 1f88c29e2abc..8eae9c7d71fa 100644 --- a/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp +++ b/services/tests/mockingservicestests/src/com/android/server/crashrecovery/Android.bp @@ -37,7 +37,7 @@ android_test { "truth", "flag-junit", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { - "true": ["service-crashrecovery.impl"], + "true": ["service-crashrecovery-pre-jarjar"], default: [], }), diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp index 2f23e02f5737..5a802d9de2ff 100644 --- a/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp +++ b/services/tests/mockingservicestests/src/com/android/server/rollback/Android.bp @@ -35,7 +35,7 @@ android_test { "truth", "flag-junit", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { - "true": ["service-crashrecovery.impl"], + "true": ["service-crashrecovery-pre-jarjar"], default: [], }), 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 58489f398775..0881b4cf9bcf 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 @@ -18,6 +18,7 @@ package com.android.server.power.hint; import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS; +import static com.android.server.power.hint.HintManagerService.DEFAULT_HEADROOM_PID; import static com.google.common.truth.Truth.assertThat; @@ -51,11 +52,15 @@ import android.content.pm.PackageManager; import android.hardware.common.fmq.MQDescriptor; import android.hardware.power.ChannelConfig; import android.hardware.power.ChannelMessage; +import android.hardware.power.CpuHeadroomParams; +import android.hardware.power.GpuHeadroomParams; import android.hardware.power.IPower; import android.hardware.power.SessionConfig; import android.hardware.power.SessionTag; import android.hardware.power.WorkDuration; import android.os.Binder; +import android.os.CpuHeadroomParamsInternal; +import android.os.GpuHeadroomParamsInternal; import android.os.IBinder; import android.os.IHintSession; import android.os.PerformanceHintManager; @@ -128,11 +133,11 @@ public class HintManagerServiceTest { private static final long[] TIMESTAMPS_ZERO = new long[] {}; private static final long[] TIMESTAMPS_TWO = new long[] {1L, 2L}; private static final WorkDuration[] WORK_DURATIONS_FIVE = new WorkDuration[] { - makeWorkDuration(1L, 11L, 1L, 8L, 4L), - makeWorkDuration(2L, 13L, 2L, 8L, 6L), - makeWorkDuration(3L, 333333333L, 3L, 8L, 333333333L), - makeWorkDuration(2L, 13L, 2L, 0L, 6L), - makeWorkDuration(2L, 13L, 2L, 8L, 0L), + makeWorkDuration(1L, 11L, 1L, 8L, 4L), + makeWorkDuration(2L, 13L, 2L, 8L, 6L), + makeWorkDuration(3L, 333333333L, 3L, 8L, 333333333L), + makeWorkDuration(2L, 13L, 2L, 0L, 6L), + makeWorkDuration(2L, 13L, 2L, 8L, 0L), }; private static final String TEST_APP_NAME = "com.android.test.app"; @@ -187,17 +192,17 @@ public class HintManagerServiceTest { when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(SESSION_TIDS_A), eq(DEFAULT_TARGET_DURATION), anyInt(), any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[0], - SESSION_IDS[0])); + SESSION_IDS[0])); when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(SESSION_TIDS_B), eq(DOUBLED_TARGET_DURATION), anyInt(), any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[1], - SESSION_IDS[1])); + SESSION_IDS[1])); when(mNativeWrapperMock.halCreateHintSessionWithConfig(eq(TGID), eq(UID), eq(SESSION_TIDS_C), eq(0L), anyInt(), any(SessionConfig.class))).thenAnswer(fakeCreateWithConfig(SESSION_PTRS[2], - SESSION_IDS[2])); + SESSION_IDS[2])); - when(mIPowerMock.getInterfaceVersion()).thenReturn(5); + when(mIPowerMock.getInterfaceVersion()).thenReturn(6); when(mIPowerMock.getSessionChannel(anyInt(), anyInt())).thenReturn(mConfig); LocalServices.removeServiceForTest(ActivityManagerInternal.class); LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock); @@ -217,8 +222,8 @@ public class HintManagerServiceTest { when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C), eq(0L))).thenReturn(SESSION_PTRS[2]); when(mNativeWrapperMock.halCreateHintSessionWithConfig(anyInt(), anyInt(), - any(int[].class), anyLong(), anyInt(), - any(SessionConfig.class))).thenThrow(new UnsupportedOperationException()); + any(int[].class), anyLong(), anyInt(), + any(SessionConfig.class))).thenThrow(new UnsupportedOperationException()); } static class NativeWrapperFake extends NativeWrapper { @@ -337,7 +342,7 @@ public class HintManagerServiceTest { SESSION_TIDS_C, 0L, SessionTag.OTHER, new SessionConfig()); assertNotNull(c); verify(mNativeWrapperMock, times(3)).halCreateHintSession(anyInt(), anyInt(), - any(int[].class), anyLong()); + any(int[].class), anyLong()); } @Test @@ -487,7 +492,7 @@ public class HintManagerServiceTest { AppHintSession a = (AppHintSession) service.getBinderServiceInstance() .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION, - SessionTag.OTHER, new SessionConfig()); + SessionTag.OTHER, new SessionConfig()); a.sendHint(PerformanceHintManager.Session.CPU_LOAD_RESET); verify(mNativeWrapperMock, times(1)).halSendHint(anyLong(), @@ -514,7 +519,7 @@ public class HintManagerServiceTest { AppHintSession a = (AppHintSession) service.getBinderServiceInstance() .createHintSessionWithConfig(token, SESSION_TIDS_A, DEFAULT_TARGET_DURATION, - SessionTag.OTHER, new SessionConfig()); + SessionTag.OTHER, new SessionConfig()); service.mUidObserver.onUidStateChanged( a.mUid, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0); @@ -1096,4 +1101,157 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(1)).getSessionChannel(eq(TGID), eq(UID)); assertTrue(service.hasChannel(TGID, UID)); } + + @Test + public void testHeadroomPowerHalNotSupported() throws Exception { + when(mIPowerMock.getInterfaceVersion()).thenReturn(5); + HintManagerService service = createService(); + assertThrows(UnsupportedOperationException.class, () -> { + service.getBinderServiceInstance().getCpuHeadroom(null); + }); + assertThrows(UnsupportedOperationException.class, () -> { + service.getBinderServiceInstance().getGpuHeadroom(null); + }); + assertThrows(UnsupportedOperationException.class, () -> { + service.getBinderServiceInstance().getCpuHeadroomMinIntervalMillis(); + }); + assertThrows(UnsupportedOperationException.class, () -> { + service.getBinderServiceInstance().getGpuHeadroomMinIntervalMillis(); + }); + } + + @Test + public void testCpuHeadroomCache() throws Exception { + when(mIPowerMock.getCpuHeadroomMinIntervalMillis()).thenReturn(2000L); + CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal(); + CpuHeadroomParams halParams1 = new CpuHeadroomParams(); + halParams1.calculationType = CpuHeadroomParams.CalculationType.MIN; + halParams1.selectionType = CpuHeadroomParams.SelectionType.ALL; + halParams1.pid = Process.myPid(); + + CpuHeadroomParamsInternal params2 = new CpuHeadroomParamsInternal(); + params2.usesDeviceHeadroom = true; + params2.calculationType = CpuHeadroomParams.CalculationType.AVERAGE; + params2.selectionType = CpuHeadroomParams.SelectionType.PER_CORE; + CpuHeadroomParams halParams2 = new CpuHeadroomParams(); + halParams2.calculationType = CpuHeadroomParams.CalculationType.AVERAGE; + halParams2.selectionType = CpuHeadroomParams.SelectionType.PER_CORE; + halParams2.pid = DEFAULT_HEADROOM_PID; + + float[] headroom1 = new float[] {0.1f}; + when(mIPowerMock.getCpuHeadroom(eq(halParams1))).thenReturn(headroom1); + float[] headroom2 = new float[] {0.1f, 0.5f}; + when(mIPowerMock.getCpuHeadroom(eq(halParams2))).thenReturn(headroom2); + + HintManagerService service = createService(); + clearInvocations(mIPowerMock); + + service.getBinderServiceInstance().getCpuHeadroomMinIntervalMillis(); + verify(mIPowerMock, times(0)).getCpuHeadroomMinIntervalMillis(); + service.getBinderServiceInstance().getCpuHeadroom(params1); + verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1)); + service.getBinderServiceInstance().getCpuHeadroom(params2); + verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams2)); + + // verify cache is working + clearInvocations(mIPowerMock); + assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1), + 0.01f); + assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(0)).getCpuHeadroom(any()); + + // after 1 more second it should be served with cache still + Thread.sleep(1000); + clearInvocations(mIPowerMock); + assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1), + 0.01f); + assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(0)).getCpuHeadroom(any()); + + // after 1.5 more second it should be served with cache still as timer reset + Thread.sleep(1500); + clearInvocations(mIPowerMock); + assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1), + 0.01f); + assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(0)).getCpuHeadroom(any()); + + // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval + Thread.sleep(2100); + clearInvocations(mIPowerMock); + assertArrayEquals(headroom1, service.getBinderServiceInstance().getCpuHeadroom(params1), + 0.01f); + assertArrayEquals(headroom2, service.getBinderServiceInstance().getCpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1)); + verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams2)); + } + + @Test + public void testGpuHeadroomCache() throws Exception { + when(mIPowerMock.getGpuHeadroomMinIntervalMillis()).thenReturn(2000L); + GpuHeadroomParamsInternal params1 = new GpuHeadroomParamsInternal(); + GpuHeadroomParams halParams1 = new GpuHeadroomParams(); + halParams1.calculationType = GpuHeadroomParams.CalculationType.MIN; + + GpuHeadroomParamsInternal params2 = new GpuHeadroomParamsInternal(); + GpuHeadroomParams halParams2 = new GpuHeadroomParams(); + params2.calculationType = + halParams2.calculationType = GpuHeadroomParams.CalculationType.AVERAGE; + + float headroom1 = 0.1f; + when(mIPowerMock.getGpuHeadroom(eq(halParams1))).thenReturn(headroom1); + float headroom2 = 0.2f; + when(mIPowerMock.getGpuHeadroom(eq(halParams2))).thenReturn(headroom2); + HintManagerService service = createService(); + clearInvocations(mIPowerMock); + + service.getBinderServiceInstance().getGpuHeadroomMinIntervalMillis(); + verify(mIPowerMock, times(0)).getGpuHeadroomMinIntervalMillis(); + assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1), + 0.01f); + assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams1)); + verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2)); + + // verify cache is working + clearInvocations(mIPowerMock); + assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1), + 0.01f); + assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(0)).getGpuHeadroom(any()); + + // after 1 more second it should be served with cache still + Thread.sleep(1000); + clearInvocations(mIPowerMock); + assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1), + 0.01f); + assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(0)).getGpuHeadroom(any()); + + // after 1.5 more second it should be served with cache still as timer reset + Thread.sleep(1500); + clearInvocations(mIPowerMock); + assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1), + 0.01f); + assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(0)).getGpuHeadroom(any()); + + // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval + Thread.sleep(2100); + clearInvocations(mIPowerMock); + assertEquals(headroom1, service.getBinderServiceInstance().getGpuHeadroom(params1), + 0.01f); + assertEquals(headroom2, service.getBinderServiceInstance().getGpuHeadroom(params2), + 0.01f); + verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams1)); + verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2)); + } } 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 359cf6376239..b48c2d7f5007 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -2705,12 +2705,11 @@ public class PowerManagerServiceTest { verify(mInvalidateInteractiveCachesMock).call(); listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId); - verify(mInvalidateInteractiveCachesMock, times(2)).call(); mService.setWakefulnessLocked(Display.DEFAULT_DISPLAY_GROUP, WAKEFULNESS_ASLEEP, mClock.now(), 0, PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0, null, null); - verify(mInvalidateInteractiveCachesMock, times(3)).call(); + verify(mInvalidateInteractiveCachesMock, times(2)).call(); } @Test @@ -2732,12 +2731,11 @@ public class PowerManagerServiceTest { verify(mInvalidateInteractiveCachesMock).call(); listener.get().onDisplayGroupAdded(nonDefaultDisplayGroupId); - verify(mInvalidateInteractiveCachesMock, times(2)).call(); mService.setWakefulnessLocked(nonDefaultDisplayGroupId, WAKEFULNESS_ASLEEP, mClock.now(), 0, PowerManager.GO_TO_SLEEP_REASON_APPLICATION, 0, null, null); - verify(mInvalidateInteractiveCachesMock, times(3)).call(); + verify(mInvalidateInteractiveCachesMock, times(2)).call(); } @Test diff --git a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java index 40e00344f87a..0da6db634330 100644 --- a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java +++ b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java @@ -16,9 +16,13 @@ package com.android.server.security.forensic; +import static android.Manifest.permission.MANAGE_FORENSIC_STATE; +import static android.Manifest.permission.READ_FORENSIC_STATE; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -29,7 +33,9 @@ import static org.mockito.Mockito.verify; import android.annotation.SuppressLint; import android.content.Context; import android.os.Looper; +import android.os.PermissionEnforcer; import android.os.RemoteException; +import android.os.test.FakePermissionEnforcer; import android.os.test.TestLooper; import android.security.forensic.ForensicEvent; import android.security.forensic.IForensicServiceCommandCallback; @@ -41,6 +47,7 @@ import androidx.test.core.app.ApplicationProvider; import com.android.server.ServiceThread; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -50,34 +57,36 @@ import java.util.Map; public class ForensicServiceTest { private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN; - private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE; - private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE; + private static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED; private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED; private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN; private static final int ERROR_PERMISSION_DENIED = IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED; - private static final int ERROR_INVALID_STATE_TRANSITION = - IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION; - private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE = - IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE; + private static final int ERROR_TRANSPORT_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE; private static final int ERROR_DATA_SOURCE_UNAVAILABLE = IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; private Context mContext; - private BackupTransportConnection mBackupTransportConnection; + private ForensicEventTransportConnection mForensicEventTransportConnection; private DataAggregator mDataAggregator; private ForensicService mForensicService; private TestLooper mTestLooper; private Looper mLooper; private TestLooper mTestLooperOfDataAggregator; private Looper mLooperOfDataAggregator; + private FakePermissionEnforcer mPermissionEnforcer; @SuppressLint("VisibleForTests") @Before public void setUp() { mContext = spy(ApplicationProvider.getApplicationContext()); + mPermissionEnforcer = new FakePermissionEnforcer(); + mPermissionEnforcer.grant(READ_FORENSIC_STATE); + mPermissionEnforcer.grant(MANAGE_FORENSIC_STATE); + mTestLooper = new TestLooper(); mLooper = mTestLooper.getLooper(); mTestLooperOfDataAggregator = new TestLooper(); @@ -87,217 +96,101 @@ public class ForensicServiceTest { } @Test - public void testMonitorState_Invisible() throws RemoteException { + public void testAddStateCallback_NoPermission() { + mPermissionEnforcer.revoke(READ_FORENSIC_STATE); StateCallback scb = new StateCallback(); assertEquals(STATE_UNKNOWN, scb.mState); - mForensicService.getBinderService().monitorState(scb); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb.mState); - } - - @Test - public void testMonitorState_Invisible_TwoMonitors() throws RemoteException { - StateCallback scb1 = new StateCallback(); - assertEquals(STATE_UNKNOWN, scb1.mState); - mForensicService.getBinderService().monitorState(scb1); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - - StateCallback scb2 = new StateCallback(); - assertEquals(STATE_UNKNOWN, scb2.mState); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb2.mState); + assertThrows(SecurityException.class, + () -> mForensicService.getBinderService().addStateCallback(scb)); } @Test - public void testMakeVisible_FromInvisible() throws RemoteException { + public void testRemoveStateCallback_NoPermission() { + mPermissionEnforcer.revoke(READ_FORENSIC_STATE); StateCallback scb = new StateCallback(); assertEquals(STATE_UNKNOWN, scb.mState); - mForensicService.getBinderService().monitorState(scb); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb.mState); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeVisible(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb.mState); - assertNull(ccb.mErrorCode); + assertThrows(SecurityException.class, + () -> mForensicService.getBinderService().removeStateCallback(scb)); } @Test - public void testMakeVisible_FromInvisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_INVISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - - doReturn(true).when(mDataAggregator).initialize(); + public void testEnable_NoPermission() { + mPermissionEnforcer.revoke(MANAGE_FORENSIC_STATE); CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeVisible(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - assertNull(ccb.mErrorCode); + assertThrows(SecurityException.class, + () -> mForensicService.getBinderService().enable(ccb)); } @Test - public void testMakeVisible_FromInvisible_TwoMonitors_DataSourceUnavailable() - throws RemoteException { - mForensicService.setState(STATE_INVISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - - doReturn(false).when(mDataAggregator).initialize(); + public void testDisable_NoPermission() { + mPermissionEnforcer.revoke(MANAGE_FORENSIC_STATE); CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeVisible(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - assertNotNull(ccb.mErrorCode); - assertEquals(ERROR_DATA_SOURCE_UNAVAILABLE, ccb.mErrorCode.intValue()); + assertThrows(SecurityException.class, + () -> mForensicService.getBinderService().disable(ccb)); } @Test - public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_VISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeVisible(ccb); + public void testAddStateCallback_Disabled() throws RemoteException { + StateCallback scb = new StateCallback(); + assertEquals(STATE_UNKNOWN, scb.mState); + mForensicService.getBinderService().addStateCallback(scb); mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - assertNull(ccb.mErrorCode); + assertEquals(STATE_DISABLED, scb.mState); } @Test - public void testMakeVisible_FromEnabled_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_ENABLED); + public void testAddStateCallback_Disabled_TwoStateCallbacks() throws RemoteException { StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_ENABLED, scb1.mState); - assertEquals(STATE_ENABLED, scb2.mState); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeVisible(ccb); + assertEquals(STATE_UNKNOWN, scb1.mState); + mForensicService.getBinderService().addStateCallback(scb1); mTestLooper.dispatchAll(); - assertEquals(STATE_ENABLED, scb1.mState); - assertEquals(STATE_ENABLED, scb2.mState); - assertNotNull(ccb.mErrorCode); - assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue()); - } + assertEquals(STATE_DISABLED, scb1.mState); - @Test - public void testMakeInvisible_FromInvisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_INVISIBLE); - StateCallback scb1 = new StateCallback(); StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeInvisible(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - assertNull(ccb.mErrorCode); - } - - @Test - public void testMakeInvisible_FromVisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_VISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeInvisible(ccb); + assertEquals(STATE_UNKNOWN, scb2.mState); + mForensicService.getBinderService().addStateCallback(scb2); mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - assertNull(ccb.mErrorCode); + assertEquals(STATE_DISABLED, scb2.mState); } @Test - public void testMakeInvisible_FromEnabled_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_ENABLED); + public void testRemoveStateCallback() throws RemoteException { + mForensicService.setState(STATE_DISABLED); StateCallback scb1 = new StateCallback(); StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); + mForensicService.getBinderService().addStateCallback(scb1); + mForensicService.getBinderService().addStateCallback(scb2); mTestLooper.dispatchAll(); - assertEquals(STATE_ENABLED, scb1.mState); - assertEquals(STATE_ENABLED, scb2.mState); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().makeInvisible(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - assertNull(ccb.mErrorCode); - } + doReturn(true).when(mDataAggregator).initialize(); + doReturn(true).when(mForensicEventTransportConnection).initialize(); - - @Test - public void testEnable_FromInvisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_INVISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); + mForensicService.getBinderService().removeStateCallback(scb2); CommandCallback ccb = new CommandCallback(); mForensicService.getBinderService().enable(ccb); mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - assertNotNull(ccb.mErrorCode); - assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue()); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); + assertNull(ccb.mErrorCode); } @Test - public void testEnable_FromVisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_VISIBLE); + public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException { + mForensicService.setState(STATE_DISABLED); StateCallback scb1 = new StateCallback(); StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); + mForensicService.getBinderService().addStateCallback(scb1); + mForensicService.getBinderService().addStateCallback(scb2); mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); - doReturn(true).when(mBackupTransportConnection).initialize(); + doReturn(true).when(mForensicEventTransportConnection).initialize(); CommandCallback ccb = new CommandCallback(); mForensicService.getBinderService().enable(ccb); @@ -310,35 +203,13 @@ public class ForensicServiceTest { } @Test - public void testEnable_FromVisible_TwoMonitors_BackupTransportUnavailable() + public void testEnable_FromEnabled_TwoStateCallbacks() throws RemoteException { - mForensicService.setState(STATE_VISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - - doReturn(false).when(mBackupTransportConnection).initialize(); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().enable(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - assertNotNull(ccb.mErrorCode); - assertEquals(ERROR_BACKUP_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue()); - } - - @Test - public void testEnable_FromEnabled_TwoMonitors() throws RemoteException { mForensicService.setState(STATE_ENABLED); StateCallback scb1 = new StateCallback(); StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); + mForensicService.getBinderService().addStateCallback(scb1); + mForensicService.getBinderService().addStateCallback(scb2); mTestLooper.dispatchAll(); assertEquals(STATE_ENABLED, scb1.mState); assertEquals(STATE_ENABLED, scb2.mState); @@ -346,62 +217,44 @@ public class ForensicServiceTest { CommandCallback ccb = new CommandCallback(); mForensicService.getBinderService().enable(ccb); mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); assertEquals(STATE_ENABLED, scb2.mState); assertNull(ccb.mErrorCode); } @Test - public void testDisable_FromInvisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_INVISIBLE); + public void testDisable_FromDisabled_TwoStateCallbacks() throws RemoteException { + mForensicService.setState(STATE_DISABLED); StateCallback scb1 = new StateCallback(); StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); + mForensicService.getBinderService().addStateCallback(scb1); + mForensicService.getBinderService().addStateCallback(scb2); mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); CommandCallback ccb = new CommandCallback(); mForensicService.getBinderService().disable(ccb); mTestLooper.dispatchAll(); - assertEquals(STATE_INVISIBLE, scb1.mState); - assertEquals(STATE_INVISIBLE, scb2.mState); - assertNotNull(ccb.mErrorCode); - assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue()); - } - @Test - public void testDisable_FromVisible_TwoMonitors() throws RemoteException { - mForensicService.setState(STATE_VISIBLE); - StateCallback scb1 = new StateCallback(); - StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); - - CommandCallback ccb = new CommandCallback(); - mForensicService.getBinderService().disable(ccb); - mTestLooper.dispatchAll(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); assertNull(ccb.mErrorCode); } @Test - public void testDisable_FromEnabled_TwoMonitors() throws RemoteException { + public void testDisable_FromEnabled_TwoStateCallbacks() throws RemoteException { mForensicService.setState(STATE_ENABLED); StateCallback scb1 = new StateCallback(); StateCallback scb2 = new StateCallback(); - mForensicService.getBinderService().monitorState(scb1); - mForensicService.getBinderService().monitorState(scb2); + mForensicService.getBinderService().addStateCallback(scb1); + mForensicService.getBinderService().addStateCallback(scb2); mTestLooper.dispatchAll(); assertEquals(STATE_ENABLED, scb1.mState); assertEquals(STATE_ENABLED, scb2.mState); - doNothing().when(mBackupTransportConnection).release(); + doNothing().when(mForensicEventTransportConnection).release(); ServiceThread mockThread = spy(ServiceThread.class); mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread); @@ -412,11 +265,35 @@ public class ForensicServiceTest { mTestLooperOfDataAggregator.dispatchAll(); // TODO: We can verify the data sources once we implement them. verify(mockThread, times(1)).quitSafely(); - assertEquals(STATE_VISIBLE, scb1.mState); - assertEquals(STATE_VISIBLE, scb2.mState); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); assertNull(ccb.mErrorCode); } + @Ignore("Enable once the ForensicEventTransportConnection is ready") + @Test + public void testEnable_FromDisable_TwoStateCallbacks_TransportUnavailable() + throws RemoteException { + mForensicService.setState(STATE_DISABLED); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().addStateCallback(scb1); + mForensicService.getBinderService().addStateCallback(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); + + doReturn(false).when(mForensicEventTransportConnection).initialize(); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().enable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_DISABLED, scb1.mState); + assertEquals(STATE_DISABLED, scb2.mState); + assertNotNull(ccb.mErrorCode); + assertEquals(ERROR_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue()); + } + @Test public void testDataAggregator_AddBatchData() { mForensicService.setState(STATE_ENABLED); @@ -441,14 +318,14 @@ public class ForensicServiceTest { events.add(eventOne); events.add(eventTwo); - doReturn(true).when(mBackupTransportConnection).addData(any()); + doReturn(true).when(mForensicEventTransportConnection).addData(any()); mDataAggregator.addBatchData(events); mTestLooperOfDataAggregator.dispatchAll(); mTestLooper.dispatchAll(); ArgumentCaptor<List<ForensicEvent>> captor = ArgumentCaptor.forClass(List.class); - verify(mBackupTransportConnection).addData(captor.capture()); + verify(mForensicEventTransportConnection).addData(captor.capture()); List<ForensicEvent> receivedEvents = captor.getValue(); assertEquals(receivedEvents.size(), 2); @@ -476,6 +353,10 @@ public class ForensicServiceTest { return mContext; } + @Override + public PermissionEnforcer getPermissionEnforcer() { + return mPermissionEnforcer; + } @Override public Looper getLooper() { @@ -483,9 +364,9 @@ public class ForensicServiceTest { } @Override - public BackupTransportConnection getBackupTransportConnection() { - mBackupTransportConnection = spy(new BackupTransportConnection(mContext)); - return mBackupTransportConnection; + public ForensicEventTransportConnection getForensicEventransportConnection() { + mForensicEventTransportConnection = spy(new ForensicEventTransportConnection(mContext)); + return mForensicEventTransportConnection; } @Override diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index f1656678d3bd..0c058df35195 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -97,7 +97,7 @@ android_test { "com_android_server_accessibility_flags_lib", "locksettings_flags_lib", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { - "true": ["service-crashrecovery.impl"], + "true": ["service-crashrecovery-pre-jarjar"], default: [], }), diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index 698bda335f83..4c381eb4429e 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -24,6 +24,7 @@ import android.app.PendingIntent; import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.DevicePolicyManagerLiteInternal; import android.app.backup.IBackupManager; +import android.app.supervision.SupervisionManagerInternal; import android.app.usage.UsageStatsManagerInternal; import android.content.Context; import android.content.Intent; @@ -488,6 +489,11 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi public Context createContextAsUser(UserHandle user) { return context; } + + @Override + SupervisionManagerInternal getSupervisionManager() { + return services.supervisionManagerInternal; + } } static class TransferOwnershipMetadataManagerMockInjector extends diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index cb4269a205e4..cf5dc4bec71c 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -109,6 +109,7 @@ import android.app.admin.PasswordMetrics; import android.app.admin.PreferentialNetworkServiceConfig; import android.app.admin.SystemUpdatePolicy; import android.app.admin.WifiSsidPolicy; +import android.app.admin.flags.Flags; import android.app.role.RoleManager; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -134,6 +135,10 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DeviceConfig; import android.provider.Settings; import android.security.KeyChain; @@ -165,6 +170,7 @@ import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; import org.mockito.internal.util.collections.Sets; @@ -207,6 +213,9 @@ public class DevicePolicyManagerTest extends DpmTestBase { public static final String INVALID_CALLING_IDENTITY_MSG = "Calling identity is not authorized"; public static final String ONGOING_CALL_MSG = "ongoing call on the device"; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + // TODO replace all instances of this with explicit {@link #mServiceContext}. @Deprecated private DpmMockContext mContext; @@ -4425,6 +4434,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @Ignore("b/359188869") public void testSetAutoTimeEnabledWithPOOfOrganizationOwnedDevice() throws Exception { setupProfileOwner(); configureProfileOwnerOfOrgOwnedDevice(admin1, CALLER_USER_HANDLE); @@ -4902,6 +4912,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED) public void testSecondaryLockscreen_profileOwner() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_UID; @@ -4930,6 +4941,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED) public void testSecondaryLockscreen_deviceOwner() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; @@ -4948,6 +4960,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED) public void testSecondaryLockscreen_nonOwner() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_UID; @@ -4964,6 +4977,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED) public void testSecondaryLockscreen_nonSupervisionApp() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_UID; @@ -4996,6 +5010,51 @@ public class DevicePolicyManagerTest extends DpmTestBase { } @Test + @RequiresFlagsEnabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED) + public void testIsSecondaryLockscreenEnabled() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + + verifyIsSecondaryLockscreenEnabled(false); + verifyIsSecondaryLockscreenEnabled(true); + } + + private void verifyIsSecondaryLockscreenEnabled(boolean expected) throws Exception { + reset(getServices().supervisionManagerInternal); + + doReturn(expected).when(getServices().supervisionManagerInternal) + .isSupervisionLockscreenEnabledForUser(anyInt()); + + final boolean enabled = dpm.isSecondaryLockscreenEnabled(UserHandle.of(CALLER_USER_HANDLE)); + verify(getServices().supervisionManagerInternal) + .isSupervisionLockscreenEnabledForUser(CALLER_USER_HANDLE); + + assertThat(enabled).isEqualTo(expected); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED) + public void testSetSecondaryLockscreenEnabled() throws Exception { + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + + verifySetSecondaryLockscreenEnabled(false); + verifySetSecondaryLockscreenEnabled(true); + } + + private void verifySetSecondaryLockscreenEnabled(boolean enabled) throws Exception { + reset(getServices().supervisionManagerInternal); + + dpm.setSecondaryLockscreenEnabled(admin1, enabled); + verify(getServices().supervisionManagerInternal).setSupervisionLockscreenEnabledForUser( + CALLER_USER_HANDLE, enabled, null); + + reset(getServices().supervisionManagerInternal); + + dpm.setSecondaryLockscreenEnabled(enabled, new PersistableBundle()); + verify(getServices().supervisionManagerInternal).setSupervisionLockscreenEnabledForUser( + eq(CALLER_USER_HANDLE), eq(enabled), any(PersistableBundle.class)); + } + + @Test public void testIsDeviceManaged() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 2e200a9268f5..3e4448c1dafa 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -35,6 +35,7 @@ import android.app.NotificationManager; import android.app.admin.DevicePolicyManager; import android.app.backup.IBackupManager; import android.app.role.RoleManager; +import android.app.supervision.SupervisionManagerInternal; import android.app.usage.UsageStatsManagerInternal; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -77,8 +78,8 @@ import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockSettingsInternal; import com.android.server.AlarmManagerInternal; -import com.android.server.pdb.PersistentDataBlockManagerInternal; import com.android.server.net.NetworkPolicyManagerInternal; +import com.android.server.pdb.PersistentDataBlockManagerInternal; import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.UserManagerInternal; import com.android.server.pm.pkg.PackageState; @@ -149,6 +150,7 @@ public class MockSystemServices { public final BuildMock buildMock = new BuildMock(); public final File dataDir; public final PolicyPathProvider pathProvider; + public final SupervisionManagerInternal supervisionManagerInternal; private final Map<String, PackageState> mTestPackageStates = new ArrayMap<>(); @@ -203,6 +205,7 @@ public class MockSystemServices { roleManager = realContext.getSystemService(RoleManager.class); roleManagerForMock = mock(RoleManagerForMock.class); subscriptionManager = mock(SubscriptionManager.class); + supervisionManagerInternal = mock(SupervisionManagerInternal.class); // Package manager is huge, so we use a partial mock instead. packageManager = spy(realContext.getPackageManager()); diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt index 79b0623640f6..8290e1cfb9db 100644 --- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt +++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt @@ -20,7 +20,7 @@ import android.app.admin.DevicePolicyManagerInternal import android.content.ComponentName import android.content.Context import android.content.pm.UserInfo -import android.os.Bundle +import android.os.PersistableBundle import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -139,7 +139,7 @@ class SupervisionServiceTest { assertThat(userData.supervisionLockScreenEnabled).isFalse() assertThat(userData.supervisionLockScreenOptions).isNull() - service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle()) + service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, PersistableBundle()) userData = service.getUserDataLocked(USER_ID) assertThat(userData.supervisionLockScreenEnabled).isTrue() assertThat(userData.supervisionLockScreenOptions).isNotNull() 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 cc0286508cdc..6af65423415b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -84,7 +84,9 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.server.UiServiceTestCase; import com.android.server.notification.GroupHelper.CachedSummary; +import com.android.server.notification.GroupHelper.FullyQualifiedGroupKey; import com.android.server.notification.GroupHelper.NotificationAttributes; +import com.android.server.notification.GroupHelper.NotificationSectioner; import org.junit.Before; import org.junit.Rule; @@ -2298,6 +2300,7 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + final int numNotifications = 2 * AUTOGROUP_AT_COUNT; int numNotificationChannel1 = 0; final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1", "TEST_CHANNEL_ID1", IMPORTANCE_DEFAULT); @@ -2307,7 +2310,7 @@ public class GroupHelperTest extends UiServiceTestCase { final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); // Post notifications with different channels that autogroup within the same section NotificationRecord r; - for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { + for (int i = 0; i < numNotifications; i++) { if (i % 2 == 0) { r = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, false, channel1); @@ -2324,12 +2327,12 @@ public class GroupHelperTest extends UiServiceTestCase { "TEST_CHANNEL_ID1"); verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), eq(expectedGroupKey_alerting), anyInt(), eq(expectedSummaryAttr)); - verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + verify(mCallback, times(numNotifications)).addAutoGroup(anyString(), eq(expectedGroupKey_alerting), eq(true)); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), - any()); + verify(mCallback, times(numNotifications - AUTOGROUP_AT_COUNT)).updateAutogroupSummary( + anyInt(), anyString(), anyString(), any()); Mockito.reset(mCallback); // Update channel1's importance @@ -2375,7 +2378,7 @@ public class GroupHelperTest extends UiServiceTestCase { final List<NotificationRecord> notificationList = new ArrayList<>(); final String pkg = "package"; final int summaryId = 0; - final int numChildNotif = 4; + final int numChildNotif = 2 * AUTOGROUP_AT_COUNT; // Create an app-provided group: summary + child notifications final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1", @@ -2435,8 +2438,8 @@ public class GroupHelperTest extends UiServiceTestCase { eq(expectedGroupKey_social), eq(true)); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); - verify(mCallback, times(numChildNotif / 2)).updateAutogroupSummary(anyInt(), anyString(), - anyString(), any()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); verify(mCallback, times(numChildNotif)).removeAppProvidedSummaryOnClassification( anyString(), eq(originalAppGroupKey)); } @@ -2513,9 +2516,10 @@ public class GroupHelperTest extends UiServiceTestCase { final List<NotificationRecord> notificationList = new ArrayList<>(); final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); final String pkg = "package"; + final int numChildNotifications = AUTOGROUP_AT_COUNT; // Post singleton groups, above forced group limit - for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) { + for (int i = 0; i < numChildNotifications; i++) { NotificationRecord summary = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, true); notificationList.add(summary); @@ -2545,13 +2549,13 @@ public class GroupHelperTest extends UiServiceTestCase { // Check that notifications are forced grouped and app-provided summaries are canceled verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); - verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(), + verify(mCallback, times(numChildNotifications)).addAutoGroup(anyString(), eq(expectedGroupKey_social), eq(true)); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); - verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), any()); - verify(mCallback, times(2)).removeAppProvidedSummaryOnClassification( + verify(mCallback, times(numChildNotifications)).removeAppProvidedSummaryOnClassification( anyString(), anyString()); // Adjust group key and cancel summaries @@ -2593,14 +2597,16 @@ public class GroupHelperTest extends UiServiceTestCase { AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); String expectedTriggeringKey = null; // Post singleton groups, above forced group limit - for (int i = 0; i < AUTOGROUP_SINGLETONS_AT_COUNT; i++) { + for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) { NotificationRecord summary = getNotificationRecord(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "testGrp " + i, true); notificationList.add(summary); NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), UserHandle.SYSTEM, "testGrp " + i, false); notificationList.add(child); - expectedTriggeringKey = child.getKey(); + if (i == AUTOGROUP_SINGLETONS_AT_COUNT - 1) { + expectedTriggeringKey = child.getKey(); + } summaryByGroup.put(summary.getGroupKey(), summary); mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); summary.isCanceled = true; // simulate removing the app summary @@ -2611,14 +2617,8 @@ public class GroupHelperTest extends UiServiceTestCase { verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), eq(expectedTriggeringKey), eq(expectedGroupKey_alerting), anyInt(), eq(getNotificationAttributes(BASE_FLAGS))); - verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(), - eq(expectedGroupKey_alerting), eq(true)); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); - verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), - any()); - verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).removeAppProvidedSummary( - anyString()); assertThat(mGroupHelper.findCanceledSummary(pkg, String.valueOf(0), 0, UserHandle.SYSTEM.getIdentifier())).isNotNull(); assertThat(mGroupHelper.findCanceledSummary(pkg, String.valueOf(1), 1, @@ -2645,12 +2645,12 @@ public class GroupHelperTest extends UiServiceTestCase { // Check that all notifications are moved to the social section group verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); - verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).addAutoGroup(anyString(), + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), eq(expectedGroupKey_social), eq(true)); // Check that the alerting section group is removed verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), eq(expectedGroupKey_alerting)); - verify(mCallback, times(AUTOGROUP_SINGLETONS_AT_COUNT)).updateAutogroupSummary(anyInt(), + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).updateAutogroupSummary(anyInt(), anyString(), anyString(), any()); } @@ -2666,7 +2666,7 @@ public class GroupHelperTest extends UiServiceTestCase { final String pkg = "package"; final int summaryId = 0; - final int numChildren = 3; + final int numChildren = AUTOGROUP_AT_COUNT; // Post a regular/valid group: summary + notifications NotificationRecord summary = getNotificationRecord(pkg, summaryId, String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); @@ -2706,13 +2706,211 @@ public class GroupHelperTest extends UiServiceTestCase { eq(true)); verify(mCallback, never()).removeAutoGroup(anyString()); verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); - verify(mCallback, times(numChildren - 1)).updateAutogroupSummary(anyInt(), anyString(), - anyString(), any()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); verify(mCallback, times(numChildren)).removeAppProvidedSummaryOnClassification(anyString(), anyString()); } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, + FLAG_NOTIFICATION_CLASSIFICATION}) + public void testUnbundleNotification_originalSummaryMissing_autogroupInNewSection() { + // Check that unbundled notifications are moved to the original section and aggregated + // with existing autogrouped notifications + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + final int summaryId = 0; + final int numChildren = AUTOGROUP_AT_COUNT - 1; + // Post a regular/valid group: summary + notifications (one less than autogroup limit) + NotificationRecord summary = getNotificationRecord(pkg, summaryId, + String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String originalAppGroupKey = summary.getGroupKey(); + final NotificationChannel originalChannel = summary.getChannel(); + for (int i = 0; i < numChildren; i++) { + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + } + + // Classify/bundle all child notifications: original group & summary is removed + final NotificationChannel socialChannel = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_DEFAULT); + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { + record.updateNotificationChannel(socialChannel); + mGroupHelper.onChannelUpdated(record); + } + } + + // Check that no autogroup summaries were created for the social section + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + verify(mCallback, times(numChildren)).removeAppProvidedSummaryOnClassification( + anyString(), eq(originalAppGroupKey)); + + // Cancel summary + summary.isCanceled = true; + summaryByGroup.clear(); + notificationList.remove(summary); + + // Add 1 ungrouped notification in the original section + NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242, + String.valueOf(4242), UserHandle.SYSTEM); + notificationList.add(ungroupedNotification); + mGroupHelper.onNotificationPosted(ungroupedNotification, false); + + // Unbundle the bundled notifications => notifications are moved back to the original group + // and an aggregate group is created because autogroup limit is reached + reset(mCallback); + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.updateNotificationChannel(originalChannel); + mGroupHelper.onNotificationUnbundled(record, false); + } + } + + // Check that a new aggregate group is created + final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier()); + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_alerting), anyInt(), any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_alerting), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, times(numChildren)).removeAutoGroupSummary(anyInt(), anyString(), + anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, + FLAG_NOTIFICATION_CLASSIFICATION}) + public void testUnbundleNotification_originalSummaryExists() { + // Check that unbundled notifications are moved to the original section and original group + // when the original summary is still present + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + final int summaryId = 0; + final int numChildren = AUTOGROUP_AT_COUNT + 1; + // Post a regular/valid group: summary + notifications + NotificationRecord summary = getNotificationRecord(pkg, summaryId, + String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String originalAppGroupKey = summary.getGroupKey(); + final NotificationChannel originalChannel = summary.getChannel(); + for (int i = 0; i < numChildren; i++) { + NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42), + UserHandle.SYSTEM, "testGrp", false); + notificationList.add(child); + mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup); + } + + // Classify/bundle child notifications: all except one, to keep the original group + final NotificationChannel socialChannel = new NotificationChannel( + NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID, + IMPORTANCE_DEFAULT); + final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg, + AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier()); + final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes( + BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT, + NotificationChannel.SOCIAL_MEDIA_ID); + int numChildrenBundled = 0; + for (NotificationRecord record: notificationList) { + if (record.getOriginalGroupKey().contains("testGrp") + && record.getNotification().isGroupChild()) { + record.updateNotificationChannel(socialChannel); + mGroupHelper.onChannelUpdated(record); + numChildrenBundled++; + if (numChildrenBundled == AUTOGROUP_AT_COUNT) { + break; + } + } + } + + // Check that 1 autogroup summaries were created for the social section + verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(), + eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social)); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(), + eq(expectedGroupKey_social), eq(true)); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString()); + verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(), + any()); + verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification( + anyString(), eq(originalAppGroupKey)); + + // Adjust group key and cancel summaries + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupSummary()) { + record.isCanceled = true; + } else if (record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.setOverrideGroupKey(expectedGroupKey_social); + } + } + + // Add 1 ungrouped notification in the original section + NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242, + String.valueOf(4242), UserHandle.SYSTEM); + notificationList.add(ungroupedNotification); + mGroupHelper.onNotificationPosted(ungroupedNotification, false); + + // Unbundle the bundled notifications => social section summary is destroyed + // and notifications are moved back to the original group + reset(mCallback); + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp") + && NotificationChannel.SYSTEM_RESERVED_IDS.contains( + record.getChannel().getId())) { + record.updateNotificationChannel(originalChannel); + mGroupHelper.onNotificationUnbundled(record, true); + } + } + + // Check that the autogroup summary for the social section was removed + // and that no new autogroup summaries were created + verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any()); + verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean()); + verify(mCallback, never()).removeAutoGroup(anyString()); + verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_social)); + verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).updateAutogroupSummary(anyInt(), eq(pkg), + eq(expectedGroupKey_social), any()); + + for (NotificationRecord record: notificationList) { + if (record.getNotification().isGroupChild() + && record.getOriginalGroupKey().contains("testGrp")) { + assertThat(record.getSbn().getOverrideGroupKey()).isNull(); + } + } + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testMoveAggregateGroups_updateChannel_groupsUngrouped() { final String pkg = "package"; @@ -3059,6 +3257,120 @@ public class GroupHelperTest extends UiServiceTestCase { @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS) + public void testNonGroupableChildren_singletonGroups_disableConversations() { + // Check that singleton groups with children that are not groupable, is not grouped + // Even though the group summary is a regular (alerting) notification, the children are + // conversations => the group should not be forced grouped. + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + // Trigger notification, ungrouped + final int triggerId = 1; + NotificationRecord triggerNotification = getNotificationRecord(pkg, triggerId, + String.valueOf(triggerId), UserHandle.SYSTEM); + notificationList.add(triggerNotification); + final NotificationSectioner triggerSection = GroupHelper.getSection(triggerNotification); + final FullyQualifiedGroupKey triggerFullAggregateGroupKey = new FullyQualifiedGroupKey( + triggerNotification.getUserId(), triggerNotification.getSbn().getPackageName(), + triggerSection); + + // Add singleton group with alerting child + final String groupName_valid = "testGrp_valid"; + final int summaryId_valid = 0; + NotificationRecord summary = getNotificationRecord(pkg, summaryId_valid, + String.valueOf(summaryId_valid), UserHandle.SYSTEM, groupName_valid, true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String groupKey_valid = summary.getGroupKey(); + NotificationRecord child = getNotificationRecord(pkg, summaryId_valid + 42, + String.valueOf(summaryId_valid + 42), UserHandle.SYSTEM, groupName_valid, false); + notificationList.add(child); + + // Add singleton group with conversation child + final String groupName_invalid = "testGrp_invalid"; + final int summaryId_invalid = 100; + summary = getNotificationRecord(pkg, summaryId_invalid, + String.valueOf(summaryId_invalid), UserHandle.SYSTEM, groupName_invalid, true); + notificationList.add(summary); + final String groupKey_invalid = summary.getGroupKey(); + summaryByGroup.put(summary.getGroupKey(), summary); + child = getNotificationRecord(pkg, summaryId_invalid + 42, + String.valueOf(summaryId_invalid + 42), UserHandle.SYSTEM, groupName_invalid, + false); + child = spy(child); + when(child.isConversation()).thenReturn(true); + notificationList.add(child); + + // Check that the invalid group will not be force grouped + final ArrayMap<String, NotificationRecord> sparseGroups = mGroupHelper.getSparseGroups( + triggerFullAggregateGroupKey, notificationList, summaryByGroup, triggerSection); + assertThat(sparseGroups).containsKey(groupKey_valid); + assertThat(sparseGroups).doesNotContainKey(groupKey_invalid); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS}) + public void testNonGroupableChildren_singletonGroups_enableConversations() { + // Check that singleton groups with children that are not groupable, is not grouped + // Conversations are groupable (FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS is enabled) + // The invalid group is the alerting notifications: because the triggering notifications' + // section is Conversations, so the alerting group should be skipped. + final List<NotificationRecord> notificationList = new ArrayList<>(); + final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>(); + final String pkg = "package"; + + // Trigger notification, ungrouped conversation + final int triggerId = 1; + NotificationRecord triggerNotification = getNotificationRecord(pkg, triggerId, + String.valueOf(triggerId), UserHandle.SYSTEM); + triggerNotification = spy(triggerNotification); + when(triggerNotification.isConversation()).thenReturn(true); + notificationList.add(triggerNotification); + final NotificationSectioner triggerSection = GroupHelper.getSection(triggerNotification); + final FullyQualifiedGroupKey triggerFullAggregateGroupKey = new FullyQualifiedGroupKey( + triggerNotification.getUserId(), triggerNotification.getSbn().getPackageName(), + triggerSection); + + // Add singleton group with conversation child + final String groupName_valid = "testGrp_valid"; + final int summaryId_valid = 0; + NotificationRecord summary = getNotificationRecord(pkg, summaryId_valid, + String.valueOf(summaryId_valid), UserHandle.SYSTEM, groupName_valid, true); + summary = spy(summary); + when(summary.isConversation()).thenReturn(true); + notificationList.add(summary); + summaryByGroup.put(summary.getGroupKey(), summary); + final String groupKey_valid = summary.getGroupKey(); + NotificationRecord child = getNotificationRecord(pkg, summaryId_valid + 42, + String.valueOf(summaryId_valid + 42), UserHandle.SYSTEM, groupName_valid, false); + child = spy(child); + when(child.isConversation()).thenReturn(true); + notificationList.add(child); + + // Add singleton group with non-conversation child + final String groupName_invalid = "testGrp_invalid"; + final int summaryId_invalid = 100; + summary = getNotificationRecord(pkg, summaryId_invalid, + String.valueOf(summaryId_invalid), UserHandle.SYSTEM, groupName_invalid, true); + notificationList.add(summary); + final String groupKey_invalid = summary.getGroupKey(); + summaryByGroup.put(summary.getGroupKey(), summary); + child = getNotificationRecord(pkg, summaryId_invalid + 42, + String.valueOf(summaryId_invalid + 42), UserHandle.SYSTEM, groupName_invalid, + false); + notificationList.add(child); + + // Check that the invalid group will not be force grouped + final ArrayMap<String, NotificationRecord> sparseGroups = mGroupHelper.getSparseGroups( + triggerFullAggregateGroupKey, notificationList, summaryByGroup, triggerSection); + assertThat(sparseGroups).containsKey(groupKey_valid); + assertThat(sparseGroups).doesNotContainKey(groupKey_invalid); + } + + @Test + @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) + @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS) public void testNonGroupableNotifications() { // Check that there is no valid section for: conversations, calls, foreground services NotificationRecord notification_conversation = mock(NotificationRecord.class); 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 6eb2f718a0e9..9eddcc94e650 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java @@ -720,6 +720,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase { } @Test + @EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION) public void testDefaultAllowedKeyAdjustments_readWriteXml() throws Exception { mAssistants.loadDefaultsFromConfig(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 42e31de295d6..817c368745d1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -82,6 +82,7 @@ import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.ArrayMap; @@ -122,6 +123,8 @@ import java.util.function.BiConsumer; * Build/Install/Run: * atest WmTests:WindowOrganizerTests */ + +// TODO revert parts of this set to set the flag to test the behavior @SmallTest @Presubmit @RunWith(WindowTestRunner.class) @@ -1299,6 +1302,7 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2) public void testEnterPipParams() { final StubOrganizer o = new StubOrganizer(); mWm.mAtmService.mTaskOrganizerController.registerTaskOrganizer(o); @@ -1314,6 +1318,7 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2) public void testChangePipParams() { class ChangeSavingOrganizer extends StubOrganizer { RunningTaskInfo mChangedInfo; diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 129494517cd6..15c8b135d2c4 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -185,6 +185,7 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser private static final int MSG_INCREASE_SENDSTRING_COUNT = 21; private static final int MSG_UPDATE_USB_SPEED = 22; private static final int MSG_UPDATE_HAL_VERSION = 23; + private static final int MSG_USER_UNLOCKED_AFTER_BOOT = 24; // Delay for debouncing USB disconnects. // We often get rapid connect/disconnect events when enabling USB functions, @@ -414,6 +415,17 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser } }; + if (Flags.checkUserActionUnlocked()) { + BroadcastReceiver userUnlockedAfterBootReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + mHandler.sendEmptyMessage(MSG_USER_UNLOCKED_AFTER_BOOT); + } + }; + mContext.registerReceiver(userUnlockedAfterBootReceiver, + new IntentFilter(Intent.ACTION_USER_UNLOCKED)); + } + mContext.registerReceiver(portReceiver, new IntentFilter(UsbManager.ACTION_USB_PORT_CHANGED)); mContext.registerReceiver(chargingReceiver, @@ -474,6 +486,7 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser mHandler.sendEmptyMessage(MSG_SYSTEM_READY); } + // Same as ACTION_LOCKED_BOOT_COMPLETED. public void bootCompleted() { if (DEBUG) Slog.d(TAG, "boot completed"); mHandler.sendEmptyMessage(MSG_BOOT_COMPLETED); @@ -632,7 +645,7 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser protected int mUsbSpeed; protected int mCurrentGadgetHalVersion; protected boolean mPendingBootAccessoryHandshakeBroadcast; - + protected boolean mUserUnlockedAfterBoot; /** * The persistent property which stores whether adb is enabled or not. * May also contain vendor-specific default functions for testing purposes. @@ -837,6 +850,12 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser return !userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER); } + private void attachAccessory() { + mUsbDeviceManager.getCurrentSettings().accessoryAttached(mCurrentAccessory); + removeMessages(MSG_ACCESSORY_HANDSHAKE_TIMEOUT); + broadcastUsbAccessoryHandshake(); + } + private void updateCurrentAccessory() { // We are entering accessory mode if we have received a request from the host // and the request has not timed out yet. @@ -863,10 +882,13 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser Slog.d(TAG, "entering USB accessory mode: " + mCurrentAccessory); // defer accessoryAttached if system is not ready - if (mBootCompleted) { - mUsbDeviceManager.getCurrentSettings().accessoryAttached(mCurrentAccessory); - removeMessages(MSG_ACCESSORY_HANDSHAKE_TIMEOUT); - broadcastUsbAccessoryHandshake(); + if (!Flags.checkUserActionUnlocked() && mBootCompleted) { + attachAccessory(); + } + // Defer accessoryAttached till user unlocks after boot. + // When no pin pattern is set, ACTION_USER_UNLOCKED would fire anyways + if (Flags.checkUserActionUnlocked() && mUserUnlockedAfterBoot) { + attachAccessory(); } // else handle in boot completed } else { Slog.e(TAG, "nativeGetAccessoryStrings failed"); @@ -887,7 +909,10 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser setEnabledFunctions(UsbManager.FUNCTION_NONE, false, operationId); if (mCurrentAccessory != null) { - if (mBootCompleted) { + if (!Flags.checkUserActionUnlocked() && mBootCompleted) { + mPermissionManager.usbAccessoryRemoved(mCurrentAccessory); + } + if (Flags.checkUserActionUnlocked() && mUserUnlockedAfterBoot) { mPermissionManager.usbAccessoryRemoved(mCurrentAccessory); } mCurrentAccessory = null; @@ -1377,6 +1402,7 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser case MSG_BOOT_COMPLETED: operationId = sUsbOperationCount.incrementAndGet(); mBootCompleted = true; + if (DEBUG) Slog.d(TAG, "MSG_BOOT_COMPLETED"); finishBoot(operationId); break; case MSG_USER_SWITCHED: { @@ -1423,14 +1449,38 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser } case MSG_INCREASE_SENDSTRING_COUNT: { mSendStringCount = mSendStringCount + 1; + break; + } + case MSG_USER_UNLOCKED_AFTER_BOOT: { + if (DEBUG) Slog.d(TAG, "MSG_USER_UNLOCKED_AFTER_BOOT"); + if (mUserUnlockedAfterBoot) { + break; + } + mUserUnlockedAfterBoot = true; + if (mCurrentUsbFunctionsReceived && mUserUnlockedAfterBoot) { + attachAccessoryAfterBoot(); + } + break; } } } + private void attachAccessoryAfterBoot() { + if (mCurrentAccessory != null) { + Slog.i(TAG, "AccessoryAttached"); + mUsbDeviceManager.getCurrentSettings().accessoryAttached(mCurrentAccessory); + broadcastUsbAccessoryHandshake(); + } else if (mPendingBootAccessoryHandshakeBroadcast) { + broadcastUsbAccessoryHandshake(); + } + mPendingBootAccessoryHandshakeBroadcast = false; + } + public abstract void handlerInitDone(int operationId); protected void finishBoot(int operationId) { if (mBootCompleted && mCurrentUsbFunctionsReceived && mSystemReady) { + if (DEBUG) Slog.d(TAG, "finishBoot all flags true"); if (mPendingBootBroadcast) { updateUsbStateBroadcastIfNeeded(getAppliedFunctions(mCurrentFunctions)); mPendingBootBroadcast = false; @@ -1441,14 +1491,12 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser } else { setEnabledFunctions(UsbManager.FUNCTION_NONE, false, operationId); } - if (mCurrentAccessory != null) { - mUsbDeviceManager.getCurrentSettings().accessoryAttached(mCurrentAccessory); - broadcastUsbAccessoryHandshake(); - } else if (mPendingBootAccessoryHandshakeBroadcast) { - broadcastUsbAccessoryHandshake(); + if (!Flags.checkUserActionUnlocked()) { + attachAccessoryAfterBoot(); + } + if (Flags.checkUserActionUnlocked() && mUserUnlockedAfterBoot) { + attachAccessoryAfterBoot(); } - - mPendingBootAccessoryHandshakeBroadcast = false; updateUsbNotification(false); updateAdbNotification(false); updateUsbFunctions(); diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig index cd96d76a1c93..a2d0efd1d063 100644 --- a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig +++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig @@ -14,3 +14,10 @@ flag { description: "This flag enables binding to MtpService when in mtp/ptp modes" bug: "332256525" } + +flag { + name: "check_user_action_unlocked" + namespace: "usb" + description: "This flag checks if phone is unlocked after boot" + bug: "73654179" +} diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 9f5e6d18dc03..7e600b3a77d6 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -16,6 +16,7 @@ package com.android.server.wm.flicker.helpers +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.content.Context import android.graphics.Insets import android.graphics.Rect @@ -74,13 +75,28 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : .waitForAndVerify() } + /** Launch an app and ensure it's moved to Desktop if it has not. */ + fun enterDesktopMode( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH), + ) { + innerHelper.launchViaIntent(wmHelper) + if (!isInDesktopWindowingMode(wmHelper)) { + enterDesktopModeWithDrag( + wmHelper = wmHelper, + device = device, + motionEventHelper = motionEventHelper + ) + } + } + /** Move an app to Desktop by dragging the app handle at the top. */ - fun enterDesktopWithDrag( + fun enterDesktopModeWithDrag( wmHelper: WindowManagerStateHelper, device: UiDevice, motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH) ) { - innerHelper.launchViaIntent(wmHelper) dragToDesktop( wmHelper = wmHelper, device = device, @@ -415,6 +431,10 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : return metricInsets.getInsetsIgnoringVisibility(typeMask) } + // Requirement of DesktopWindowingMode is having a minimum of 1 app in WINDOWING_MODE_FREEFORM. + private fun isInDesktopWindowingMode(wmHelper: WindowManagerStateHelper) = + wmHelper.getWindow(innerHelper)?.windowingMode == WINDOWING_MODE_FREEFORM + private companion object { val TIMEOUT: Duration = Duration.ofSeconds(3) const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index 91483eb41387..8be74eaccd20 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -37,7 +37,7 @@ android_test { "truth", ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { "true": [ - "service-crashrecovery.impl", + "service-crashrecovery-pre-jarjar", "framework-crashrecovery.impl", ], default: [], diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 1bef5f8b17f6..1d4adc4a57d8 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -207,14 +207,13 @@ static ResourceTable::CollisionResult MergeConfigValue( Value* dst_value = dst_config_value->value.get(); Value* src_value = src_config_value->value.get(); - CollisionResult collision_result; - if (overlay) { - collision_result = - ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool); - } else { - collision_result = - ResourceTable::ResolveFlagCollision(dst_value->GetFlagStatus(), src_value->GetFlagStatus()); - if (collision_result == CollisionResult::kConflict) { + CollisionResult collision_result = + ResourceTable::ResolveFlagCollision(dst_value->GetFlagStatus(), src_value->GetFlagStatus()); + if (collision_result == CollisionResult::kConflict) { + if (overlay) { + collision_result = + ResolveMergeCollision(override_styles_instead_of_overlaying, dst_value, src_value, pool); + } else { collision_result = ResourceTable::ResolveValueCollision(dst_value, src_value); } } |