diff options
234 files changed, 7821 insertions, 1806 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 3e5f1cb0e1bd..fa1127843840 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -379,6 +379,7 @@ cc_aconfig_library { aconfig_declarations { name: "com.android.internal.os.flags-aconfig", package: "com.android.internal.os", + container: "system", srcs: ["core/java/com/android/internal/os/flags.aconfig"], } @@ -853,6 +854,7 @@ java_aconfig_library { aconfig_declarations { name: "com.android.server.contextualsearch.flags-aconfig", package: "com.android.server.contextualsearch.flags", + container: "system", srcs: ["services/contextualsearch/flags/flags.aconfig"], } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 443a6c0e91e6..5a3ff83b6861 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -263,6 +263,7 @@ package android.app { method @RequiresPermission("android.permission.MANAGE_APPOPS") public void setHistoryParameters(int, long, int); method @RequiresPermission(android.Manifest.permission.MANAGE_APP_OPS_MODES) public void setMode(int, int, String, int); method public static int strOpToOp(@NonNull String); + method public int unsafeCheckOpRawNoThrow(@NonNull String, @NonNull android.content.AttributionSource); field public static final int ATTRIBUTION_CHAIN_ID_NONE = -1; // 0xffffffff field public static final int ATTRIBUTION_FLAGS_NONE = 0; // 0x0 field public static final int ATTRIBUTION_FLAG_ACCESSOR = 1; // 0x1 diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index f4d1304c7f25..b3312a829298 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -8788,6 +8788,18 @@ public class AppOpsManager { * Does not throw a security exception, does not translate {@link #MODE_FOREGROUND}. * @hide */ + @TestApi + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + public int unsafeCheckOpRawNoThrow( + @NonNull String op, @NonNull AttributionSource attributionSource) { + return unsafeCheckOpRawNoThrow(strOpToOp(op), attributionSource); + } + + /** + * Returns the <em>raw</em> mode associated with the op. + * Does not throw a security exception, does not translate {@link #MODE_FOREGROUND}. + * @hide + */ public int unsafeCheckOpRawNoThrow(int op, int uid, @NonNull String packageName) { return unsafeCheckOpRawNoThrow(op, uid, packageName, Context.DEVICE_ID_DEFAULT); } @@ -8798,8 +8810,8 @@ public class AppOpsManager { if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) { return mService.checkOperationRaw(op, uid, packageName, null); } else { - return mService.checkOperationRawForDevice(op, uid, packageName, null, - Context.DEVICE_ID_DEFAULT); + return mService.checkOperationRawForDevice( + op, uid, packageName, null, virtualDeviceId); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index f7e0e9f6a416..714454b01d93 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -93,7 +93,10 @@ import android.text.style.AbsoluteSizeSpan; import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; import android.text.style.TextAppearanceSpan; +import android.text.style.UnderlineSpan; import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -765,11 +768,24 @@ public class Notification implements Parcelable MessagingStyle.class, CallStyle.class); /** @hide */ - @IntDef(flag = true, prefix = { "FLAG_" }, value = {FLAG_SHOW_LIGHTS, FLAG_ONGOING_EVENT, - FLAG_INSISTENT, FLAG_ONLY_ALERT_ONCE, - FLAG_AUTO_CANCEL, FLAG_NO_CLEAR, FLAG_FOREGROUND_SERVICE, FLAG_HIGH_PRIORITY, - FLAG_LOCAL_ONLY, FLAG_GROUP_SUMMARY, FLAG_AUTOGROUP_SUMMARY, FLAG_BUBBLE, - FLAG_USER_INITIATED_JOB}) + @IntDef(flag = true, prefix = {"FLAG_"}, value = { + FLAG_SHOW_LIGHTS, + FLAG_ONGOING_EVENT, + FLAG_INSISTENT, + FLAG_ONLY_ALERT_ONCE, + FLAG_AUTO_CANCEL, + FLAG_NO_CLEAR, + FLAG_FOREGROUND_SERVICE, + FLAG_HIGH_PRIORITY, + FLAG_LOCAL_ONLY, + FLAG_GROUP_SUMMARY, + FLAG_AUTOGROUP_SUMMARY, + FLAG_CAN_COLORIZE, + FLAG_BUBBLE, + FLAG_NO_DISMISS, + FLAG_FSI_REQUESTED_BUT_DENIED, + FLAG_USER_INITIATED_JOB + }) @Retention(RetentionPolicy.SOURCE) public @interface NotificationFlags{}; @@ -3154,9 +3170,6 @@ public class Notification implements Parcelable + " instance is a custom Parcelable and not allowed in Notification"); return cs.toString(); } - if (Flags.cleanUpSpansAndNewLines()) { - return stripStyling(cs); - } return removeTextSizeSpans(cs); } @@ -3779,10 +3792,10 @@ public class Notification implements Parcelable if (this.tickerText != null) { sb.append(" tick"); } - sb.append(" defaults=0x"); - sb.append(Integer.toHexString(this.defaults)); - sb.append(" flags=0x"); - sb.append(Integer.toHexString(this.flags)); + sb.append(" defaults="); + sb.append(defaultsToString(this.defaults)); + sb.append(" flags="); + sb.append(flagsToString(this.flags)); sb.append(String.format(" color=0x%08x", this.color)); if (this.category != null) { sb.append(" category="); @@ -3851,6 +3864,124 @@ public class Notification implements Parcelable } /** + * {@hide} + */ + public static String flagsToString(@NotificationFlags int flags) { + final List<String> flagStrings = new ArrayList<String>(); + if ((flags & FLAG_SHOW_LIGHTS) != 0) { + flagStrings.add("SHOW_LIGHTS"); + flags &= ~FLAG_SHOW_LIGHTS; + } + if ((flags & FLAG_ONGOING_EVENT) != 0) { + flagStrings.add("ONGOING_EVENT"); + flags &= ~FLAG_ONGOING_EVENT; + } + if ((flags & FLAG_INSISTENT) != 0) { + flagStrings.add("INSISTENT"); + flags &= ~FLAG_INSISTENT; + } + if ((flags & FLAG_ONLY_ALERT_ONCE) != 0) { + flagStrings.add("ONLY_ALERT_ONCE"); + flags &= ~FLAG_ONLY_ALERT_ONCE; + } + if ((flags & FLAG_AUTO_CANCEL) != 0) { + flagStrings.add("AUTO_CANCEL"); + flags &= ~FLAG_AUTO_CANCEL; + } + if ((flags & FLAG_NO_CLEAR) != 0) { + flagStrings.add("NO_CLEAR"); + flags &= ~FLAG_NO_CLEAR; + } + if ((flags & FLAG_FOREGROUND_SERVICE) != 0) { + flagStrings.add("FOREGROUND_SERVICE"); + flags &= ~FLAG_FOREGROUND_SERVICE; + } + if ((flags & FLAG_HIGH_PRIORITY) != 0) { + flagStrings.add("HIGH_PRIORITY"); + flags &= ~FLAG_HIGH_PRIORITY; + } + if ((flags & FLAG_LOCAL_ONLY) != 0) { + flagStrings.add("LOCAL_ONLY"); + flags &= ~FLAG_LOCAL_ONLY; + } + if ((flags & FLAG_GROUP_SUMMARY) != 0) { + flagStrings.add("GROUP_SUMMARY"); + flags &= ~FLAG_GROUP_SUMMARY; + } + if ((flags & FLAG_AUTOGROUP_SUMMARY) != 0) { + flagStrings.add("AUTOGROUP_SUMMARY"); + flags &= ~FLAG_AUTOGROUP_SUMMARY; + } + if ((flags & FLAG_CAN_COLORIZE) != 0) { + flagStrings.add("CAN_COLORIZE"); + flags &= ~FLAG_CAN_COLORIZE; + } + if ((flags & FLAG_BUBBLE) != 0) { + flagStrings.add("BUBBLE"); + flags &= ~FLAG_BUBBLE; + } + if ((flags & FLAG_NO_DISMISS) != 0) { + flagStrings.add("NO_DISMISS"); + flags &= ~FLAG_NO_DISMISS; + } + if ((flags & FLAG_FSI_REQUESTED_BUT_DENIED) != 0) { + flagStrings.add("FSI_REQUESTED_BUT_DENIED"); + flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED; + } + if ((flags & FLAG_USER_INITIATED_JOB) != 0) { + flagStrings.add("USER_INITIATED_JOB"); + flags &= ~FLAG_USER_INITIATED_JOB; + } + if (Flags.lifetimeExtensionRefactor()) { + if ((flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) != 0) { + flagStrings.add("LIFETIME_EXTENDED_BY_DIRECT_REPLY"); + flags &= ~FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; + } + } + + if (flagStrings.isEmpty()) { + return "0"; + } + + if (flags != 0) { + flagStrings.add(String.format("UNKNOWN(0x%08x)", flags)); + } + + return String.join("|", flagStrings); + } + + /** @hide */ + public static String defaultsToString(int defaults) { + final List<String> defaultStrings = new ArrayList<String>(); + if ((defaults & DEFAULT_ALL) == DEFAULT_ALL) { + defaultStrings.add("ALL"); + defaults &= ~DEFAULT_ALL; + } + if ((defaults & DEFAULT_SOUND) != 0) { + defaultStrings.add("SOUND"); + defaults &= ~DEFAULT_SOUND; + } + if ((defaults & DEFAULT_VIBRATE) != 0) { + defaultStrings.add("VIBRATE"); + defaults &= ~DEFAULT_VIBRATE; + } + if ((defaults & DEFAULT_LIGHTS) != 0) { + defaultStrings.add("LIGHTS"); + defaults &= ~DEFAULT_LIGHTS; + } + + if (defaultStrings.isEmpty()) { + return "0"; + } + + if (defaults != 0) { + defaultStrings.add(String.format("UNKNOWN(0x%08x)", defaults)); + } + + return String.join("|", defaultStrings); + } + + /** * @hide */ public boolean hasCompletedProgress() { @@ -8154,9 +8285,6 @@ public class Notification implements Parcelable */ public BigTextStyle bigText(CharSequence cs) { mBigText = safeCharSequence(cs); - if (Flags.cleanUpSpansAndNewLines()) { - mBigText = cleanUpNewLines(mBigText); - } return this; } @@ -8227,6 +8355,9 @@ public class Notification implements Parcelable // Replace the text with the big text, but only if the big text is not empty. CharSequence bigTextText = mBuilder.processLegacyText(mBigText); + if (Flags.cleanUpSpansAndNewLines()) { + bigTextText = cleanUpNewLines(stripStyling(bigTextText)); + } if (!TextUtils.isEmpty(bigTextText)) { p.text(bigTextText); } @@ -9142,11 +9273,43 @@ public class Notification implements Parcelable */ public void ensureColorContrastOrStripStyling(int backgroundColor) { if (Flags.cleanUpSpansAndNewLines()) { - mText = stripStyling(mText); + mText = stripNonStyleSpans(mText); } else { ensureColorContrast(backgroundColor); } } + + private CharSequence stripNonStyleSpans(CharSequence text) { + + if (text instanceof Spanned) { + Spanned ss = (Spanned) text; + Object[] spans = ss.getSpans(0, ss.length(), Object.class); + SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + for (Object span : spans) { + final Object resultSpan; + if (span instanceof StyleSpan + || span instanceof StrikethroughSpan + || span instanceof UnderlineSpan) { + resultSpan = span; + } else if (span instanceof TextAppearanceSpan) { + final TextAppearanceSpan originalSpan = (TextAppearanceSpan) span; + resultSpan = new TextAppearanceSpan( + null, + originalSpan.getTextStyle(), + -1, + null, + null); + } else { + continue; + } + builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), + ss.getSpanFlags(span)); + } + return builder; + } + return text; + } + /** * Updates TextAppearance spans in the message text so it has sufficient contrast * against its background. diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java index c080a6b423c5..cfbe7416bf9d 100644 --- a/core/java/android/app/contextualsearch/ContextualSearchManager.java +++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java @@ -27,6 +27,7 @@ import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.util.Log; import java.lang.annotation.Retention; @@ -51,6 +52,7 @@ public final class ContextualSearchManager { */ public static final String EXTRA_ENTRYPOINT = "android.app.contextualsearch.extra.ENTRYPOINT"; + /** * Key to get the flag_secure value from the extras of the activity launched by contextual * search. The value will be true if flag_secure is found in any of the visible activities. @@ -58,12 +60,14 @@ public final class ContextualSearchManager { */ public static final String EXTRA_FLAG_SECURE_FOUND = "android.app.contextualsearch.extra.FLAG_SECURE_FOUND"; + /** * Key to get the screenshot from the extras of the activity launched by contextual search. * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. */ public static final String EXTRA_SCREENSHOT = "android.app.contextualsearch.extra.SCREENSHOT"; + /** * Key to check whether managed profile is visible from the extras of the activity launched by * contextual search. The value will be true if any one of the visible apps is managed. @@ -71,6 +75,7 @@ public final class ContextualSearchManager { */ public static final String EXTRA_IS_MANAGED_PROFILE_VISIBLE = "android.app.contextualsearch.extra.IS_MANAGED_PROFILE_VISIBLE"; + /** * Key to get the list of visible packages from the extras of the activity launched by * contextual search. @@ -80,6 +85,18 @@ public final class ContextualSearchManager { "android.app.contextualsearch.extra.VISIBLE_PACKAGE_NAMES"; /** + * Key to get the time the user made the invocation request, based on + * {@link SystemClock#uptimeMillis()}. + * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * + * TODO: un-hide in W + * + * @hide + */ + public static final String EXTRA_INVOCATION_TIME_MS = + "android.app.contextualsearch.extra.INVOCATION_TIME_MS"; + + /** * Key to get the binder token from the extras of the activity launched by contextual search. * This token is needed to invoke {@link CallbackToken#getContextualSearchState} method. * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java index 06a0f5c09e18..769b658c78ce 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java @@ -237,19 +237,15 @@ public final class VirtualCameraConfig implements Parcelable { @IntRange(from = 1) int height, @ImageFormat.Format int format, @IntRange(from = 1) int maximumFramesPerSecond) { - // TODO(b/310857519): Check dimension upper limits based on the maximum texture size - // supported by the current device, instead of hardcoded limits. - if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) { + if (width <= 0) { throw new IllegalArgumentException( "Invalid width passed for stream config: " + width - + ", must be between 1 and " - + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT); + + ", must be greater than 0"); } - if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) { + if (height <= 0) { throw new IllegalArgumentException( "Invalid height passed for stream config: " + height - + ", must be between 1 and " - + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT); + + ", must be greater than 0"); } if (!isFormatSupported(format)) { throw new IllegalArgumentException( diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java index 00a814e7a02e..6ab66b3d2309 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java @@ -39,11 +39,6 @@ import java.util.Objects; public final class VirtualCameraStreamConfig implements Parcelable { // TODO(b/310857519): Check if we should increase the fps upper limit in future. static final int MAX_FPS_UPPER_LIMIT = 60; - // This is the minimum guaranteed upper bound of texture size supported by all devices. - // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc - // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size - // supported by the current device. - static final int DIMENSION_UPPER_LIMIT = 2048; private final int mWidth; private final int mHeight; diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 535cebb1d1af..7ac95475d24b 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1548,6 +1548,26 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long INSETS_DECOUPLED_CONFIGURATION_ENFORCED = 151861875L; /** + * When enabled, the activity will receive configuration decoupled from system bar insets. + * + * <p>This will only apply if the activity is targeting SDK level 34 or earlier versions. + * + * <p>This will only in effect if the device is trying to provide a different value by default + * other than the legacy value, i.e., the + * {@code Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout()} is set to true. + * + * <p>If the {@code Flags.insetsDecoupledConfiguration()} is also set to true, all apps + * targeting SDK level 35 or later, and apps with this override flag will receive the insets + * decoupled configuration. + * + * @hide + */ + @ChangeId + @Disabled + @Overridable + public static final long OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION = 327313645L; + + /** * Optional set of a certificates identifying apps that are allowed to embed this activity. From * the "knownActivityEmbeddingCerts" attribute. */ diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 6d3a56dcf14a..c57a3a69cb1b 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -113,6 +113,26 @@ flag { is_fixed_read_only: true } +flag { + name: "fix_avatar_picker_read_back_order" + namespace: "multiuser" + description: "Talkback focus doesn't move to the 'If you change your Google Account picture…' after swiping next to move the focus from 'Choose a picture'" + bug: "330835921" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "fix_avatar_picker_selected_read_back" + namespace: "multiuser" + description: "Talkback doesn't announce 'selected' after double tapping the button in the picture list in 'Choose a picture' page." + bug: "330840549" + metadata { + purpose: PURPOSE_BUGFIX + } +} + # This flag guards the private space feature and all its implementations excluding the APIs. APIs are guarded by android.os.Flags.allow_private_profile. flag { name: "enable_private_space_features" diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 7aa034966011..3c4307c63cf7 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -1671,7 +1671,7 @@ public class DatabaseUtils { return null; } final int eos = Math.min(n+3, end); - return sql.substring(n, eos); + return sql.substring(n, eos).toUpperCase(Locale.ROOT); } /** diff --git a/core/java/android/os/connectivity/WifiActivityEnergyInfo.java b/core/java/android/os/connectivity/WifiActivityEnergyInfo.java index ad74a9f5c906..2ceb1c71f2aa 100644 --- a/core/java/android/os/connectivity/WifiActivityEnergyInfo.java +++ b/core/java/android/os/connectivity/WifiActivityEnergyInfo.java @@ -37,8 +37,10 @@ import java.lang.annotation.RetentionPolicy; * real-time. * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass @SystemApi public final class WifiActivityEnergyInfo implements Parcelable { + private static final long DEFERRED_ENERGY_ESTIMATE = -1; @ElapsedRealtimeLong private final long mTimeSinceBootMillis; @StackState @@ -52,7 +54,7 @@ public final class WifiActivityEnergyInfo implements Parcelable { @IntRange(from = 0) private final long mControllerIdleDurationMillis; @IntRange(from = 0) - private final long mControllerEnergyUsedMicroJoules; + private long mControllerEnergyUsedMicroJoules; /** @hide */ @Retention(RetentionPolicy.SOURCE) @@ -99,9 +101,10 @@ public final class WifiActivityEnergyInfo implements Parcelable { rxDurationMillis, scanDurationMillis, idleDurationMillis, - calculateEnergyMicroJoules(txDurationMillis, rxDurationMillis, idleDurationMillis)); + DEFERRED_ENERGY_ESTIMATE); } + @android.ravenwood.annotation.RavenwoodReplace private static long calculateEnergyMicroJoules( long txDurationMillis, long rxDurationMillis, long idleDurationMillis) { final Context context = ActivityThread.currentActivityThread().getSystemContext(); @@ -125,6 +128,11 @@ public final class WifiActivityEnergyInfo implements Parcelable { * voltage); } + private static long calculateEnergyMicroJoules$ravenwood(long txDurationMillis, + long rxDurationMillis, long idleDurationMillis) { + return 0; + } + /** @hide */ public WifiActivityEnergyInfo( @ElapsedRealtimeLong long timeSinceBootMillis, @@ -152,7 +160,7 @@ public final class WifiActivityEnergyInfo implements Parcelable { + " mControllerRxDurationMillis=" + mControllerRxDurationMillis + " mControllerScanDurationMillis=" + mControllerScanDurationMillis + " mControllerIdleDurationMillis=" + mControllerIdleDurationMillis - + " mControllerEnergyUsedMicroJoules=" + mControllerEnergyUsedMicroJoules + + " mControllerEnergyUsedMicroJoules=" + getControllerEnergyUsedMicroJoules() + " }"; } @@ -231,6 +239,11 @@ public final class WifiActivityEnergyInfo implements Parcelable { /** Get the energy consumed by Wifi, in microjoules. */ @IntRange(from = 0) public long getControllerEnergyUsedMicroJoules() { + if (mControllerEnergyUsedMicroJoules == DEFERRED_ENERGY_ESTIMATE) { + mControllerEnergyUsedMicroJoules = calculateEnergyMicroJoules( + mControllerTxDurationMillis, mControllerRxDurationMillis, + mControllerIdleDurationMillis); + } return mControllerEnergyUsedMicroJoules; } diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index 3b5990181d8b..55011e52724c 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -98,7 +98,7 @@ interface IPermissionManager { IBinder registerAttributionSource(in AttributionSourceState source); - int getNumRegisteredAttributionSources(int uid); + int getRegisteredAttributionSourceCount(int uid); boolean isRegisteredAttributionSource(in AttributionSourceState source); diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 2daf4ac8226e..55bb430d1523 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -1680,9 +1680,9 @@ public final class PermissionManager { * @hide */ @RequiresPermission(Manifest.permission.UPDATE_APP_OPS_STATS) - public int getNumRegisteredAttributionSourcesForTest(int uid) { + public int getRegisteredAttributionSourceCountForTest(int uid) { try { - return mPermissionManager.getNumRegisteredAttributionSources(uid); + return mPermissionManager.getRegisteredAttributionSourceCount(uid); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java index 9a02b74b37d0..5da0cb48ad1f 100644 --- a/core/java/android/service/dreams/DreamOverlayService.java +++ b/core/java/android/service/dreams/DreamOverlayService.java @@ -79,6 +79,11 @@ public abstract class DreamOverlayService extends Service { mService.endDream(this); } + @Override + public void comeToFront() { + mService.comeToFront(this); + } + private void onExitRequested() { try { mDreamOverlayCallback.onExitRequested(); @@ -130,6 +135,16 @@ public abstract class DreamOverlayService extends Service { }); } + private void comeToFront(OverlayClient client) { + mExecutor.execute(() -> { + if (mCurrentClient != client) { + return; + } + + onComeToFront(); + }); + } + private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() { @Override public void getClient(IDreamOverlayClientCallback callback) { @@ -190,6 +205,13 @@ public abstract class DreamOverlayService extends Service { public void onWakeUp() {} /** + * This method is overridden by implementations to handle when the dream is coming to the front + * (after having lost focus to something on top of it). + * @hide + */ + public void onComeToFront() {} + + /** * This method is overridden by implementations to handle when the dream has ended. There may * be earlier signals leading up to this step, such as @{@link #onWakeUp(Runnable)}. * diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 353828c105bb..c26d83c4853d 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -18,6 +18,7 @@ package android.service.dreams; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.dreams.Flags.dreamHandlesConfirmKeys; +import static android.service.dreams.Flags.dreamTracksFocus; import android.annotation.FlaggedApi; import android.annotation.IdRes; @@ -52,6 +53,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.service.controls.flags.Flags; +import android.service.dreams.utils.DreamAccessibility; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; @@ -273,6 +275,7 @@ public class DreamService extends Service implements Window.Callback { private boolean mDebug = false; private ComponentName mDreamComponent; + private DreamAccessibility mDreamAccessibility; private boolean mShouldShowComplications; private DreamServiceWrapper mDreamServiceWrapper; @@ -455,6 +458,15 @@ public class DreamService extends Service implements Window.Callback { /** {@inheritDoc} */ @Override public void onWindowFocusChanged(boolean hasFocus) { + if (!dreamTracksFocus()) { + return; + } + + try { + mDreamManager.onDreamFocusChanged(hasFocus); + } catch (RemoteException ex) { + // system server died + } } /** {@inheritDoc} */ @@ -664,6 +676,7 @@ public class DreamService extends Service implements Window.Callback { */ public void setInteractive(boolean interactive) { mInteractive = interactive; + updateAccessibilityMessage(); } /** @@ -1149,6 +1162,19 @@ public class DreamService extends Service implements Window.Callback { wakeUp(false); } + /** + * Tells the dream to come to the front (which in turn tells the overlay to come to the front). + */ + private void comeToFront() { + mOverlayConnection.addConsumer(overlay -> { + try { + overlay.comeToFront(); + } catch (RemoteException e) { + Log.e(mTag, "could not tell overlay to come to front:" + e); + } + }); + } + private void wakeUp(boolean fromSystem) { if (mDebug) { Slog.v(mTag, "wakeUp(): fromSystem=" + fromSystem + ", mWaking=" + mWaking @@ -1430,7 +1456,7 @@ public class DreamService extends Service implements Window.Callback { // Hide all insets when the dream is showing mWindow.getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); mWindow.setDecorFitsSystemWindows(false); - + updateAccessibilityMessage(); mWindow.getDecorView().addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { private Consumer<IDreamOverlayClient> mDreamStartOverlayConsumer; @@ -1477,6 +1503,15 @@ public class DreamService extends Service implements Window.Callback { }); } + private void updateAccessibilityMessage() { + if (mWindow == null) return; + if (mDreamAccessibility == null) { + final View rootView = mWindow.getDecorView(); + mDreamAccessibility = new DreamAccessibility(this, rootView); + } + mDreamAccessibility.updateAccessibilityConfiguration(isInteractive()); + } + private boolean getWindowFlagValue(int flag, boolean defaultValue) { return mWindow == null ? defaultValue : (mWindow.getAttributes().flags & flag) != 0; } @@ -1596,6 +1631,15 @@ public class DreamService extends Service implements Window.Callback { public void wakeUp() { mHandler.post(() -> DreamService.this.wakeUp(true /*fromSystem*/)); } + + @Override + public void comeToFront() { + if (!dreamTracksFocus()) { + return; + } + + mHandler.post(DreamService.this::comeToFront); + } } /** @hide */ diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl index e45384f5512a..85f0368a7b5c 100644 --- a/core/java/android/service/dreams/IDreamManager.aidl +++ b/core/java/android/service/dreams/IDreamManager.aidl @@ -48,4 +48,5 @@ interface IDreamManager { void setSystemDreamComponent(in ComponentName componentName); void registerDreamOverlayService(in ComponentName componentName); void startDreamActivity(in Intent intent); + void onDreamFocusChanged(in boolean hasFocus); } diff --git a/core/java/android/service/dreams/IDreamOverlayClient.aidl b/core/java/android/service/dreams/IDreamOverlayClient.aidl index 78b7280ae652..5054d4d749d3 100644 --- a/core/java/android/service/dreams/IDreamOverlayClient.aidl +++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl @@ -42,4 +42,7 @@ interface IDreamOverlayClient { /** Called when the dream has ended. */ void endDream(); + + /** Called when the dream is coming to the front. */ + void comeToFront(); } diff --git a/core/java/android/service/dreams/IDreamService.aidl b/core/java/android/service/dreams/IDreamService.aidl index 8b5d8754647c..2e2651bfb91d 100644 --- a/core/java/android/service/dreams/IDreamService.aidl +++ b/core/java/android/service/dreams/IDreamService.aidl @@ -25,4 +25,5 @@ oneway interface IDreamService { void attach(IBinder windowToken, boolean canDoze, boolean isPreviewMode, IRemoteCallback started); void detach(); void wakeUp(); + void comeToFront(); } diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig index 88f1090750d2..0a458bccc83c 100644 --- a/core/java/android/service/dreams/flags.aconfig +++ b/core/java/android/service/dreams/flags.aconfig @@ -19,3 +19,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "dream_tracks_focus" + namespace: "communal" + description: "This flag enables the ability for dreams to track whether or not they have focus" + bug: "331798001" +} diff --git a/core/java/android/service/dreams/utils/DreamAccessibility.java b/core/java/android/service/dreams/utils/DreamAccessibility.java new file mode 100644 index 000000000000..c38f41bab5a6 --- /dev/null +++ b/core/java/android/service/dreams/utils/DreamAccessibility.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.dreams.utils; + +import android.annotation.NonNull; +import android.content.Context; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import com.android.internal.R; + +/** + * {@link DreamAccessibility} allows customization of accessibility + * actions for the root view of the dream overlay. + * @hide + */ +public class DreamAccessibility { + private final Context mContext; + private final View mView; + private final View.AccessibilityDelegate mAccessibilityDelegate; + + public DreamAccessibility(@NonNull Context context, @NonNull View view) { + mContext = context; + mView = view; + mAccessibilityDelegate = createNewAccessibilityDelegate(mContext); + } + + /** + * @param interactive + * Removes and add accessibility configuration depending if the dream is interactive or not + */ + public void updateAccessibilityConfiguration(Boolean interactive) { + if (!interactive) { + addAccessibilityConfiguration(); + } else { + removeCustomAccessibilityAction(); + } + } + + /** + * Configures the accessibility actions for the given root view. + */ + private void addAccessibilityConfiguration() { + mView.setAccessibilityDelegate(mAccessibilityDelegate); + } + + /** + * Removes Configured the accessibility actions for the given root view. + */ + private void removeCustomAccessibilityAction() { + if (mView.getAccessibilityDelegate() == mAccessibilityDelegate) { + mView.setAccessibilityDelegate(null); + } + } + + private View.AccessibilityDelegate createNewAccessibilityDelegate(Context context) { + return new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + for (AccessibilityNodeInfo.AccessibilityAction action : info.getActionList()) { + if (action.getId() == AccessibilityNodeInfo.ACTION_CLICK) { + info.removeAction(action); + break; + } + } + info.addAction(new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, + context.getResources().getString(R.string.dream_accessibility_action_click) + )); + } + }; + } +} diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index fb57921b1529..4281da12720b 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -1685,6 +1685,10 @@ public class PhoneStateListener { public final void onSimultaneousCallingStateChanged(int[] subIds) { // not supported on the deprecated interface - Use TelephonyCallback instead } + + public final void onCarrierRoamingNtnModeChanged(boolean active) { + // not supported on the deprecated interface - Use TelephonyCallback instead + } } private void log(String s) { diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index dc6a035a8176..b8b84d93c97c 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -644,6 +644,15 @@ public class TelephonyCallback { public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; /** + * Event for listening to changes in carrier roaming non-terrestrial network mode. + * + * @see CarrierRoamingNtnModeListener + * + * @hide + */ + public static final int EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED = 42; + + /** * @hide */ @IntDef(prefix = {"EVENT_"}, value = { @@ -687,7 +696,8 @@ public class TelephonyCallback { EVENT_TRIGGER_NOTIFY_ANBR, EVENT_MEDIA_QUALITY_STATUS_CHANGED, EVENT_EMERGENCY_CALLBACK_MODE_CHANGED, - EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED + EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED, + EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface TelephonyEvent { @@ -1686,6 +1696,24 @@ public class TelephonyCallback { } /** + * Interface for carrier roaming non-terrestrial network listener. + * + * @hide + */ + public interface CarrierRoamingNtnModeListener { + /** + * Callback invoked when carrier roaming non-terrestrial network mode changes. + * + * @param active {@code true} If the device is connected to carrier roaming + * non-terrestrial network or was connected within the + * {CarrierConfigManager + * #KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT} duration, + * {code false} otherwise. + */ + void onCarrierRoamingNtnModeChanged(boolean active); + } + + /** * The callback methods need to be called on the handler thread where * this object was created. If the binder did that for us it'd be nice. * <p> @@ -2086,5 +2114,16 @@ public class TelephonyCallback { Binder.withCleanCallingIdentity( () -> mExecutor.execute(() -> listener.onCallBackModeStopped(type, reason))); } + + public void onCarrierRoamingNtnModeChanged(boolean active) { + if (!Flags.carrierEnabledSatelliteFlag()) return; + + CarrierRoamingNtnModeListener listener = + (CarrierRoamingNtnModeListener) mTelephonyCallbackWeakRef.get(); + if (listener == null) return; + + Binder.withCleanCallingIdentity( + () -> mExecutor.execute(() -> listener.onCarrierRoamingNtnModeChanged(active))); + } } } diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index d39c4ce4ce24..6160fdb223f2 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -1073,6 +1073,24 @@ public class TelephonyRegistryManager { } /** + * Notify external listeners that carrier roaming non-terrestrial network mode changed. + * @param subId subscription ID. + * @param active {@code true} If the device is connected to carrier roaming + * non-terrestrial network or was connected within the + * {CarrierConfigManager#KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT} + * duration, {code false} otherwise. + * @hide + */ + public void notifyCarrierRoamingNtnModeChanged(int subId, boolean active) { + try { + sRegistry.notifyCarrierRoamingNtnModeChanged(subId, active); + } catch (RemoteException ex) { + // system server crash + throw ex.rethrowFromSystemServer(); + } + } + + /** * Processes potential event changes from the provided {@link TelephonyCallback}. * * @param telephonyCallback callback for monitoring callback changes to the telephony state. @@ -1224,6 +1242,10 @@ public class TelephonyRegistryManager { eventList.add( TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); } + + if (telephonyCallback instanceof TelephonyCallback.CarrierRoamingNtnModeListener) { + eventList.add(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED); + } return eventList; } diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java index 38aeb3757eae..161a79b39d75 100644 --- a/core/java/android/text/MeasuredParagraph.java +++ b/core/java/android/text/MeasuredParagraph.java @@ -30,6 +30,8 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.text.LineBreakConfig; import android.graphics.text.MeasuredText; +import android.icu.lang.UCharacter; +import android.icu.lang.UCharacterDirection; import android.icu.text.Bidi; import android.text.AutoGrowArray.ByteArray; import android.text.AutoGrowArray.FloatArray; @@ -711,7 +713,12 @@ public class MeasuredParagraph { // check the paragraph count here and replace the CR letters and re-calculate // BiDi again. for (int i = 0; i < mTextLength; ++i) { - if (mCopiedBuffer[i] == '\r') { + if (Character.isSurrogate(mCopiedBuffer[i])) { + // All block separators are in BMP. + continue; + } + if (UCharacter.getDirection(mCopiedBuffer[i]) + == UCharacterDirection.BLOCK_SEPARATOR) { mCopiedBuffer[i] = OBJECT_REPLACEMENT_CHARACTER; } } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index bde9c7770eb7..3015791ee0a9 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -477,7 +477,12 @@ public class TextLine { } drawBounds.setEmpty(); float w = measure(mLen, false, fmi, drawBounds, lineInfo); - float boundsWidth = drawBounds.width(); + float boundsWidth; + if (w >= 0) { + boundsWidth = Math.max(drawBounds.right, w) - Math.min(0, drawBounds.left); + } else { + boundsWidth = Math.max(drawBounds.right, 0) - Math.min(w, drawBounds.left); + } if (Math.abs(w) > boundsWidth) { return w; } else { diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 51d7caacd4b7..65d9b3ae6207 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -291,6 +291,15 @@ interface IWindowManager int getDefaultDisplayRotation(); /** + * Retrieve the display user rotation. + * @param displayId Id of the display + * @return Rotation one of {@link android.view.Surface#ROTATION_0}, + * {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180}, + * {@link android.view.Surface#ROTATION_270} or -1 if display is not found. + */ + int getDisplayUserRotation(int displayId); + + /** * Watch the rotation of the specified screen. Returns the current rotation, * calls back when it changes. */ diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java index 6c852c3a2d3f..86e5bea46882 100644 --- a/core/java/android/view/ViewStructure.java +++ b/core/java/android/view/ViewStructure.java @@ -76,6 +76,21 @@ public abstract class ViewStructure { "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION"; /** + * Key used for writing the type of the view that generated the virtual structure of its + * children. + * + * For example, if the virtual structure is generated by a webview, the value would be + * "WebView". If the virtual structure is generated by a compose view, then the value would be + * "ComposeView". The value is of type String. + * + * This value is added to mainly help with debugging purpose. + * + * @hide + */ + public static final String EXTRA_VIRTUAL_STRUCTURE_TYPE = + "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_TYPE"; + + /** * Set the identifier for this view. * * @param id The view's identifier, as per {@link View#getId View.getId()}. diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index fbb5116fb82f..fb1c331171a6 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -11254,8 +11254,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener width = des; } else { if (mUseBoundsForWidth) { - width = Math.max(boring.width, - (int) Math.ceil(boring.getDrawingBoundingBox().width())); + 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; } diff --git a/core/java/com/android/internal/os/flags.aconfig b/core/java/com/android/internal/os/flags.aconfig index 4c2bbd45834e..c8d6810e274a 100644 --- a/core/java/com/android/internal/os/flags.aconfig +++ b/core/java/com/android/internal/os/flags.aconfig @@ -1,4 +1,5 @@ package: "com.android.internal.os" +container: "system" flag { name: "enable_apache_http_legacy_preload" @@ -7,4 +8,4 @@ flag { # Fixed read-only is required as the flag is read during zygote init. is_fixed_read_only: true bug: "241474956" -}
\ No newline at end of file +} diff --git a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index 969f95db002d..792c22348d77 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -81,4 +81,5 @@ oneway interface IPhoneStateListener { void onCallBackModeStarted(int type); void onCallBackModeStopped(int type, int reason); void onSimultaneousCallingStateChanged(in int[] subIds); + void onCarrierRoamingNtnModeChanged(in boolean active); } diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index 0203ea49f252..04332cd758a6 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -120,4 +120,5 @@ interface ITelephonyRegistry { void notifyCallbackModeStarted(int phoneId, int subId, int type); void notifyCallbackModeStopped(int phoneId, int subId, int type, int reason); + void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active); } diff --git a/core/res/res/values/config_battery_stats.xml b/core/res/res/values/config_battery_stats.xml index ae4789937832..8d9736273145 100644 --- a/core/res/res/values/config_battery_stats.xml +++ b/core/res/res/values/config_battery_stats.xml @@ -35,6 +35,9 @@ <!-- Mobile Radio power stats collection throttle period in milliseconds. --> <integer name="config_defaultPowerStatsThrottlePeriodMobileRadio">3600000</integer> + <!-- Mobile Radio power stats collection throttle period in milliseconds. --> + <integer name="config_defaultPowerStatsThrottlePeriodWifi">3600000</integer> + <!-- PowerStats aggregation period in milliseconds. This is the interval at which the power stats aggregation procedure is performed and the results stored in PowerStatsStore. --> <integer name="config_powerStatsAggregationPeriod">14400000</integer> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 60289c1921c2..e96240d4fc1e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1018,6 +1018,9 @@ <!-- The title to use when a dream is opened in preview mode. [CHAR LIMIT=NONE] --> <string name="dream_preview_title">Preview, <xliff:g id="dream_name" example="Clock">%1$s</xliff:g></string> + <!-- The title to use when a dream is open in accessibility mode to let users know to double tap to dismiss the dream [CHAR LIMIT=32] --> + <string name="dream_accessibility_action_click">dismiss</string> + <!-- Permissions --> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index fbc88116bd6b..0bf6347b8284 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4471,6 +4471,7 @@ <java-symbol type="string" name="capability_title_canTakeScreenshot" /> <java-symbol type="string" name="dream_preview_title" /> + <java-symbol type="string" name="dream_accessibility_action_click" /> <java-symbol type="string" name="config_servicesExtensionPackage" /> @@ -5218,6 +5219,7 @@ <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" /> <java-symbol type="integer" name="config_defaultPowerStatsThrottlePeriodCpu" /> <java-symbol type="integer" name="config_defaultPowerStatsThrottlePeriodMobileRadio" /> + <java-symbol type="integer" name="config_defaultPowerStatsThrottlePeriodWifi" /> <java-symbol type="integer" name="config_powerStatsAggregationPeriod" /> <java-symbol type="integer" name="config_aggregatedPowerStatsSpanDuration" /> diff --git a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java index c9cb2cc416e8..e25fdf9f2291 100644 --- a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java +++ b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java @@ -90,6 +90,12 @@ public class DatabaseUtilsTest { assertEquals(ddl, getSqlStatementType("ALTER TABLE t1 ADD COLUMN j int")); assertEquals(ddl, getSqlStatementType("CREATE TABLE t1 (i int)")); + // Verify that the answers are case-insensitive + assertEquals(sel, getSqlStatementType("select")); + assertEquals(sel, getSqlStatementType("sElect")); + assertEquals(sel, getSqlStatementType("sELECT")); + assertEquals(sel, getSqlStatementType("seLECT")); + // Short statements, leading comments, and WITH are decoded to "other" in the public API. final int othr = STATEMENT_OTHER; assertEquals(othr, getSqlStatementType("SE")); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index a67821b7e819..0297901c8921 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -288,13 +288,16 @@ public class BadgedImageView extends ConstraintLayout { /** Sets the position of the dot and badge, animating them out and back in if requested. */ void animateDotBadgePositions(boolean onLeft) { - mOnLeft = onLeft; - - if (onLeft != getDotOnLeft() && shouldDrawDot()) { - animateDotScale(0f /* showDot */, () -> { - invalidate(); - animateDotScale(1.0f, null /* after */); - }); + if (onLeft != getDotOnLeft()) { + if (shouldDrawDot()) { + animateDotScale(0f /* showDot */, () -> { + mOnLeft = onLeft; + invalidate(); + animateDotScale(1.0f, null /* after */); + }); + } else { + mOnLeft = onLeft; + } } // TODO animate badge showBadge(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 74f087b6d8f8..0b3c2ba37caf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -446,6 +446,8 @@ public class BubbleExpandedView extends LinearLayout { mManageButton.setVisibility(GONE); } else { mTaskView = bubbleTaskView.getTaskView(); + // reset the insets that might left after TaskView is shown in BubbleBarExpandedView + mTaskView.setCaptionInsets(null); bubbleTaskView.setDelegateListener(mTaskViewListener); // set a fixed width so it is not recalculated as part of a rotation. the width will be diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java index b87c2f6ebad5..7ceaaea3962f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java @@ -125,6 +125,7 @@ public class PipBoundsState { private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); + private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>(); // the size of the current bounds relative to the max size spec private float mBoundsScale; @@ -297,7 +298,12 @@ public class PipBoundsState { /** Set the PIP aspect ratio. */ public void setAspectRatio(float aspectRatio) { - mAspectRatio = aspectRatio; + if (Float.compare(mAspectRatio, aspectRatio) != 0) { + mAspectRatio = aspectRatio; + for (Consumer<Float> callback : mOnAspectRatioChangedCallbacks) { + callback.accept(mAspectRatio); + } + } } /** Get the PIP aspect ratio. */ @@ -527,6 +533,23 @@ public class PipBoundsState { mOnPipExclusionBoundsChangeCallbacks.remove(onPipExclusionBoundsChangeCallback); } + /** Adds callback to listen on aspect ratio change. */ + public void addOnAspectRatioChangedCallback( + @NonNull Consumer<Float> onAspectRatioChangedCallback) { + if (!mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) { + mOnAspectRatioChangedCallbacks.add(onAspectRatioChangedCallback); + onAspectRatioChangedCallback.accept(mAspectRatio); + } + } + + /** Removes callback to listen on aspect ratio change. */ + public void removeOnAspectRatioChangedCallback( + @NonNull Consumer<Float> onAspectRatioChangedCallback) { + if (mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) { + mOnAspectRatioChangedCallbacks.remove(onAspectRatioChangedCallback); + } + } + public LauncherState getLauncherState() { return mLauncherState; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 57cf99211b6e..e885262658f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -597,6 +597,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } + if (mPipTransitionState.isEnteringPip() + && !mPipTransitionState.getInSwipePipToHomeTransition()) { + // If we are still entering PiP with Shell playing enter animation, jump-cut to + // the end of the enter animation and reschedule exitPip to run after enter-PiP + // has finished its transition and allowed the client to draw in PiP mode. + mPipTransitionController.end(() -> { + exitPip(animationDurationMs, requestEnterSplit); + }); + return; + } + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState); final WindowContainerTransaction wct = new WindowContainerTransaction(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 10b7619c4adb..fdde3ee01264 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -43,7 +43,6 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; -import android.animation.Animator; import android.annotation.IntDef; import android.app.ActivityManager; import android.app.TaskInfo; @@ -348,9 +347,16 @@ public class PipTransition extends PipTransitionController { @Override public void end() { - Animator animator = mPipAnimationController.getCurrentAnimator(); - if (animator != null && animator.isRunning()) { - animator.end(); + end(null); + } + + @Override + public void end(@Nullable Runnable onTransitionEnd) { + if (mPipAnimationController.isAnimating()) { + mPipAnimationController.getCurrentAnimator().end(); + } + if (onTransitionEnd != null) { + onTransitionEnd.run(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 32442f740a52..4f71a02528c3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -305,6 +305,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void end() { } + /** + * End the currently-playing PiP animation. + * + * @param onTransitionEnd callback to run upon finishing the playing transition. + */ + public void end(@Nullable Runnable onTransitionEnd) { + } + /** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */ public void startHighPerfSession() {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index c1adfffce074..d8ac8e948a97 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -219,6 +219,7 @@ public class PipTouchHandler { mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(), this::onAccessibilityShowMenu, this::updateMovementBounds, this::animateToUnStashedState, mainExecutor); + mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); // TODO(b/181599115): This should really be initializes as part of the pip controller, but // until all PIP implementations derive from the controller, just initialize the touch handler diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index cc8e3e0934e6..472003cb435f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -208,6 +208,7 @@ public class PipTouchHandler { new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, mTouchState, this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor, mPipPerfHintController); + mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize); if (PipUtils.isPip2ExperimentEnabled()) { shellInit.addInitCallback(this::onInit, this); diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt new file mode 100644 index 000000000000..5563bb9fa934 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +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.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppWithAppHeaderExitLandscape : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_90) { + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt new file mode 100644 index 000000000000..3d16d2219c78 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +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.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppWithAppHeaderExitPortrait : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_0) { + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt new file mode 100644 index 000000000000..75dfeba3e662 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt @@ -0,0 +1,118 @@ +/* + * 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.service.desktopmode.flicker + +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways +import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart +import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.desktopmode.Components +import android.tools.flicker.extractors.ITransitionMatcher +import android.tools.flicker.extractors.ShellTransitionScenarioExtractor +import android.tools.traces.wm.Transition +import android.tools.traces.wm.TransitionType + +class DesktopModeFlickerScenarios { + companion object { + val END_DRAG_TO_DESKTOP = + FlickerConfigEntry( + scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP + } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd( + Components.DESKTOP_MODE_APP + ) + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { it.type == TransitionType.CLOSE } + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + + val CLOSE_LAST_APP = + FlickerConfigEntry( + scenarioId = ScenarioId("CLOSE_LAST_APP"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions.findLast { it.type == TransitionType.CLOSE } + return if (lastTransition != null) listOf(lastTransition) + else emptyList() + } + } + ), + assertions = + AssertionTemplates.COMMON_ASSERTIONS + + listOf( + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + LauncherWindowMovesToTop() + ) + .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt index 4c781d36acf6..9dfafe958b0b 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt @@ -17,58 +17,28 @@ package com.android.wm.shell.flicker.service.desktopmode.flicker import android.tools.Rotation -import android.tools.flicker.AssertionInvocationGroup import android.tools.flicker.FlickerConfig import android.tools.flicker.annotation.ExpectedScenarios import android.tools.flicker.annotation.FlickerConfigProvider -import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways -import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd -import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd -import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfig -import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.FlickerServiceConfig -import android.tools.flicker.config.ScenarioId -import android.tools.flicker.config.desktopmode.Components -import android.tools.flicker.extractors.ITransitionMatcher -import android.tools.flicker.extractors.ShellTransitionScenarioExtractor import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import android.tools.traces.wm.Transition -import android.tools.traces.wm.TransitionType +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag import org.junit.Test import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) { - @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() = - super.enterDesktopWithDrag() + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) + @Test + override fun enterDesktopWithDrag() = super.enterDesktopWithDrag() companion object { - private val END_DRAG_TO_DESKTOP = FlickerConfigEntry( - scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), - extractor = ShellTransitionScenarioExtractor( - transitionMatcher = object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP} - } - }), - assertions = AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP) - ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) @JvmStatic @FlickerConfigProvider fun flickerConfigProvider(): FlickerConfig = - FlickerConfig() - .use(FlickerServiceConfig.DEFAULT) - .use(END_DRAG_TO_DESKTOP) + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt index d99d875fb126..1c7d6237eb8a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt @@ -17,58 +17,27 @@ package com.android.wm.shell.flicker.service.desktopmode.flicker import android.tools.Rotation -import android.tools.flicker.AssertionInvocationGroup import android.tools.flicker.FlickerConfig import android.tools.flicker.annotation.ExpectedScenarios import android.tools.flicker.annotation.FlickerConfigProvider -import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways -import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd -import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd -import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfig -import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.FlickerServiceConfig -import android.tools.flicker.config.ScenarioId -import android.tools.flicker.config.desktopmode.Components -import android.tools.flicker.extractors.ITransitionMatcher -import android.tools.flicker.extractors.ShellTransitionScenarioExtractor import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner -import android.tools.traces.wm.Transition -import android.tools.traces.wm.TransitionType +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag import org.junit.Test import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) { - @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) @Test override fun enterDesktopWithDrag() = - super.enterDesktopWithDrag() + @ExpectedScenarios(["END_DRAG_TO_DESKTOP"]) + @Test + override fun enterDesktopWithDrag() = super.enterDesktopWithDrag() companion object { - private val END_DRAG_TO_DESKTOP = FlickerConfigEntry( - scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), - extractor = ShellTransitionScenarioExtractor( - transitionMatcher = object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP} - } - }), - assertions = AssertionTemplates.COMMON_ASSERTIONS + - listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd(Components.DESKTOP_MODE_APP) - ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), - ) - @JvmStatic @FlickerConfigProvider fun flickerConfigProvider(): FlickerConfig = - FlickerConfig() - .use(FlickerServiceConfig.DEFAULT) - .use(END_DRAG_TO_DESKTOP) + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP) } } diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt new file mode 100644 index 000000000000..0c2b5015840d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt @@ -0,0 +1,77 @@ +/* + * 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.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Base Test Class") +abstract class CloseAllAppsWithAppHeaderExit +@JvmOverloads +constructor(val rotation: Rotation = Rotation.ROTATION_0) { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + + + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode()) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopWithDrag(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + nonResizeableApp.launchViaIntent(wmHelper) + } + + @Test + open fun closeAllAppsInDesktop() { + nonResizeableApp.closeDesktopApp(wmHelper, device) + mailApp.closeDesktopApp(wmHelper, device) + testApp.closeDesktopApp(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt index 0403b4f64faf..9e9998ef7c2a 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt @@ -23,15 +23,18 @@ import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags import com.android.wm.shell.flicker.service.common.Utils -import com.android.wm.shell.flicker.utils.DesktopModeUtils import org.junit.After +import org.junit.Assume import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test + @Ignore("Base Test Class") abstract class EnterDesktopWithDrag @JvmOverloads @@ -41,19 +44,20 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { private val tapl = LauncherInstrumentation() private val wmHelper = WindowManagerStateHelper(instrumentation) private val device = UiDevice.getInstance(instrumentation) - private val testApp = SimpleAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @Before fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode()) tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) } @Test open fun enterDesktopWithDrag() { - DesktopModeUtils.enterDesktopWithDrag(wmHelper, device, testApp) + testApp.enterDesktopWithDrag(wmHelper, device) } @After diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt deleted file mode 100644 index 345bc5ebb20e..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/DesktopModeUtils.kt +++ /dev/null @@ -1,112 +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.wm.shell.flicker.utils - -import android.tools.device.apphelpers.StandardAppHelper -import android.tools.helpers.SYSTEMUI_PACKAGE -import android.tools.traces.component.IComponentMatcher -import android.tools.traces.parsers.WindowManagerStateHelper -import android.tools.traces.wm.WindowingMode -import androidx.test.uiautomator.By -import androidx.test.uiautomator.BySelector -import androidx.test.uiautomator.UiDevice -import androidx.test.uiautomator.Until - -/** - * Provides a collection of utility functions for desktop mode testing. - */ -object DesktopModeUtils { - private const val TIMEOUT_MS = 3_000L - private const val CAPTION = "desktop_mode_caption" - private const val CAPTION_HANDLE = "caption_handle" - private const val MAXIMIZE_BUTTON = "maximize_button_view" - - private val captionFullscreen: BySelector - get() = By.res(SYSTEMUI_PACKAGE, CAPTION) - private val captionHandle: BySelector - get() = By.res(SYSTEMUI_PACKAGE, CAPTION_HANDLE) - private val maximizeButton: BySelector - get() = By.res(SYSTEMUI_PACKAGE, MAXIMIZE_BUTTON) - - /** - * Wait for an app moved to desktop to finish its transition. - */ - private fun waitForAppToMoveToDesktop( - wmHelper: WindowManagerStateHelper, - currentApp: IComponentMatcher, - ) { - wmHelper - .StateSyncBuilder() - .withWindowSurfaceAppeared(currentApp) - .withFreeformApp(currentApp) - .withAppTransitionIdle() - .waitForAndVerify() - } - - /** - * Click maximise button on the app header for the given app. - */ - fun maximiseDesktopApp( - wmHelper: WindowManagerStateHelper, - device: UiDevice, - currentApp: StandardAppHelper - ) { - if (wmHelper.getWindow(currentApp)?.windowingMode - != WindowingMode.WINDOWING_MODE_FREEFORM.value) - error("expected a freeform window to maximise but window is not in freefrom mode") - - val maximizeButton = - device.wait(Until.findObject(maximizeButton), TIMEOUT_MS) - ?: error("Unable to find view $maximizeButton\n") - maximizeButton.click() - } - - /** - * Move an app to Desktop by dragging the app handle at the top. - */ - fun enterDesktopWithDrag( - wmHelper: WindowManagerStateHelper, - device: UiDevice, - currentApp: StandardAppHelper, - ) { - currentApp.launchViaIntent(wmHelper) - dragToDesktop(wmHelper, currentApp, device) - waitForAppToMoveToDesktop(wmHelper, currentApp) - } - - private fun dragToDesktop( - wmHelper: WindowManagerStateHelper, - currentApp: StandardAppHelper, - device: UiDevice - ) { - val windowRect = wmHelper.getWindowRegion(currentApp).bounds - val startX = windowRect.centerX() - - // Start dragging a little under the top to prevent dragging the notification shade. - val startY = 10 - - val displayRect = - wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect - ?: throw IllegalStateException("Default display is null") - - // The position we want to drag to - val endY = displayRect.centerY() / 2 - - // drag the window to move to desktop - device.drag(startX, startY, startX, endY, 100) - } -} diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index b87002371775..8e07a2f31de1 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -15,14 +15,18 @@ */ #include "ShaderCache.h" + #include <GrDirectContext.h> #include <SkData.h> #include <gui/TraceUtils.h> #include <log/log.h> #include <openssl/sha.h> + #include <algorithm> #include <array> +#include <mutex> #include <thread> + #include "FileBlobCache.h" #include "Properties.h" diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp index 6993d5240187..7a155c583fd4 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.cpp +++ b/libs/hwui/renderthread/HintSessionWrapper.cpp @@ -45,7 +45,7 @@ void HintSessionWrapper::HintSessionBinding::init() { LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!"); BIND_APH_METHOD(getManager); - BIND_APH_METHOD(createSession); + BIND_APH_METHOD(createSessionInternal); BIND_APH_METHOD(closeSession); BIND_APH_METHOD(updateTargetWorkDuration); BIND_APH_METHOD(reportActualWorkDuration); @@ -122,7 +122,8 @@ bool HintSessionWrapper::init() { int64_t targetDurationNanos = mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration; mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] { - return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos); + return mBinding->createSessionInternal(manager, tids.data(), tids.size(), + targetDurationNanos, SessionTag::HWUI); }); return false; } diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h index 14e7a53fd94f..859cc57dea9f 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.h +++ b/libs/hwui/renderthread/HintSessionWrapper.h @@ -17,6 +17,7 @@ #pragma once #include <android/performance_hint.h> +#include <private/performance_hint_private.h> #include <future> #include <optional> @@ -80,9 +81,10 @@ private: virtual ~HintSessionBinding() = default; virtual void init(); APerformanceHintManager* (*getManager)(); - APerformanceHintSession* (*createSession)(APerformanceHintManager* manager, - const int32_t* tids, size_t tidCount, - int64_t defaultTarget) = nullptr; + APerformanceHintSession* (*createSessionInternal)(APerformanceHintManager* manager, + const int32_t* tids, size_t tidCount, + int64_t defaultTarget, + SessionTag tag) = nullptr; void (*closeSession)(APerformanceHintSession* session) = nullptr; void (*updateTargetWorkDuration)(APerformanceHintSession* session, int64_t targetDuration) = nullptr; diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp index c16602c29e2a..a8db0f4aa4f0 100644 --- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp +++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp @@ -52,8 +52,8 @@ protected: void init() override; MOCK_METHOD(APerformanceHintManager*, fakeGetManager, ()); - MOCK_METHOD(APerformanceHintSession*, fakeCreateSession, - (APerformanceHintManager*, const int32_t*, size_t, int64_t)); + MOCK_METHOD(APerformanceHintSession*, fakeCreateSessionInternal, + (APerformanceHintManager*, const int32_t*, size_t, int64_t, SessionTag)); MOCK_METHOD(void, fakeCloseSession, (APerformanceHintSession*)); MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t)); MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t)); @@ -72,22 +72,28 @@ protected: // Must be static so we can point to them as normal fn pointers with HintSessionBinding static APerformanceHintManager* stubGetManager() { return sMockBinding->fakeGetManager(); }; - static APerformanceHintSession* stubCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + static APerformanceHintSession* stubCreateSessionInternal(APerformanceHintManager* manager, + const int32_t* ids, size_t idsSize, + int64_t initialTarget, + SessionTag tag) { + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } - static APerformanceHintSession* stubManagedCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { + static APerformanceHintSession* stubManagedCreateSessionInternal( + APerformanceHintManager* manager, const int32_t* ids, size_t idsSize, + int64_t initialTarget, SessionTag tag) { sMockBinding->allowCreationToFinish.get_future().wait(); - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } - static APerformanceHintSession* stubSlowCreateSession(APerformanceHintManager* manager, - const int32_t* ids, size_t idsSize, - int64_t initialTarget) { + static APerformanceHintSession* stubSlowCreateSessionInternal(APerformanceHintManager* manager, + const int32_t* ids, + size_t idsSize, + int64_t initialTarget, + SessionTag tag) { std::this_thread::sleep_for(50ms); - return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget); + return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget, + SessionTag::HWUI); } static void stubCloseSession(APerformanceHintSession* session) { sMockBinding->fakeCloseSession(session); @@ -139,14 +145,14 @@ void HintSessionWrapperTests::SetUp() { mWrapper = std::make_shared<HintSessionWrapper>(uiThreadId, renderThreadId); mWrapper->mBinding = sMockBinding; EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr)); - ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr)); + ON_CALL(*sMockBinding, fakeCreateSessionInternal).WillByDefault(Return(sessionPtr)); ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0)); } void HintSessionWrapperTests::MockHintSessionBinding::init() { sMockBinding->getManager = &stubGetManager; - if (sMockBinding->createSession == nullptr) { - sMockBinding->createSession = &stubCreateSession; + if (sMockBinding->createSessionInternal == nullptr) { + sMockBinding->createSessionInternal = &stubCreateSessionInternal; } sMockBinding->closeSession = &stubCloseSession; sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration; @@ -163,14 +169,14 @@ void HintSessionWrapperTests::TearDown() { TEST_F(HintSessionWrapperTests, destructorClosesBackgroundSession) { EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = stubSlowCreateSession; + sMockBinding->createSessionInternal = stubSlowCreateSessionInternal; mWrapper->init(); mWrapper = nullptr; Mock::VerifyAndClearExpectations(sMockBinding.get()); } TEST_F(HintSessionWrapperTests, sessionInitializesCorrectly) { - EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1); + EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1); mWrapper->init(); waitForWrapperReady(); } @@ -219,7 +225,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesBeforeAsyncCreationFinish // Here we test whether queueing delayedDestroy works while creation is still happening, if // creation happens after EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = &stubManagedCreateSession; + sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal; // Start creating the session and destroying it at the same time mWrapper->init(); @@ -246,7 +252,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesAfterAsyncCreationFinishe // Here we test whether queueing delayedDestroy works while creation is still happening, if // creation happens before EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); - sMockBinding->createSession = &stubManagedCreateSession; + sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal; // Start creating the session and destroying it at the same time mWrapper->init(); @@ -352,7 +358,7 @@ TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct } TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) { - EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1); + EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1); EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22}))) .Times(1); mWrapper->init(); diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 8a13c034995d..4492c858c084 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -2088,31 +2088,27 @@ static status_t extractInfosFromObject( } return BAD_VALUE; } - size_t offset = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoOffset)); - size_t size = static_cast<size_t>(env->GetIntField(param, gFields.bufferInfoSize)); + ssize_t offset = static_cast<ssize_t>(env->GetIntField(param, gFields.bufferInfoOffset)); + ssize_t size = static_cast<ssize_t>(env->GetIntField(param, gFields.bufferInfoSize)); uint32_t flags = static_cast<uint32_t>(env->GetIntField(param, gFields.bufferInfoFlags)); - if (flags == 0 && size == 0) { - if (errorDetailMsg) { - *errorDetailMsg = "Error: Queuing an empty BufferInfo"; - } - return BAD_VALUE; - } if (i == 0) { *initialOffset = offset; - if (CC_UNLIKELY(*initialOffset < 0)) { - if (errorDetailMsg) { - *errorDetailMsg = "Error: offset/size in BufferInfo"; - } - return BAD_VALUE; - } } - if (CC_UNLIKELY(((ssize_t)(UINT32_MAX - offset) < (ssize_t)size) - || ((offset - *initialOffset) != *totalSize))) { + if (CC_UNLIKELY((offset < 0) + || (size < 0) + || ((INT32_MAX - offset) < size) + || ((offset - (*initialOffset)) != *totalSize))) { if (errorDetailMsg) { *errorDetailMsg = "Error: offset/size in BufferInfo"; } return BAD_VALUE; } + if (flags == 0 && size == 0) { + if (errorDetailMsg) { + *errorDetailMsg = "Error: Queuing an empty BufferInfo"; + } + return BAD_VALUE; + } infos->emplace_back( flags, size, diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 1c203e37c710..346c87daec87 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -366,6 +366,7 @@ LIBANDROID_PLATFORM { APerformanceHint_setIHintManagerForTesting; APerformanceHint_sendHint; APerformanceHint_getThreadIds; + APerformanceHint_createSessionInternal; extern "C++" { ASurfaceControl_registerSurfaceStatsListener*; ASurfaceControl_unregisterSurfaceStatsListener*; diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index fbb35e2bc355..83056b267365 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -463,6 +463,15 @@ APerformanceHintSession* APerformanceHint_createSession(APerformanceHintManager* return manager->createSession(threadIds, size, initialTargetWorkDurationNanos); } +APerformanceHintSession* APerformanceHint_createSessionInternal( + APerformanceHintManager* manager, const int32_t* threadIds, size_t size, + int64_t initialTargetWorkDurationNanos, SessionTag tag) { + VALIDATE_PTR(manager) + VALIDATE_PTR(threadIds) + return manager->createSession(threadIds, size, initialTargetWorkDurationNanos, + static_cast<hal::SessionTag>(tag)); +} + int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) { VALIDATE_PTR(manager) return manager->getPreferredRateNanos(); @@ -486,9 +495,9 @@ void APerformanceHint_closeSession(APerformanceHintSession* session) { delete session; } -int APerformanceHint_sendHint(void* session, SessionHint hint) { +int APerformanceHint_sendHint(APerformanceHintSession* session, SessionHint hint) { VALIDATE_PTR(session) - return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint); + return session->sendHint(hint); } int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds, @@ -498,11 +507,10 @@ int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* t return session->setThreads(threadIds, size); } -int APerformanceHint_getThreadIds(void* aPerformanceHintSession, int32_t* const threadIds, +int APerformanceHint_getThreadIds(APerformanceHintSession* session, int32_t* const threadIds, size_t* const size) { - VALIDATE_PTR(aPerformanceHintSession) - return static_cast<APerformanceHintSession*>(aPerformanceHintSession) - ->getThreadIds(threadIds, size); + VALIDATE_PTR(session) + return session->getThreadIds(threadIds, size); } int APerformanceHint_setPreferPowerEfficiency(APerformanceHintSession* session, bool enabled) { diff --git a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp index 974e6e63b424..58f56b873246 100644 --- a/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp +++ b/native/android/tests/performance_hint/PerformanceHintNativeTest.cpp @@ -30,9 +30,7 @@ #include <memory> #include <vector> -using aidl::android::hardware::power::SessionConfig; -using aidl::android::hardware::power::SessionTag; -using aidl::android::hardware::power::WorkDuration; +namespace hal = aidl::android::hardware::power; using aidl::android::os::IHintManager; using aidl::android::os::IHintSession; using ndk::ScopedAStatus; @@ -45,7 +43,7 @@ class MockIHintManager : public IHintManager { public: MOCK_METHOD(ScopedAStatus, createHintSessionWithConfig, (const SpAIBinder& token, const ::std::vector<int32_t>& tids, int64_t durationNanos, - SessionTag tag, std::optional<SessionConfig>* config, + hal::SessionTag tag, std::optional<hal::SessionConfig>* config, std::shared_ptr<IHintSession>* _aidl_return), (override)); MOCK_METHOD(ScopedAStatus, getHintSessionPreferredRate, (int64_t * _aidl_return), (override)); @@ -71,7 +69,7 @@ public: MOCK_METHOD(ScopedAStatus, setMode, (int32_t mode, bool enabled), (override)); MOCK_METHOD(ScopedAStatus, close, (), (override)); MOCK_METHOD(ScopedAStatus, reportActualWorkDuration2, - (const ::std::vector<WorkDuration>& workDurations), (override)); + (const ::std::vector<hal::WorkDuration>& workDurations), (override)); MOCK_METHOD(SpAIBinder, asBinder, (), (override)); MOCK_METHOD(bool, isRemote, (), (override)); }; @@ -95,7 +93,7 @@ public: return APerformanceHint_getManager(); } APerformanceHintSession* createSession(APerformanceHintManager* manager, - int64_t targetDuration = 56789L) { + int64_t targetDuration = 56789L, bool isHwui = false) { mMockSession = ndk::SharedRefBase::make<NiceMock<MockIHintSession>>(); int64_t sessionId = 123; std::vector<int32_t> tids; @@ -104,8 +102,8 @@ public: ON_CALL(*mMockIHintManager, createHintSessionWithConfig(_, Eq(tids), Eq(targetDuration), _, _, _)) - .WillByDefault(DoAll(SetArgPointee<4>( - std::make_optional<SessionConfig>({.id = sessionId})), + .WillByDefault(DoAll(SetArgPointee<4>(std::make_optional<hal::SessionConfig>( + {.id = sessionId})), SetArgPointee<5>(std::shared_ptr<IHintSession>(mMockSession)), [] { return ScopedAStatus::ok(); })); @@ -124,6 +122,10 @@ public: ON_CALL(*mMockSession, reportActualWorkDuration2(_)).WillByDefault([] { return ScopedAStatus::ok(); }); + if (isHwui) { + return APerformanceHint_createSessionInternal(manager, tids.data(), tids.size(), + targetDuration, SessionTag::HWUI); + } return APerformanceHint_createSession(manager, tids.data(), tids.size(), targetDuration); } @@ -131,7 +133,7 @@ public: std::shared_ptr<NiceMock<MockIHintSession>> mMockSession = nullptr; }; -bool equalsWithoutTimestamp(WorkDuration lhs, WorkDuration rhs) { +bool equalsWithoutTimestamp(hal::WorkDuration lhs, hal::WorkDuration rhs) { return lhs.workPeriodStartTimestampNanos == rhs.workPeriodStartTimestampNanos && lhs.cpuDurationNanos == rhs.cpuDurationNanos && lhs.gpuDurationNanos == rhs.gpuDurationNanos && lhs.durationNanos == rhs.durationNanos; @@ -194,6 +196,16 @@ TEST_F(PerformanceHintTest, TestUpdatedSessionCreation) { APerformanceHint_closeSession(session); } +TEST_F(PerformanceHintTest, TestHwuiSessionCreation) { + EXPECT_CALL(*mMockIHintManager, + createHintSessionWithConfig(_, _, _, hal::SessionTag::HWUI, _, _)) + .Times(1); + APerformanceHintManager* manager = createManager(); + APerformanceHintSession* session = createSession(manager, 56789L, true); + ASSERT_TRUE(session); + APerformanceHint_closeSession(session); +} + TEST_F(PerformanceHintTest, SetThreads) { APerformanceHintManager* manager = createManager(); @@ -249,8 +261,8 @@ MATCHER_P(WorkDurationEq, expected, "") { return false; } for (int i = 0; i < expected.size(); ++i) { - WorkDuration expectedWorkDuration = expected[i]; - WorkDuration actualWorkDuration = arg[i]; + hal::WorkDuration expectedWorkDuration = expected[i]; + hal::WorkDuration actualWorkDuration = arg[i]; if (!equalsWithoutTimestamp(expectedWorkDuration, actualWorkDuration)) { *result_listener << "WorkDuration at [" << i << "] is different: " << "Expected: " << expectedWorkDuration.toString() @@ -273,7 +285,7 @@ TEST_F(PerformanceHintTest, TestAPerformanceHint_reportActualWorkDuration2) { usleep(2); // Sleep for longer than preferredUpdateRateNanos. struct TestPair { - WorkDuration duration; + hal::WorkDuration duration; int expectedResult; }; std::vector<TestPair> testPairs{ @@ -282,7 +294,7 @@ TEST_F(PerformanceHintTest, TestAPerformanceHint_reportActualWorkDuration2) { {{1, -20, 1, 13, -8}, EINVAL}, }; for (auto&& pair : testPairs) { - std::vector<WorkDuration> actualWorkDurations; + std::vector<hal::WorkDuration> actualWorkDurations; actualWorkDurations.push_back(pair.duration); EXPECT_CALL(*mMockSession, reportActualWorkDuration2(WorkDurationEq(actualWorkDurations))) diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig index cddbb6b8954d..8cdef38356da 100644 --- a/packages/CrashRecovery/aconfig/flags.aconfig +++ b/packages/CrashRecovery/aconfig/flags.aconfig @@ -16,3 +16,11 @@ flag { description: "Enables various dependencies of crashrecovery module" bug: "289203818" } + +flag { + name: "allow_rescue_party_flag_resets" + namespace: "crashrecovery" + description: "Enables rescue party flag resets" + bug: "287618292" + is_fixed_read_only: true +} diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index bf69d3ba7603..443747530315 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -23,6 +23,7 @@ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> + <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> diff --git a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml index 77979f0dc7ec..875524775a3c 100644 --- a/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml +++ b/packages/SettingsLib/SettingsTheme/res/drawable-v35/settingslib_arrow_drop_down.xml @@ -16,11 +16,12 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:viewportWidth="18" - android:viewportHeight="18" - android:width="24dp" - android:height="24dp"> + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@color/settingslib_materialColorOnPrimaryContainer"> <path - android:pathData="M7 10l5 5 5 -5z" - android:fillColor="@color/settingslib_materialColorOnPrimaryContainer"/> + android:fillColor="@android:color/white" + android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z"/> </vector> diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt index 8f67354b76f3..7ea4b184eeb6 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/database/ContentChangeFlow.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flowOn /** Content change flow for the given [uri]. */ -internal fun Context.contentChangeFlow( +fun Context.contentChangeFlow( uri: Uri, sendInitial: Boolean = true, ): Flow<Unit> = callbackFlow { diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index a67839a00f5a..73c96d98355b 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -200,6 +200,10 @@ <string name="bluetooth_active_battery_level">Active. <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for untethered headset. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_battery_level_untethered">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the left part of the untethered headset. [CHAR LIMIT=NONE] --> + <string name="bluetooth_active_battery_level_untethered_left">Active. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string> + <!-- Connected devices settings. Message when Bluetooth is connected and active, showing remote device status and battery level for the right part of the untethered headset. [CHAR LIMIT=NONE] --> + <string name="bluetooth_active_battery_level_untethered_right">Active. R: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery</string> <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] --> <string name="bluetooth_battery_level"><xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery</string> <!-- Connected devices settings. Message on TV when Bluetooth is connected but not in use, showing remote device battery level. [CHAR LIMIT=NONE] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java b/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java index 4af9e3c441de..2f6d839f840b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java @@ -106,8 +106,7 @@ final public class AuthenticatorHelper extends BroadcastReceiver { AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType); Context authContext = context.createPackageContextAsUser(desc.packageName, 0, mUserHandle); - icon = mContext.getPackageManager().getUserBadgedIcon( - authContext.getDrawable(desc.iconId), mUserHandle); + icon = authContext.getDrawable(desc.iconId); synchronized (mAccTypeIconCache) { mAccTypeIconCache.put(accountType, icon); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 36a9ecfe99f5..a7b7da598d12 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1476,30 +1476,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } - // Try to show left/right information if can not get it from battery for hearing + // Try to show left/right information for hearing // aids specifically. boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid; boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio && isConnectedHapClientDevice(); if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) { - final Set<CachedBluetoothDevice> memberDevices = getMemberDevice(); - final CachedBluetoothDevice subDevice = getSubDevice(); - if (memberDevices.stream().anyMatch(m -> m.isConnected())) { - stringRes = R.string.bluetooth_hearing_aid_left_and_right_active; - } else if (subDevice != null && subDevice.isConnected()) { - stringRes = R.string.bluetooth_hearing_aid_left_and_right_active; - } else { - int deviceSide = getDeviceSide(); - if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { - stringRes = R.string.bluetooth_hearing_aid_left_and_right_active; - } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) { - stringRes = R.string.bluetooth_hearing_aid_left_active; - } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) { - stringRes = R.string.bluetooth_hearing_aid_right_active; - } else { - stringRes = R.string.bluetooth_active_no_battery_level; - } - } + return getHearingDeviceSummary(leftBattery, rightBattery, shortSummary); } } } @@ -1567,6 +1550,62 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return spannableBuilder; } + private CharSequence getHearingDeviceSummary(int leftBattery, int rightBattery, + boolean shortSummary) { + + CachedBluetoothDevice memberDevice = getMemberDevice().stream().filter( + CachedBluetoothDevice::isConnected).findFirst().orElse(null); + if (memberDevice == null && mSubDevice != null && mSubDevice.isConnected()) { + memberDevice = mSubDevice; + } + + CachedBluetoothDevice leftDevice = null; + CachedBluetoothDevice rightDevice = null; + final int deviceSide = getDeviceSide(); + if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT) { + leftDevice = this; + rightDevice = memberDevice; + } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_RIGHT) { + leftDevice = memberDevice; + rightDevice = this; + } else if (deviceSide == HearingAidInfo.DeviceSide.SIDE_LEFT_AND_RIGHT) { + leftDevice = this; + rightDevice = this; + } + + if (leftBattery < 0 && leftDevice != null) { + leftBattery = leftDevice.getBatteryLevel(); + } + if (rightBattery < 0 && rightDevice != null) { + rightBattery = rightDevice.getBatteryLevel(); + } + + if (leftDevice != null && rightDevice != null) { + if (leftBattery >= 0 && rightBattery >= 0 && !shortSummary) { + return mContext.getString(R.string.bluetooth_active_battery_level_untethered, + Utils.formatPercentage(leftBattery), Utils.formatPercentage(rightBattery)); + } else { + return mContext.getString(R.string.bluetooth_hearing_aid_left_and_right_active); + } + } else if (leftDevice != null) { + if (leftBattery >= 0 && !shortSummary) { + return mContext.getString(R.string.bluetooth_active_battery_level_untethered_left, + Utils.formatPercentage(leftBattery)); + } else { + return mContext.getString(R.string.bluetooth_hearing_aid_left_active); + } + } else if (rightDevice != null) { + if (rightBattery >= 0 && !shortSummary) { + return mContext.getString(R.string.bluetooth_active_battery_level_untethered_right, + Utils.formatPercentage(rightBattery)); + } else { + return mContext.getString(R.string.bluetooth_hearing_aid_right_active); + } + } + + return mContext.getString(R.string.bluetooth_active_no_battery_level); + } + private void addBatterySpan(SpannableStringBuilder builder, String batteryString, boolean lowBattery, int lowBatteryColorRes) { if (lowBattery && lowBatteryColorRes != SUMMARY_NO_COLOR_FOR_LOW_BATTERY) { diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 892d90757fca..6174ab982719 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -169,7 +169,6 @@ android_library { "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", - "androidx.test.rules", ], libs: [ "keepanno-annotations", @@ -426,6 +425,7 @@ android_app { kotlincflags: ["-Xjvm-default=all"], optimize: { shrink_resources: false, + optimized_shrink_resources: false, proguard_flags_files: ["proguard.flags"], }, @@ -515,6 +515,7 @@ systemui_optimized_java_defaults { optimize: true, shrink: true, shrink_resources: true, + optimized_shrink_resources: true, ignore_warnings: false, proguard_compatibility: false, }, diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 561c85e8b61a..5cc3cafd4aa1 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -1036,6 +1036,7 @@ <receiver android:name=".statusbar.KeyboardShortcutsReceiver" + android:visibleToInstantApps="true" android:exported="true"> <intent-filter> <action android:name="com.android.intent.action.DISMISS_KEYBOARD_SHORTCUTS" /> @@ -1116,5 +1117,11 @@ android:name="android.service.dream" android:resource="@xml/home_controls_dream_metadata" /> </service> + + <activity android:name="com.android.systemui.keyboard.shortcut.ShortcutHelperActivity" + android:exported="false" + android:theme="@style/ShortcutHelperTheme" + android:excludeFromRecents="true" + android:finishOnCloseSystemDialogs="true" /> </application> </manifest> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 16bc7f2dae5e..e38f347c4093 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -776,6 +776,13 @@ flag { } flag { + name: "keyboard_shortcut_helper_rewrite" + namespace: "systemui" + description: "A new implementation of the keyboards shortcuts helper sheet." + bug: "327364197" +} + +flag { name: "dream_overlay_bouncer_swipe_direction_filtering" namespace: "systemui" description: "do not initiate bouncer swipe when the direction is opposite of the expansion" @@ -814,3 +821,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enforce_brightness_base_user_restriction" + namespace: "systemui" + description: "Enforce BaseUserRestriction for DISALLOW_CONFIG_BRIGHTNESS." + bug: "329205638" +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index f1f1b575e818..f539a23d9cb0 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -97,6 +97,8 @@ class AnimatableClockView @JvmOverloads constructor( @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb -> TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) } + + // Used by screenshot tests to provide stability @VisibleForTesting var isAnimationEnabled: Boolean = true @VisibleForTesting var timeOverrideInMillis: Long? = null @@ -242,15 +244,8 @@ class AnimatableClockView @JvmOverloads constructor( } logger.d({ "onDraw($str1)"}) { str1 = text.toString() } - // Use textAnimator to render text if animation is enabled. - // Otherwise default to using standard draw functions. - if (isAnimationEnabled) { - // intentionally doesn't call super.onDraw here or else the text will be rendered twice - textAnimator?.draw(canvas) - } else { - super.onDraw(canvas) - } - + // intentionally doesn't call super.onDraw here or else the text will be rendered twice + textAnimator?.draw(canvas) canvas.restore() } @@ -318,7 +313,7 @@ class AnimatableClockView @JvmOverloads constructor( weight = lockScreenWeight, textSize = -1f, color = lockScreenColor, - animate = isAnimationEnabled, + animate = true, duration = APPEAR_ANIM_DURATION, interpolator = Interpolators.EMPHASIZED_DECELERATE, delay = 0, @@ -327,7 +322,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateFoldAppear(animate: Boolean = true) { - if (isAnimationEnabled && textAnimator == null) { + if (textAnimator == null) { return } logger.d("animateFoldAppear") @@ -344,7 +339,7 @@ class AnimatableClockView @JvmOverloads constructor( weight = dozingWeightInternal, textSize = -1f, color = dozingColor, - animate = animate && isAnimationEnabled, + animate = animate, interpolator = Interpolators.EMPHASIZED_DECELERATE, duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(), delay = 0, @@ -363,7 +358,7 @@ class AnimatableClockView @JvmOverloads constructor( weight = if (isDozing()) dozingWeight else lockScreenWeight, textSize = -1f, color = null, - animate = isAnimationEnabled, + animate = true, duration = CHARGE_ANIM_DURATION_PHASE_1, delay = 0, onAnimationEnd = null @@ -373,7 +368,7 @@ class AnimatableClockView @JvmOverloads constructor( weight = if (isDozing()) lockScreenWeight else dozingWeight, textSize = -1f, color = null, - animate = isAnimationEnabled, + animate = true, duration = CHARGE_ANIM_DURATION_PHASE_0, delay = chargeAnimationDelay.toLong(), onAnimationEnd = startAnimPhase2 @@ -386,7 +381,7 @@ class AnimatableClockView @JvmOverloads constructor( weight = if (isDozing) dozingWeight else lockScreenWeight, textSize = -1f, color = if (isDozing) dozingColor else lockScreenColor, - animate = animate && isAnimationEnabled, + animate = animate, duration = DOZE_ANIM_DURATION, delay = 0, onAnimationEnd = null @@ -445,9 +440,6 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter - if (color != null && !isAnimationEnabled) { - setTextColor(color) - } } else { // when the text animator is set, update its start values onTextAnimatorInitialized = Runnable { @@ -462,9 +454,6 @@ class AnimatableClockView @JvmOverloads constructor( onAnimationEnd = onAnimationEnd ) textAnimator?.glyphFilter = glyphFilter - if (color != null && !isAnimationEnabled) { - setTextColor(color) - } } } } @@ -482,7 +471,7 @@ class AnimatableClockView @JvmOverloads constructor( weight = weight, textSize = textSize, color = color, - animate = animate && isAnimationEnabled, + animate = animate, interpolator = null, duration = duration, delay = delay, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java index 04c4efbf7c78..fefe5a011358 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java @@ -149,7 +149,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { mUiEventLogger); when(mScrimManager.getCurrentController()).thenReturn(mScrimController); - when(mCentralSurfaces.isBouncerShowing()).thenReturn(false); when(mValueAnimatorCreator.create(anyFloat(), anyFloat())).thenReturn(mValueAnimator); when(mVelocityTrackerFactory.obtain()).thenReturn(mVelocityTracker); when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn(Float.MAX_VALUE); @@ -193,11 +192,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { 2)).isTrue(); } - private enum Direction { - DOWN, - UP, - } - @Test public void testSwipeUp_whenBouncerInitiallyShowing_reduceHeightWithExclusionRects() { mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, mRegion, @@ -210,7 +204,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { SCREEN_HEIGHT_PX * MIN_BOUNCER_HEIGHT; final int minAllowableBottom = SCREEN_HEIGHT_PX - Math.round(minBouncerHeight); - expected.set(0, minAllowableBottom , SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX); + expected.set(0, minAllowableBottom, SCREEN_WIDTH_PX, SCREEN_HEIGHT_PX); assertThat(bounds).isEqualTo(expected); @@ -278,69 +272,11 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { } /** - * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount. - */ - @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING) - @Test - public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); - - final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - - final float percent = .3f; - final float distanceY = SCREEN_HEIGHT_PX * percent; - - // Swiping up near the top of the screen where the touch initiation region is. - final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, distanceY, 0); - final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, 0, 0); - - assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isTrue(); - - verify(mScrimController, never()).expand(any()); - } - - /** - * Makes sure swiping up when bouncer initially showing doesn't change the expansion amount. - */ - @Test - @EnableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING) - public void testSwipeUp_whenBouncerInitiallyShowing_doesNotSetExpansion_directionFiltering() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); - - final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - - final float percent = .3f; - final float distanceY = SCREEN_HEIGHT_PX * percent; - - // Swiping up near the top of the screen where the touch initiation region is. - final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, distanceY, 0); - final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, 0, 0); - - assertThat(gestureListener.onScroll(event1, event2, 0, distanceY)).isFalse(); - - verify(mScrimController, never()).expand(any()); - } - - /** - * Makes sure swiping down when bouncer initially hidden doesn't change the expansion amount. + * Makes sure swiping down doesn't change the expansion amount. */ @Test @DisableFlags(Flags.FLAG_DREAM_OVERLAY_BOUNCER_SWIPE_DIRECTION_FILTERING) - public void testSwipeDown_whenBouncerInitiallyHidden_doesNotSetExpansion() { + public void testSwipeDown_doesNotSetExpansion() { mTouchHandler.onSessionStart(mTouchSession); ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); @@ -401,34 +337,8 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - verifyScroll(.3f, Direction.UP, false, gestureListener); - - // Ensure that subsequent gestures are treated as expanding even if the bouncer state - // changes. - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - verifyScroll(.7f, Direction.UP, false, gestureListener); - } - - /** - * Makes sure the expansion amount is proportional to scroll. - */ - @Test - public void testSwipeDown_setsCorrectExpansionAmount() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - mTouchHandler.onSessionStart(mTouchSession); - ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); - verify(mTouchSession).registerGestureListener(gestureListenerCaptor.capture()); - - final OnGestureListener gestureListener = gestureListenerCaptor.getValue(); - - verifyScroll(.3f, Direction.DOWN, true, gestureListener); - - // Ensure that subsequent gestures are treated as collapsing even if the bouncer state - // changes. - when(mCentralSurfaces.isBouncerShowing()).thenReturn(false); - verifyScroll(.7f, Direction.DOWN, true, gestureListener); + verifyScroll(.3f, gestureListener); + verifyScroll(.7f, gestureListener); } /** @@ -493,25 +403,24 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { verify(mCentralSurfaces, never()).awakenDreams(); } - private void verifyScroll(float percent, Direction direction, - boolean isBouncerInitiallyShowing, GestureDetector.OnGestureListener gestureListener) { + private void verifyScroll(float percent, + OnGestureListener gestureListener) { final float distanceY = SCREEN_HEIGHT_PX * percent; final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0); + 0, SCREEN_HEIGHT_PX, 0); final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0); + 0, SCREEN_HEIGHT_PX - distanceY, 0); reset(mScrimController); assertThat(gestureListener.onScroll(event1, event2, 0, - direction == Direction.UP ? distanceY : -distanceY)) + distanceY)) .isTrue(); // Ensure only called once verify(mScrimController).expand(any()); - final float expansion = isBouncerInitiallyShowing ? percent : 1 - percent; - final float dragDownAmount = event2.getY() - event1.getY(); + final float expansion = 1 - percent; // Ensure correct expansion passed in. ShadeExpansionChangeEvent event = @@ -529,7 +438,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float expansion = 1 - swipeUpPercentage; // The upward velocity is ignored. final float velocityY = -1; - swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); + swipeToPosition(swipeUpPercentage, velocityY); verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncerConstants.EXPANSION_HIDDEN)); @@ -552,7 +461,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float expansion = 1 - swipeUpPercentage; // The downward velocity is ignored. final float velocityY = 1; - swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); + swipeToPosition(swipeUpPercentage, velocityY); verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); @@ -573,57 +482,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { } /** - * Tests that ending a downward swipe above the set threshold will continue the expansion, - * but will not trigger logging of the DREAM_SWIPED event. - */ - @Test - public void testSwipeDownPositionAboveThreshold_expandsBouncer_doesNotLog() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - - final float swipeDownPercentage = .3f; - // The downward velocity is ignored. - final float velocityY = 1; - swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY); - - verify(mValueAnimatorCreator).create(eq(swipeDownPercentage), - eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); - verify(mValueAnimator, never()).addListener(any()); - - verify(mFlingAnimationUtils).apply(eq(mValueAnimator), - eq(SCREEN_HEIGHT_PX * swipeDownPercentage), - eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_VISIBLE), - eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); - verify(mValueAnimator).start(); - verify(mUiEventLogger, never()).log(any()); - } - - /** - * Tests that swiping down with a speed above the set threshold leads to bouncer collapsing - * down. - */ - @Test - public void testSwipeDownVelocityAboveMin_collapsesBouncer() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - when(mFlingAnimationUtils.getMinVelocityPxPerSecond()).thenReturn((float) 0); - - // The ending position above the set threshold is ignored. - final float swipeDownPercentage = .3f; - final float velocityY = 1; - swipeToPosition(swipeDownPercentage, Direction.DOWN, velocityY); - - verify(mValueAnimatorCreator).create(eq(swipeDownPercentage), - eq(KeyguardBouncerConstants.EXPANSION_HIDDEN)); - verify(mValueAnimator, never()).addListener(any()); - - verify(mFlingAnimationUtilsClosing).apply(eq(mValueAnimator), - eq(SCREEN_HEIGHT_PX * swipeDownPercentage), - eq(SCREEN_HEIGHT_PX * KeyguardBouncerConstants.EXPANSION_HIDDEN), - eq(velocityY), eq((float) SCREEN_HEIGHT_PX)); - verify(mValueAnimator).start(); - verify(mUiEventLogger, never()).log(any()); - } - - /** * Tests that swiping up with a speed above the set threshold will continue the expansion. */ @Test @@ -634,7 +492,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float swipeUpPercentage = .3f; final float expansion = 1 - swipeUpPercentage; final float velocityY = -1; - swipeToPosition(swipeUpPercentage, Direction.UP, velocityY); + swipeToPosition(swipeUpPercentage, velocityY); verify(mValueAnimatorCreator).create(eq(expansion), eq(KeyguardBouncerConstants.EXPANSION_VISIBLE)); @@ -654,26 +512,6 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { verify(mUiEventLogger).log(BouncerSwipeTouchHandler.DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE); } - /** - * Ensures {@link CentralSurfaces} - */ - @Test - public void testInformBouncerShowingOnExpand() { - swipeToPosition(1f, Direction.UP, 0); - } - - /** - * Ensures {@link CentralSurfaces} - */ - @Test - public void testInformBouncerHidingOnCollapse() { - // Must swipe up to set initial state. - swipeToPosition(1f, Direction.UP, 0); - Mockito.clearInvocations(mCentralSurfaces); - - swipeToPosition(0f, Direction.DOWN, 0); - } - @Test public void testTouchSessionOnRemovedCalledTwice() { mTouchHandler.onSessionStart(mTouchSession); @@ -684,7 +522,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { onRemovedCallbackCaptor.getValue().onRemoved(); } - private void swipeToPosition(float percent, Direction direction, float velocityY) { + private void swipeToPosition(float percent, float velocityY) { Mockito.clearInvocations(mTouchSession); mTouchHandler.onSessionStart(mTouchSession); ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerCaptor = @@ -699,12 +537,12 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase { final float distanceY = SCREEN_HEIGHT_PX * percent; final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX : 0, 0); + 0, SCREEN_HEIGHT_PX, 0); final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, - 0, direction == Direction.UP ? SCREEN_HEIGHT_PX - distanceY : distanceY, 0); + 0, SCREEN_HEIGHT_PX - distanceY, 0); assertThat(gestureListenerCaptor.getValue().onScroll(event1, event2, 0, - direction == Direction.UP ? distanceY : -distanceY)) + distanceY)) .isTrue(); final MotionEvent upEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java index 27bffd0818e7..11a42413c4ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.java @@ -18,8 +18,10 @@ package com.android.systemui.ambient.touch; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.view.GestureDetector; import android.view.MotionEvent; @@ -28,7 +30,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.system.InputChannelCompat; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -36,6 +37,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -49,66 +51,89 @@ public class ShadeTouchHandlerTest extends SysuiTestCase { CentralSurfaces mCentralSurfaces; @Mock - ShadeViewController mShadeViewController; - - @Mock TouchHandler.TouchSession mTouchSession; ShadeTouchHandler mTouchHandler; + @Captor + ArgumentCaptor<GestureDetector.OnGestureListener> mGestureListenerCaptor; + @Captor + ArgumentCaptor<InputChannelCompat.InputEventListener> mInputListenerCaptor; + private static final int TOUCH_HEIGHT = 20; @Before public void setup() { MockitoAnnotations.initMocks(this); - mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController, - TOUCH_HEIGHT); + + mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), TOUCH_HEIGHT); } - /** - * Verify that touches aren't handled when the bouncer is showing. - */ + // Verifies that a swipe down in the gesture region is captured by the shade touch handler. @Test - public void testInactiveOnBouncer() { - when(mCentralSurfaces.isBouncerShowing()).thenReturn(true); - mTouchHandler.onSessionStart(mTouchSession); - verify(mTouchSession).pop(); + public void testSwipeDown_captured() { + final boolean captured = swipe(Direction.DOWN); + + assertThat(captured).isTrue(); } - /** - * Make sure {@link ShadeTouchHandler} - */ + // Verifies that a swipe in the upward direction is not catpured. @Test - public void testTouchPilferingOnScroll() { - final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class); - final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class); + public void testSwipeUp_notCaptured() { + final boolean captured = swipe(Direction.UP); - final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor = - ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + // Motion events not captured as the swipe is going in the wrong direction. + assertThat(captured).isFalse(); + } - mTouchHandler.onSessionStart(mTouchSession); - verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture()); + // Verifies that a swipe down forwards captured touches to the shade window for handling. + @Test + public void testSwipeDown_sentToShadeWindow() { + swipe(Direction.DOWN); - assertThat(gestureListenerArgumentCaptor.getValue() - .onScroll(motionEvent1, motionEvent2, 1, 1)) - .isTrue(); + // Both motion events are sent for the shade window to process. + verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any()); } - /** - * Ensure touches are propagated to the {@link ShadeViewController}. - */ + // Verifies that a swipe down is not forwarded to the shade window. @Test - public void testEventPropagation() { - final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); + public void testSwipeUp_touchesNotSent() { + swipe(Direction.UP); - final ArgumentCaptor<InputChannelCompat.InputEventListener> - inputEventListenerArgumentCaptor = - ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + // Motion events are not sent for the shade window to process as the swipe is going in the + // wrong direction. + verify(mCentralSurfaces, never()).handleExternalShadeWindowTouch(any()); + } + /** + * Simulates a swipe in the given direction and returns true if the touch was intercepted by the + * touch handler's gesture listener. + * <p> + * Swipe down starts from a Y coordinate of 0 and goes downward. Swipe up starts from the edge + * of the gesture region, {@link #TOUCH_HEIGHT}, and goes upward to 0. + */ + private boolean swipe(Direction direction) { + Mockito.clearInvocations(mTouchSession); mTouchHandler.onSessionStart(mTouchSession); - verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); - inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); - verify(mShadeViewController).handleExternalTouch(motionEvent); + + verify(mTouchSession).registerGestureListener(mGestureListenerCaptor.capture()); + verify(mTouchSession).registerInputListener(mInputListenerCaptor.capture()); + + final float startY = direction == Direction.UP ? TOUCH_HEIGHT : 0; + final float endY = direction == Direction.UP ? 0 : TOUCH_HEIGHT; + + // Send touches to the input and gesture listener. + final MotionEvent event1 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, startY, 0); + final MotionEvent event2 = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, endY, 0); + mInputListenerCaptor.getValue().onInputEvent(event1); + mInputListenerCaptor.getValue().onInputEvent(event2); + final boolean captured = mGestureListenerCaptor.getValue().onScroll(event1, event2, 0, + startY - endY); + + return captured; } + private enum Direction { + DOWN, UP, + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt new file mode 100644 index 000000000000..722eb2b9b622 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalBackupRestoreStartableTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.Intent +import android.content.mockedContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.FakeBroadcastDispatcher +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.widgets.CommunalWidgetModule +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalBackupRestoreStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + @Mock private lateinit var communalInteractor: CommunalInteractor + + private val mapCaptor = kotlinArgumentCaptor<Map<Int, Int>>() + + private lateinit var context: Context + private lateinit var broadcastDispatcher: FakeBroadcastDispatcher + private lateinit var underTest: CommunalBackupRestoreStartable + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + context = kosmos.mockedContext + broadcastDispatcher = kosmos.broadcastDispatcher + + underTest = + CommunalBackupRestoreStartable( + broadcastDispatcher, + communalInteractor, + logcatLogBuffer("CommunalBackupRestoreStartable"), + ) + } + + @Test + fun testRestoreWidgetsUponHostRestored() = + testScope.runTest { + underTest.start() + + // Verify restore widgets not called + verify(communalInteractor, never()).restoreWidgets(any()) + + // Trigger app widget host restored + val intent = + Intent().apply { + action = AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED + putExtra( + AppWidgetManager.EXTRA_HOST_ID, + CommunalWidgetModule.APP_WIDGET_HOST_ID + ) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS, intArrayOf(1, 2, 3)) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(7, 8, 9)) + } + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + // Verify restore widgets called + verify(communalInteractor).restoreWidgets(mapCaptor.capture()) + val oldToNewWidgetIdMap = mapCaptor.value + assertThat(oldToNewWidgetIdMap) + .containsExactlyEntriesIn( + mapOf( + Pair(1, 7), + Pair(2, 8), + Pair(3, 9), + ) + ) + } + + @Test + fun testDoNotRestoreWidgetsIfNotForCommunalWidgetHost() = + testScope.runTest { + underTest.start() + + // Trigger app widget host restored, but for another host + val hostId = CommunalWidgetModule.APP_WIDGET_HOST_ID + 1 + val intent = + Intent().apply { + action = AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED + putExtra(AppWidgetManager.EXTRA_HOST_ID, hostId) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS, intArrayOf(1, 2, 3)) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(7, 8, 9)) + } + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + // Verify restore widgets not called + verify(communalInteractor, never()).restoreWidgets(any()) + } + + @Test + fun testAbortRestoreWidgetsIfOldToNewIdsMappingInvalid() = + testScope.runTest { + underTest.start() + + // Trigger app widget host restored, but new ids list is one too many for old ids + val oldIds = intArrayOf(1, 2, 3) + val newIds = intArrayOf(6, 7, 8, 9) + val intent = + Intent().apply { + action = AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED + putExtra( + AppWidgetManager.EXTRA_HOST_ID, + CommunalWidgetModule.APP_WIDGET_HOST_ID + ) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS, oldIds) + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, newIds) + } + broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + + // Verify restore widgets aborted + verify(communalInteractor).abortRestoreWidgets() + verify(communalInteractor, never()).restoreWidgets(any()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt index 76f15d2ed257..b4b812d60a1a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt @@ -32,7 +32,9 @@ 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.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.notificationShadeWindowController import com.android.systemui.testKosmos import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat @@ -68,8 +70,10 @@ class CommunalSceneStartableTest : SysuiTestCase() { keyguardTransitionInteractor = keyguardTransitionInteractor, keyguardInteractor = keyguardInteractor, systemSettings = fakeSettings, + notificationShadeWindowController = notificationShadeWindowController, applicationScope = applicationCoroutineScope, bgScope = applicationCoroutineScope, + mainDispatcher = testDispatcher, ) .apply { start() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index a4c7abdbfc19..fe4d32d88612 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -16,18 +16,23 @@ package com.android.systemui.communal.data.repository +import android.app.backup.BackupManager import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName +import android.content.applicationContext import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem +import com.android.systemui.communal.nano.CommunalHubState +import com.android.systemui.communal.proto.toByteArray import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.CommunalWidgetHost @@ -42,6 +47,7 @@ import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -68,7 +74,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var providerInfoA: AppWidgetProviderInfo @Mock private lateinit var communalWidgetHost: CommunalWidgetHost @Mock private lateinit var communalWidgetDao: CommunalWidgetDao + @Mock private lateinit var backupManager: BackupManager + private lateinit var backupUtils: CommunalBackupUtils private lateinit var logBuffer: LogBuffer private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>> @@ -89,6 +97,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) fakeWidgets = MutableStateFlow(emptyMap()) logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") + backupUtils = CommunalBackupUtils(kosmos.applicationContext) setAppWidgetIds(emptyList()) @@ -106,6 +115,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { communalWidgetHost, communalWidgetDao, logBuffer, + backupManager, + backupUtils, ) } @@ -129,6 +140,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { priority = communalItemRankEntry.rank, ) ) + + // Verify backup not requested + verify(backupManager, never()).dataChanged() } @Test @@ -152,6 +166,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) verify(communalWidgetDao).addWidget(id, provider, priority) + + // Verify backup requested + verify(backupManager).dataChanged() } @Test @@ -176,6 +193,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) verify(communalWidgetDao, never()).addWidget(id, provider, priority) verify(appWidgetHost).deleteAppWidgetId(id) + + // Verify backup not requested + verify(backupManager, never()).dataChanged() } @Test @@ -202,6 +222,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) verify(communalWidgetDao, never()).addWidget(id, provider, priority) verify(appWidgetHost).deleteAppWidgetId(id) + + // Verify backup not requested + verify(backupManager, never()).dataChanged() } @Test @@ -225,10 +248,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetHost).allocateIdAndBindWidget(provider, user) verify(communalWidgetDao).addWidget(id, provider, priority) + + // Verify backup requested + verify(backupManager).dataChanged() } @Test - fun deleteWidget_deletefromDbTrue_alsoDeleteFromHost() = + fun deleteWidget_deleteFromDbTrue_alsoDeleteFromHost() = testScope.runTest { val id = 1 whenever(communalWidgetDao.deleteWidgetById(eq(id))).thenReturn(true) @@ -237,10 +263,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetDao).deleteWidgetById(id) verify(appWidgetHost).deleteAppWidgetId(id) + + // Verify backup requested + verify(backupManager).dataChanged() } @Test - fun deleteWidget_deletefromDbFalse_doesNotDeleteFromHost() = + fun deleteWidget_deleteFromDbFalse_doesNotDeleteFromHost() = testScope.runTest { val id = 1 whenever(communalWidgetDao.deleteWidgetById(eq(id))).thenReturn(false) @@ -249,6 +278,9 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetDao).deleteWidgetById(id) verify(appWidgetHost, never()).deleteAppWidgetId(id) + + // Verify backup not requested + verify(backupManager, never()).dataChanged() } @Test @@ -259,6 +291,147 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { runCurrent() verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) + + // Verify backup requested + verify(backupManager).dataChanged() + } + + @Test + fun restoreWidgets_deleteStateFileIfRestoreFails() = + testScope.runTest { + // Write a state file that is invalid, and verify it is written + backupUtils.writeBytesToDisk(byteArrayOf(1, 2, 3, 4, 5, 6)) + assertThat(backupUtils.fileExists()).isTrue() + + // Try to restore widgets + underTest.restoreWidgets(emptyMap()) + runCurrent() + + // The restore should fail, and verify that the file is deleted + assertThat(backupUtils.fileExists()).isFalse() + } + + @Test + fun restoreWidgets_deleteStateFileAfterWidgetsRestored() = + testScope.runTest { + // Write a state file, and verify it is written + backupUtils.writeBytesToDisk(fakeState.toByteArray()) + assertThat(backupUtils.fileExists()).isTrue() + + // Set up app widget host with widget ids + setAppWidgetIds(listOf(11, 12)) + + // Restore widgets + underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12))) + runCurrent() + + // Verify state restored + verify(communalWidgetDao).restoreCommunalHubState(any()) + + // Verify state file deleted + assertThat(backupUtils.fileExists()).isFalse() + } + + @Test + fun restoreWidgets_restoredWidgetsNotRegisteredWithHostAreSkipped() = + testScope.runTest { + // Write fake state to file + backupUtils.writeBytesToDisk(fakeState.toByteArray()) + + // Set up app widget host with widget ids. Widget 12 (previously 2) is absent. + setAppWidgetIds(listOf(11)) + + // Restore widgets. + underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12))) + runCurrent() + + // Verify state restored, and widget 2 skipped + val restoredState = + withArgCaptor<CommunalHubState> { + verify(communalWidgetDao).restoreCommunalHubState(capture()) + } + val restoredWidgets = restoredState.widgets.toList() + assertThat(restoredWidgets).hasSize(1) + + val restoredWidget = restoredWidgets.first() + val expectedWidget = fakeState.widgets.first() + + // Verify widget id is updated, and the rest remain the same as expected + assertThat(restoredWidget.widgetId).isEqualTo(11) + assertThat(restoredWidget.componentName).isEqualTo(expectedWidget.componentName) + assertThat(restoredWidget.rank).isEqualTo(expectedWidget.rank) + } + + @Test + fun restoreWidgets_registeredWidgetsNotRestoredAreRemoved() = + testScope.runTest { + // Write fake state to file + backupUtils.writeBytesToDisk(fakeState.toByteArray()) + + // Set up app widget host with widget ids. Widget 13 will not be restored. + setAppWidgetIds(listOf(11, 12, 13)) + + // Restore widgets. + underTest.restoreWidgets(mapOf(Pair(1, 11), Pair(2, 12))) + runCurrent() + + // Verify widget 1 and 2 are restored, and are now 11 and 12. + val restoredState = + withArgCaptor<CommunalHubState> { + verify(communalWidgetDao).restoreCommunalHubState(capture()) + } + val restoredWidgets = restoredState.widgets.toList() + assertThat(restoredWidgets).hasSize(2) + + val restoredWidget1 = restoredWidgets[0] + val expectedWidget1 = fakeState.widgets[0] + assertThat(restoredWidget1.widgetId).isEqualTo(11) + assertThat(restoredWidget1.componentName).isEqualTo(expectedWidget1.componentName) + assertThat(restoredWidget1.rank).isEqualTo(expectedWidget1.rank) + + val restoredWidget2 = restoredWidgets[1] + val expectedWidget2 = fakeState.widgets[1] + assertThat(restoredWidget2.widgetId).isEqualTo(12) + assertThat(restoredWidget2.componentName).isEqualTo(expectedWidget2.componentName) + assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank) + + // Verify widget 13 removed since it is not restored + verify(appWidgetHost).deleteAppWidgetId(13) + } + + @Test + fun restoreWidgets_onlySomeWidgetsGotNewIds() = + testScope.runTest { + // Write fake state to file + backupUtils.writeBytesToDisk(fakeState.toByteArray()) + + // Set up app widget host with widget ids. Widget 2 gets a new id: 12, but widget 1 does + // not. + setAppWidgetIds(listOf(1, 12)) + + // Restore widgets. + underTest.restoreWidgets(mapOf(Pair(2, 12))) + runCurrent() + + // Verify widget 1 and 2 are restored, and are now 1 and 12. + val restoredState = + withArgCaptor<CommunalHubState> { + verify(communalWidgetDao).restoreCommunalHubState(capture()) + } + val restoredWidgets = restoredState.widgets.toList() + assertThat(restoredWidgets).hasSize(2) + + val restoredWidget1 = restoredWidgets[0] + val expectedWidget1 = fakeState.widgets[0] + assertThat(restoredWidget1.widgetId).isEqualTo(1) + assertThat(restoredWidget1.componentName).isEqualTo(expectedWidget1.componentName) + assertThat(restoredWidget1.rank).isEqualTo(expectedWidget1.rank) + + val restoredWidget2 = restoredWidgets[1] + val expectedWidget2 = fakeState.widgets[1] + assertThat(restoredWidget2.widgetId).isEqualTo(12) + assertThat(restoredWidget2.componentName).isEqualTo(expectedWidget2.componentName) + assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank) } private fun installedProviders(providers: List<AppWidgetProviderInfo>) { @@ -278,5 +451,22 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { widgetFeatures = WIDGET_FEATURE_CONFIGURATION_OPTIONAL or WIDGET_FEATURE_RECONFIGURABLE } + val fakeState = + CommunalHubState().apply { + widgets = + listOf( + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 1 + componentName = "pk_name/fake_widget_1" + rank = 1 + }, + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 2 + componentName = "pk_name/fake_widget_2" + rank = 2 + }, + ) + .toTypedArray() + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java index 41bc1dc271f1..e2e5169db029 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.dreams; +import static kotlinx.coroutines.flow.FlowKt.emptyFlow; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; @@ -28,6 +30,7 @@ import static org.mockito.Mockito.when; import android.content.res.Resources; import android.graphics.Region; import android.os.Handler; +import android.testing.TestableLooper.RunWithLooper; import android.view.AttachedSurfaceControl; import android.view.ViewGroup; import android.view.ViewRootImpl; @@ -43,6 +46,7 @@ import com.android.systemui.ambient.touch.scrim.BouncerlessScrimController; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback; import com.android.systemui.complication.ComplicationHostViewController; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.statusbar.BlurUtils; import org.junit.Before; @@ -52,8 +56,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import kotlinx.coroutines.CoroutineDispatcher; + @SmallTest @RunWith(AndroidJUnit4.class) +@RunWithLooper(setAsMainLooper = true) public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { private static final int MAX_BURN_IN_OFFSET = 20; private static final long BURN_IN_PROTECTION_UPDATE_INTERVAL = 10; @@ -87,6 +94,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { Handler mHandler; @Mock + CoroutineDispatcher mDispatcher; + + @Mock BlurUtils mBlurUtils; @Mock @@ -103,6 +113,8 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { @Mock DreamOverlayStateController mStateController; + @Mock + KeyguardTransitionInteractor mKeyguardTransitionInteractor; DreamOverlayContainerViewController mController; @@ -115,6 +127,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot); when(mDreamOverlayContainerView.getRootSurfaceControl()) .thenReturn(mAttachedSurfaceControl); + when(mKeyguardTransitionInteractor.isFinishedInStateWhere(any())).thenReturn(emptyFlow()); mController = new DreamOverlayContainerViewController( mDreamOverlayContainerView, @@ -124,6 +137,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mLowLightTransitionCoordinator, mBlurUtils, mHandler, + mDispatcher, mResources, MAX_BURN_IN_OFFSET, BURN_IN_PROTECTION_UPDATE_INTERVAL, @@ -131,7 +145,8 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase { mPrimaryBouncerCallbackInteractor, mAnimationsController, mStateController, - mBouncerlessScrimController); + mBouncerlessScrimController, + mKeyguardTransitionInteractor); } @Test 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 b18a8ecee946..bdb0c9aeb6ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -17,35 +17,52 @@ package com.android.systemui.dreams import android.content.ComponentName import android.content.Intent -import android.os.RemoteException import android.service.dreams.IDreamOverlay import android.service.dreams.IDreamOverlayCallback import android.service.dreams.IDreamOverlayClient import android.service.dreams.IDreamOverlayClientCallback +import android.testing.TestableLooper import android.view.View import android.view.ViewGroup import android.view.WindowManager 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.compose.animation.scene.ObservableTransitionState import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent +import com.android.systemui.ambient.touch.scrim.ScrimController +import com.android.systemui.ambient.touch.scrim.ScrimManager +import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.complication.ComplicationHostViewController 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.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.android.systemui.touch.TouchInsetManager import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -53,20 +70,24 @@ import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.invocation.InvocationOnMock +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class DreamOverlayServiceTest : SysuiTestCase() { private val mFakeSystemClock = FakeSystemClock() private val mMainExecutor = FakeExecutor(mFakeSystemClock) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope @Mock lateinit var mLifecycleOwner: DreamOverlayLifecycleOwner - @Mock lateinit var mLifecycleRegistry: LifecycleRegistry + private lateinit var lifecycleRegistry: FakeLifecycleRegistry - lateinit var mWindowParams: WindowManager.LayoutParams + private lateinit var mWindowParams: WindowManager.LayoutParams @Mock lateinit var mDreamOverlayCallback: IDreamOverlayCallback @@ -114,18 +135,33 @@ class DreamOverlayServiceTest : SysuiTestCase() { @Mock lateinit var mUiEventLogger: UiEventLogger + @Mock lateinit var mScrimManager: ScrimManager + + @Mock lateinit var mScrimController: ScrimController + + @Mock lateinit var mSystemDialogsCloser: SystemDialogsCloser + @Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController + private lateinit var bouncerRepository: FakeKeyguardBouncerRepository + private lateinit var communalRepository: FakeCommunalRepository + @Captor var mViewCaptor: ArgumentCaptor<View>? = null - var mService: DreamOverlayService? = null + private lateinit var mService: DreamOverlayService + @Before fun setup() { MockitoAnnotations.initMocks(this) + + lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner) + bouncerRepository = kosmos.fakeKeyguardBouncerRepository + communalRepository = kosmos.fakeCommunalRepository + whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController()) .thenReturn(mDreamOverlayContainerViewController) whenever(mComplicationComponent.getComplicationHostViewController()) .thenReturn(mComplicationHostViewController) - whenever(mLifecycleOwner.registry).thenReturn(mLifecycleRegistry) + whenever(mLifecycleOwner.registry).thenReturn(lifecycleRegistry) whenever(mComplicationComponentFactory.create(any(), any(), any(), any())) .thenReturn(mComplicationComponent) whenever(mComplicationComponent.getVisibilityController()) @@ -141,6 +177,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { whenever(mAmbientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor) whenever(mDreamOverlayContainerViewController.containerView) .thenReturn(mDreamOverlayContainerView) + whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController) mWindowParams = WindowManager.LayoutParams() mService = DreamOverlayService( @@ -154,24 +191,30 @@ class DreamOverlayServiceTest : SysuiTestCase() { mAmbientTouchComponentFactory, mStateController, mKeyguardUpdateMonitor, + mScrimManager, + kosmos.communalInteractor, + mSystemDialogsCloser, mUiEventLogger, mTouchInsetManager, LOW_LIGHT_COMPONENT, HOME_CONTROL_PANEL_DREAM_COMPONENT, mDreamOverlayCallbackController, + kosmos.keyguardInteractor, WINDOW_NAME ) } - @get:Throws(RemoteException::class) - val client: IDreamOverlayClient + private val client: IDreamOverlayClient get() { - val proxy = mService!!.onBind(Intent()) + mService.onCreate() + TestableLooper.get(this).processAllMessages() + + val proxy = mService.onBind(Intent()) val overlay = IDreamOverlay.Stub.asInterface(proxy) val callback = Mockito.mock(IDreamOverlayClientCallback::class.java) overlay.getClient(callback) val clientCaptor = ArgumentCaptor.forClass(IDreamOverlayClient::class.java) - Mockito.verify(callback).onDreamOverlayClient(clientCaptor.capture()) + verify(callback).onDreamOverlayClient(clientCaptor.capture()) return clientCaptor.value } @@ -187,9 +230,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mUiEventLogger) - .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START) - Mockito.verify(mUiEventLogger) + verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START) + verify(mUiEventLogger) .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) } @@ -205,7 +247,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mWindowManager).addView(any(), any()) + verify(mWindowManager).addView(any(), any()) } // Validates that {@link DreamOverlayService} properly handles the case where the dream's @@ -224,14 +266,14 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mWindowManager).addView(any(), any()) - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) - Mockito.verify(mStateController).setEntryAnimationsFinished(false) - Mockito.verify(mStateController, Mockito.never()).setOverlayActive(true) - Mockito.verify(mUiEventLogger, Mockito.never()) + verify(mWindowManager).addView(any(), any()) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) + verify(mStateController).setEntryAnimationsFinished(false) + verify(mStateController, Mockito.never()).setOverlayActive(true) + verify(mUiEventLogger, Mockito.never()) .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START) - Mockito.verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream() + verify(mDreamOverlayCallbackController, Mockito.never()).onStartDream() } @Test @@ -246,7 +288,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mDreamOverlayContainerViewController).init() + verify(mDreamOverlayContainerViewController).init() } @Test @@ -264,7 +306,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView) + verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView) } @Test @@ -279,7 +321,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Truth.assertThat(mService!!.shouldShowComplications()).isTrue() + assertThat(mService.shouldShowComplications()).isTrue() } @Test @@ -294,8 +336,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Truth.assertThat(mService!!.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT) - Mockito.verify(mStateController).setLowLightActive(true) + assertThat(mService.dreamComponent).isEqualTo(LOW_LIGHT_COMPONENT) + verify(mStateController).setLowLightActive(true) } @Test @@ -310,8 +352,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Truth.assertThat(mService!!.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT) - Mockito.verify(mStateController).setHomeControlPanelActive(true) + assertThat(mService.dreamComponent).isEqualTo(HOME_CONTROL_PANEL_DREAM_COMPONENT) + verify(mStateController).setHomeControlPanelActive(true) } @Test @@ -328,19 +370,19 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Verify view added. - Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) + verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) // Service destroyed. - mService!!.onEndDream() + mService.onEndDream() mMainExecutor.runAllReady() // Verify view removed. - Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value) + verify(mWindowManager).removeView(mViewCaptor!!.value) // Verify state correctly set. - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) - Mockito.verify(mStateController).setEntryAnimationsFinished(false) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) + verify(mStateController).setEntryAnimationsFinished(false) } @Test @@ -373,7 +415,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { // Schedule the endDream call in the middle of the startDream implementation, as any // ordering is possible. - Mockito.doAnswer { invocation: InvocationOnMock? -> + Mockito.doAnswer { client.endDream() null } @@ -409,37 +451,37 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Verify view added. - Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) + verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) // Service destroyed. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() // Verify view removed. - Mockito.verify(mWindowManager).removeView(mViewCaptor!!.value) + verify(mWindowManager).removeView(mViewCaptor!!.value) // Verify state correctly set. - Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any()) - Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) - Mockito.verify(mStateController).setEntryAnimationsFinished(false) + verify(mKeyguardUpdateMonitor).removeCallback(any()) + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) + verify(mStateController).setEntryAnimationsFinished(false) } @Test fun testDoNotRemoveViewOnDestroyIfOverlayNotStarted() { // Service destroyed without ever starting dream. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() // Verify no view is removed. - Mockito.verify(mWindowManager, Mockito.never()).removeView(any()) + verify(mWindowManager, Mockito.never()).removeView(any()) // Verify state still correctly set. - Mockito.verify(mKeyguardUpdateMonitor).removeCallback(any()) - Mockito.verify(mLifecycleRegistry).currentState = Lifecycle.State.DESTROYED - Mockito.verify(mStateController).setOverlayActive(false) - Mockito.verify(mStateController).setLowLightActive(false) + verify(mKeyguardUpdateMonitor).removeCallback(any()) + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.DESTROYED) + verify(mStateController).setOverlayActive(false) + verify(mStateController).setLowLightActive(false) } @Test @@ -447,7 +489,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { val client = client // Destroy the service. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() // Inform the overlay service of dream starting. @@ -458,15 +500,15 @@ class DreamOverlayServiceTest : SysuiTestCase() { false /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - Mockito.verify(mWindowManager, Mockito.never()).addView(any(), any()) + verify(mWindowManager, Mockito.never()).addView(any(), any()) } @Test fun testNeverRemoveDecorViewIfNotAdded() { // Service destroyed before dream started. - mService!!.onDestroy() + mService.onDestroy() mMainExecutor.runAllReady() - Mockito.verify(mWindowManager, Mockito.never()).removeView(any()) + verify(mWindowManager, Mockito.never()).removeView(any()) } @Test @@ -483,11 +525,11 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Verify that a new window is added. - Mockito.verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) + verify(mWindowManager).addView(mViewCaptor!!.capture(), any()) val windowDecorView = mViewCaptor!!.value // Assert that the overlay is not showing complications. - Truth.assertThat(mService!!.shouldShowComplications()).isFalse() + assertThat(mService.shouldShowComplications()).isFalse() Mockito.clearInvocations(mDreamOverlayComponent) Mockito.clearInvocations(mAmbientTouchComponent) Mockito.clearInvocations(mWindowManager) @@ -504,16 +546,16 @@ class DreamOverlayServiceTest : SysuiTestCase() { mMainExecutor.runAllReady() // Assert that the overlay is showing complications. - Truth.assertThat(mService!!.shouldShowComplications()).isTrue() + assertThat(mService.shouldShowComplications()).isTrue() // Verify that the old overlay window has been removed, and a new one created. - Mockito.verify(mWindowManager).removeView(windowDecorView) - Mockito.verify(mWindowManager).addView(any(), any()) + verify(mWindowManager).removeView(windowDecorView) + verify(mWindowManager).addView(any(), any()) // Verify that new instances of overlay container view controller and overlay touch monitor // are created. - Mockito.verify(mDreamOverlayComponent).getDreamOverlayContainerViewController() - Mockito.verify(mAmbientTouchComponent).getTouchMonitor() + verify(mDreamOverlayComponent).getDreamOverlayContainerViewController() + verify(mAmbientTouchComponent).getTouchMonitor() } @Test @@ -528,15 +570,15 @@ class DreamOverlayServiceTest : SysuiTestCase() { true /*shouldShowComplication*/ ) mMainExecutor.runAllReady() - mService!!.onWakeUp() - Mockito.verify(mDreamOverlayContainerViewController).wakeUp() - Mockito.verify(mDreamOverlayCallbackController).onWakeUp() + mService.onWakeUp() + verify(mDreamOverlayContainerViewController).wakeUp() + verify(mDreamOverlayCallbackController).onWakeUp() } @Test fun testWakeUpBeforeStartDoesNothing() { - mService!!.onWakeUp() - Mockito.verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp() + mService.onWakeUp() + verify(mDreamOverlayContainerViewController, Mockito.never()).wakeUp() } @Test @@ -554,8 +596,8 @@ class DreamOverlayServiceTest : SysuiTestCase() { val paramsCaptor = ArgumentCaptor.forClass(WindowManager.LayoutParams::class.java) // Verify that a new window is added. - Mockito.verify(mWindowManager).addView(any(), paramsCaptor.capture()) - Truth.assertThat( + verify(mWindowManager).addView(any(), paramsCaptor.capture()) + assertThat( paramsCaptor.value.privateFlags and WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS == WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS @@ -563,6 +605,254 @@ class DreamOverlayServiceTest : SysuiTestCase() { .isTrue() } + // Tests that the bouncer closes when DreamOverlayService is told that the dream is coming to + // the front. + @Test + fun testBouncerRetractedWhenDreamComesToFront() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + true /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + whenever(mDreamOverlayContainerViewController.isBouncerShowing()).thenReturn(true) + mService!!.onComeToFront() + verify(mScrimController).expand(any()) + } + + // Tests that glanceable hub is hidden when DreamOverlayService is told that the dream is + // coming to the front. + @Test + fun testGlanceableHubHiddenWhenDreamComesToFront() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + true /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + mService!!.onComeToFront() + assertThat(communalRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } + + // Tests that system dialogs (e.g. notification shade) closes when DreamOverlayService is told + // that the dream is coming to the front. + @Test + fun testSystemDialogsClosedWhenDreamComesToFront() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + true /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + mService!!.onComeToFront() + verify(mSystemDialogsCloser).closeSystemDialogs() + } + + @Test + fun testLifecycle_createdAfterConstruction() { + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun testLifecycle_resumedAfterDreamStarts() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.mLifecycles) + .containsExactly( + Lifecycle.State.CREATED, + Lifecycle.State.STARTED, + Lifecycle.State.RESUMED + ) + } + + @Test + fun testLifecycle_destroyedAfterOnDestroy() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + mService.onDestroy() + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.mLifecycles) + .containsExactly( + Lifecycle.State.CREATED, + Lifecycle.State.STARTED, + Lifecycle.State.RESUMED, + Lifecycle.State.DESTROYED + ) + } + + @Test + fun testNotificationShadeShown_setsLifecycleState() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + val callbackCaptor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java) + verify(mKeyguardUpdateMonitor).registerCallback(callbackCaptor.capture()) + + // Notification shade opens. + callbackCaptor.value.onShadeExpandedChanged(true) + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Notification shade closes. + callbackCaptor.value.onShadeExpandedChanged(false) + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun testBouncerShown_setsLifecycleState() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + + // Bouncer shows. + bouncerRepository.setPrimaryShow(true) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Bouncer closes. + bouncerRepository.setPrimaryShow(false) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun testCommunalVisible_setsLifecycleState() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + val transitionState: MutableStateFlow<ObservableTransitionState> = + MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank)) + communalRepository.setTransitionState(transitionState) + + // Communal becomes visible. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Communal closes. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + // Verifies the dream's lifecycle + @Test + fun testLifecycleStarted_whenAnyOcclusion() { + val client = client + + // Inform the overlay service of dream starting. Do not show dream complications. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + val transitionState: MutableStateFlow<ObservableTransitionState> = + MutableStateFlow(ObservableTransitionState.Idle(CommunalScenes.Blank)) + communalRepository.setTransitionState(transitionState) + + // Communal becomes visible. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Communal) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes from resumed back to started when the notification shade shows. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.STARTED) + + // Communal closes. + transitionState.value = ObservableTransitionState.Idle(CommunalScenes.Blank) + testScope.runCurrent() + mMainExecutor.runAllReady() + + // Lifecycle state goes back to RESUMED. + assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) { + val mLifecycles: MutableList<State> = ArrayList() + + override var currentState: State + get() = mLifecycles[mLifecycles.size - 1] + set(state) { + mLifecycles.add(state) + } + } + companion object { private val LOW_LIGHT_COMPONENT = ComponentName("package", "lowlight") private val HOME_CONTROL_PANEL_DREAM_COMPONENT = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index 29fbee01a18b..e3c6deed1527 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -108,7 +108,7 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { mTouchHandler.onSessionStart(mTouchSession); verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); - verify(mCentralSurfaces).handleDreamTouch(motionEvent); + verify(mCentralSurfaces).handleExternalShadeWindowTouch(motionEvent); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt index 805b4a828bda..855b6d0b95d7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt @@ -44,14 +44,14 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) -class SeekableSliderHapticPluginTest : SysuiTestCase() { +class SeekbarHapticPluginTest : SysuiTestCase() { private val kosmos = Kosmos() @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var vibratorHelper: VibratorHelper private val seekBar = SeekBar(mContext) - private lateinit var plugin: SeekableSliderHapticPlugin + private lateinit var plugin: SeekbarHapticPlugin @Before fun setup() { @@ -142,7 +142,7 @@ class SeekableSliderHapticPluginTest : SysuiTestCase() { private fun createPlugin() { plugin = - SeekableSliderHapticPlugin( + SeekbarHapticPlugin( vibratorHelper, kosmos.fakeSystemClock, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt new file mode 100644 index 000000000000..88189dbafa6c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderStateProducerTest.kt @@ -0,0 +1,134 @@ +/* + * 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.haptics.slider + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import junit.framework.Assert.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SliderStateProducerTest : SysuiTestCase() { + + private val eventProducer = SliderStateProducer() + private val eventFlow = eventProducer.produceEvents() + + @Test + fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest { + val latest by collectLastValue(eventFlow) + + eventProducer.onStartTracking(/*fromUser =*/ true) + + assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest) + } + + @Test + fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest { + val latest by collectLastValue(eventFlow) + + eventProducer.onStopTracking(/*fromUser =*/ true) + + assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest) + } + + @Test + fun onStartTrackingProgram_noProgress_trackingTouchEventProduced() = runTest { + val latest by collectLastValue(eventFlow) + + eventProducer.onStartTracking(/*fromUser =*/ false) + + assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, 0F), latest) + } + + @Test + fun onStopTrackingProgram_noProgress_StoppedTrackingTouchEventProduced() = runTest { + val latest by collectLastValue(eventFlow) + + eventProducer.onStopTracking(/*fromUser =*/ false) + + assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, 0F), latest) + } + + @Test + fun onProgressChangeByUser_changeByUserEventProduced() = runTest { + val progress = 0.5f + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(/*fromUser =*/ true, progress) + + assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress), latest) + } + + @Test + fun onProgressChangeByProgram_changeByProgramEventProduced() = runTest { + val progress = 0.5f + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(/*fromUser =*/ false, progress) + + assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress), latest) + } + + @Test + fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced() = runTest { + val progress = 0.5f + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(/*fromUser =*/ true, progress) + eventProducer.onStartTracking(/*fromUser =*/ true) + + assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress), latest) + } + + @Test + fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced() = runTest { + val progress = 0.5f + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(/*fromUser =*/ true, progress) + eventProducer.onStopTracking(/*fromUser =*/ true) + + assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, progress), latest) + } + + @Test + fun onStartTrackingProgram_afterProgress_trackingProgramEventProduced() = runTest { + val progress = 0.5f + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(/*fromUser =*/ false, progress) + eventProducer.onStartTracking(/*fromUser =*/ false) + + assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress), latest) + } + + @Test + fun onStopTrackingProgram_afterProgress_stopTrackingProgramEventProduced() = runTest { + val progress = 0.5f + val latest by collectLastValue(eventFlow) + + eventProducer.onProgressChanged(/*fromUser =*/ false, progress) + eventProducer.onStopTracking(/*fromUser =*/ false) + + assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress), latest) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt index db31ad52e3b0..4d69f0ddc4b7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagParameterizationTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.Flags.FLAG_EXAMPLE_FLAG import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase import com.android.systemui.flags.andSceneContainer +import com.android.systemui.flags.parameterizeSceneContainerFlag import com.google.common.truth.Truth import org.junit.Test import org.junit.runner.RunWith @@ -41,6 +42,14 @@ internal class SceneContainerFlagParameterizationTest : SysuiTestCase() { } @Test + fun parameterizeSceneContainer() { + val result = parameterizeSceneContainerFlag() + Truth.assertThat(result).hasSize(2) + Truth.assertThat(result[0].mOverrides[FLAG_SCENE_CONTAINER]).isFalse() + Truth.assertThat(result[1].mOverrides[FLAG_SCENE_CONTAINER]).isTrue() + } + + @Test fun oneUnrelatedAndSceneContainer() { val unrelatedFlag = FLAG_EXAMPLE_FLAG val result = FlagsParameterization.allCombinationsOf(unrelatedFlag).andSceneContainer() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index b89ccefddf6b..a8da1160c503 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -29,7 +29,6 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; @@ -37,12 +36,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; - import android.app.IActivityManager; import android.content.pm.ActivityInfo; import android.content.res.Configuration; -import android.testing.AndroidTestingRunner; +import android.platform.test.flag.junit.FlagsParameterization; import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.view.WindowManager; @@ -50,51 +47,26 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; -import com.android.keyguard.KeyguardSecurityModel; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FakeFeatureFlagsClassic; +import com.android.systemui.flags.SceneContainerFlagParameterizationKt; import com.android.systemui.keyguard.KeyguardViewMediator; -import com.android.systemui.keyguard.data.repository.FakeCommandQueue; -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor; -import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; import com.android.systemui.scene.FakeWindowRootViewComponent; -import com.android.systemui.scene.data.repository.SceneContainerRepository; -import com.android.systemui.scene.domain.interactor.SceneInteractor; -import com.android.systemui.scene.shared.logger.SceneLogger; import com.android.systemui.settings.UserTracker; -import com.android.systemui.shade.data.repository.FakeShadeRepository; -import com.android.systemui.shade.domain.interactor.ShadeInteractor; -import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl; -import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository; -import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.phone.KeyguardBypassController; -import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; -import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; -import com.android.systemui.user.domain.interactor.UserSwitcherInteractor; import com.google.common.util.concurrent.MoreExecutors; @@ -110,13 +82,13 @@ import org.mockito.Spy; import java.util.List; import java.util.concurrent.Executor; -import kotlinx.coroutines.test.TestScope; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; -@RunWith(AndroidTestingRunner.class) +@RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper(setAsMainLooper = true) @SmallTest public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { - @Mock private WindowManager mWindowManager; @Mock private DozeParameters mDozeParameters; @Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy( @@ -128,29 +100,31 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private SysuiColorExtractor mColorExtractor; @Mock private ColorExtractor.GradientColors mGradientColors; @Mock private DumpManager mDumpManager; - @Mock private KeyguardSecurityModel mKeyguardSecurityModel; @Mock private KeyguardStateController mKeyguardStateController; @Mock private AuthController mAuthController; @Mock private ShadeWindowLogger mShadeWindowLogger; @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock private UserTracker mUserTracker; - @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper; @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListener; private final Executor mMainExecutor = MoreExecutors.directExecutor(); private final Executor mBackgroundExecutor = MoreExecutors.directExecutor(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private final TestScope mTestScope = mKosmos.getTestScope(); - private ShadeInteractor mShadeInteractor; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; private float mPreferredRefreshRate = -1; - private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor; - private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor; - private ScreenOffAnimationController mScreenOffAnimationController; private SysuiStatusBarStateController mStatusBarStateController; + @Parameters(name = "{0}") + public static List<FlagsParameterization> getParams() { + return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag(); + } + + public NotificationShadeWindowControllerImplTest(FlagsParameterization flags) { + mSetFlagsRule.setFlagsParameterization(flags); + } + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -164,71 +138,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { when(mDozeParameters.getAlwaysOn()).thenReturn(true); when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors); - FakeKeyguardRepository keyguardRepository = new FakeKeyguardRepository(); - FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); - FakeShadeRepository shadeRepository = new FakeShadeRepository(); - - mScreenOffAnimationController = mKosmos.getScreenOffAnimationController(); mStatusBarStateController = spy(mKosmos.getStatusBarStateController()); - PowerInteractor powerInteractor = mKosmos.getPowerInteractor(); - - SceneInteractor sceneInteractor = new SceneInteractor( - mTestScope.getBackgroundScope(), - new SceneContainerRepository( - mTestScope.getBackgroundScope(), - mKosmos.getFakeSceneContainerConfig(), - mKosmos.getSceneDataSource()), - mock(SceneLogger.class), - mKosmos.getDeviceUnlockedInteractor()); - - FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); - KeyguardTransitionInteractor keyguardTransitionInteractor = - mKosmos.getKeyguardTransitionInteractor(); - KeyguardInteractor keyguardInteractor = new KeyguardInteractor( - keyguardRepository, - new FakeCommandQueue(), - powerInteractor, - new FakeKeyguardBouncerRepository(), - new ConfigurationInteractor(configurationRepository), - shadeRepository, - keyguardTransitionInteractor, - () -> sceneInteractor, - () -> mKosmos.getFromGoneTransitionInteractor(), - () -> mKosmos.getSharedNotificationContainerInteractor(), - mTestScope); - CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); - - mFromLockscreenTransitionInteractor = mKosmos.getFromLockscreenTransitionInteractor(); - mFromPrimaryBouncerTransitionInteractor = - mKosmos.getFromPrimaryBouncerTransitionInteractor(); - - DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor = - mock(DeviceEntryUdfpsInteractor.class); - when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); - - mShadeInteractor = new ShadeInteractorImpl( - mTestScope.getBackgroundScope(), - mKosmos.getDeviceProvisioningInteractor(), - new FakeDisableFlagsRepository(), - mock(DozeParameters.class), - keyguardRepository, - keyguardTransitionInteractor, - powerInteractor, - new FakeUserSetupRepository(), - mock(UserSwitcherInteractor.class), - new ShadeInteractorLegacyImpl( - mTestScope.getBackgroundScope(), - keyguardRepository, - new SharedNotificationContainerInteractor( - configurationRepository, - mContext, - new ResourcesSplitShadeStateController(), - keyguardInteractor, - deviceEntryUdfpsInteractor, - () -> mLargeScreenHeaderHelper), - shadeRepository - ) - ); mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl( mContext, @@ -245,13 +155,13 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mColorExtractor, mDumpManager, mKeyguardStateController, - mScreenOffAnimationController, + mKosmos.getScreenOffAnimationController(), mAuthController, - () -> mShadeInteractor, + mKosmos::getShadeInteractor, mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - () -> communalInteractor) { + mKosmos::getCommunalInteractor) { @Override protected boolean isDebuggable() { return false; 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 96b2b7a20342..aa0ca186c1ee 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 @@ -24,7 +24,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.andSceneContainer +import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.DozeStateModel @@ -80,7 +80,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization?) : SysuiTestCase() { @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() + return parameterizeSceneContainerFlag() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt index 2ab934c2386e..07c4b00364ff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt @@ -16,7 +16,7 @@ package com.android.systemui.shade.domain.startable -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey @@ -25,9 +25,8 @@ import com.android.systemui.authentication.data.repository.fakeAuthenticationRep import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository -import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope @@ -50,24 +49,42 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(AndroidJUnit4::class) -class ShadeStartableTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val shadeInteractor = kosmos.shadeInteractor - private val sceneInteractor = kosmos.sceneInteractor - private val shadeExpansionStateManager = kosmos.shadeExpansionStateManager - private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository - private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor - private val fakeConfigurationRepository = kosmos.fakeConfigurationRepository - private val fakeSceneDataSource = kosmos.fakeSceneDataSource - - private val underTest = kosmos.shadeStartable + private val shadeInteractor by lazy { kosmos.shadeInteractor } + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val shadeExpansionStateManager by lazy { kosmos.shadeExpansionStateManager } + private val fakeConfigurationRepository by lazy { kosmos.fakeConfigurationRepository } + private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } + + private lateinit var underTest: ShadeStartable + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + + @Before + fun setup() { + underTest = kosmos.shadeStartable + } @Test fun hydrateShadeMode() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 2397de69c063..5312ad809a72 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.common.ui.data.repository.fakeConfigurationRepositor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +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 @@ -65,6 +66,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper +@EnableSceneContainer class ShadeSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index be5af885c895..fc9535cf6950 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,60 +19,32 @@ package com.android.systemui.statusbar import android.animation.ObjectAnimator -import android.testing.AndroidTestingRunner +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake 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.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.classifier.FalsingCollectorFake -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer -import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.jank.interactionJankMonitor -import com.android.systemui.keyguard.data.repository.FakeCommandQueue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor -import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.LargeScreenHeaderHelper -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.shade.domain.interactor.ShadeInteractor -import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl -import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl -import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository -import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor -import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor -import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController -import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository -import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.testKosmos import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -87,26 +59,34 @@ import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.Mockito.`when` as whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(ParameterizedAndroidJunit4::class) @TestableLooper.RunWithLooper -class StatusBarStateControllerImplTest : SysuiTestCase() { +class StatusBarStateControllerImplTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private lateinit var shadeInteractor: ShadeInteractor - private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor - private lateinit var fromPrimaryBouncerTransitionInteractor: - FromPrimaryBouncerTransitionInteractor + private val mockDarkAnimator = mock<ObjectAnimator>() - private val deviceEntryUdfpsInteractor = mock<DeviceEntryUdfpsInteractor>() - private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() private lateinit var underTest: StatusBarStateControllerImpl private lateinit var uiEventLogger: UiEventLoggerFake + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return parameterizeSceneContainerFlag() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -118,7 +98,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { uiEventLogger, kosmos.interactionJankMonitor, JavaAdapter(testScope.backgroundScope), - { shadeInteractor }, + { kosmos.shadeInteractor }, { kosmos.deviceUnlockedInteractor }, { kosmos.sceneInteractor }, { kosmos.keyguardClockInteractor }, @@ -127,59 +107,6 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { return mockDarkAnimator } } - - val powerInteractor = - PowerInteractor(FakePowerRepository(), FalsingCollectorFake(), mock(), underTest) - val keyguardRepository = FakeKeyguardRepository() - val keyguardTransitionRepository = FakeKeyguardTransitionRepository() - val featureFlags = FakeFeatureFlagsClassic() - val shadeRepository = FakeShadeRepository() - val configurationRepository = FakeConfigurationRepository() - val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor - fromLockscreenTransitionInteractor = kosmos.fromLockscreenTransitionInteractor - fromPrimaryBouncerTransitionInteractor = kosmos.fromPrimaryBouncerTransitionInteractor - - val keyguardInteractor = - KeyguardInteractor( - keyguardRepository, - FakeCommandQueue(), - powerInteractor, - FakeKeyguardBouncerRepository(), - ConfigurationInteractor(configurationRepository), - shadeRepository, - keyguardTransitionInteractor, - { kosmos.sceneInteractor }, - { kosmos.fromGoneTransitionInteractor }, - { kosmos.sharedNotificationContainerInteractor }, - testScope, - ) - - whenever(deviceEntryUdfpsInteractor.isUdfpsSupported).thenReturn(MutableStateFlow(false)) - shadeInteractor = - ShadeInteractorImpl( - testScope.backgroundScope, - kosmos.deviceProvisioningInteractor, - FakeDisableFlagsRepository(), - mock(), - keyguardRepository, - keyguardTransitionInteractor, - powerInteractor, - FakeUserSetupRepository(), - mock(), - ShadeInteractorLegacyImpl( - testScope.backgroundScope, - keyguardRepository, - SharedNotificationContainerInteractor( - configurationRepository, - mContext, - ResourcesSplitShadeStateController(), - keyguardInteractor, - deviceEntryUdfpsInteractor, - largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } - ), - shadeRepository, - ) - ) } @Test diff --git a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml new file mode 100644 index 000000000000..47fd78a4d368 --- /dev/null +++ b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:anim/accelerate_interpolator" + android:zAdjustment="top"> + + <translate + android:fromYDelta="0" + android:toYDelta="100%" + android:duration="@android:integer/config_shortAnimTime" /> +</set> diff --git a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml new file mode 100644 index 000000000000..77edf588bd2e --- /dev/null +++ b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Animation for when a dock window at the bottom of the screen is entering. --> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:anim/accelerate_decelerate_interpolator" + android:zAdjustment="top"> + + <translate android:fromYDelta="100%" + android:toYDelta="0" + android:startOffset="@android:integer/config_shortAnimTime" + android:duration="@android:integer/config_mediumAnimTime"/> +</set> diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml new file mode 100644 index 000000000000..292e49610e2a --- /dev/null +++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/shortcut_helper_sheet_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:id="@+id/shortcut_helper_sheet" + style="@style/Widget.Material3.BottomSheet" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + <!-- Drag handle for accessibility --> + <com.google.android.material.bottomsheet.BottomSheetDragHandleView + android:id="@+id/drag_handle" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:gravity="center" + android:textAppearance="?textAppearanceDisplayLarge" + android:background="?colorTertiaryContainer" + android:text="Shortcut Helper Content" /> + </LinearLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml index 291f8a20bdd2..546bf1c3ac95 100644 --- a/packages/SystemUI/res/values-night/styles.xml +++ b/packages/SystemUI/res/values-night/styles.xml @@ -72,4 +72,8 @@ <item name="android:textColor">@color/material_dynamic_secondary80</item> </style> + <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon"> + <item name="android:windowLightNavigationBar">false</item> + </style> + </resources> diff --git a/packages/SystemUI/res/values-xlarge-land/config.xml b/packages/SystemUI/res/values-xlarge-land/config.xml new file mode 100644 index 000000000000..5e4304e1c13a --- /dev/null +++ b/packages/SystemUI/res/values-xlarge-land/config.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">0.8</item> +</resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 4a73d85e77b7..19273ec3af82 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1010,4 +1010,7 @@ <!-- Whether volume panel should use the large screen layout or not --> <bool name="volume_panel_is_large_screen">false</bool> + + <!-- The width of the shortcut helper container, as a fraction of the screen's width. --> + <item name="shortcut_helper_screen_width_fraction" format="float" type="dimen">1.0</item> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9aff9fdc2965..27aa15fb94a1 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1991,7 +1991,7 @@ <string name="keyboard_shortcut_clear_text">Clear search query</string> <!-- The title for keyboard shortcut search list [CHAR LIMIT=25] --> <string name="keyboard_shortcut_search_list_title">Keyboard Shortcuts</string> - <!-- The hint for keyboard shortcut search list [CHAR LIMIT=25] --> + <!-- The hint for keyboard shortcut search list [CHAR LIMIT=50] --> <string name="keyboard_shortcut_search_list_hint">Search shortcuts</string> <!-- The description for no shortcuts results [CHAR LIMIT=25] --> <string name="keyboard_shortcut_search_list_no_result">No shortcuts found</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 2c9006e50f92..3d57111253f8 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1654,4 +1654,28 @@ <style name="Theme.SystemUI.Dialog.StickyKeys" parent="@style/Theme.SystemUI.Dialog"> <item name="android:colorBackground">@color/transparent</item> </style> + + <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity"> + <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item> + <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item> + <item name="android:activityOpenExitAnimation">@anim/shortcut_helper_close_anim</item> + <item name="android:taskOpenExitAnimation">@anim/shortcut_helper_close_anim</item> + </style> + + <style name="ShortcutHelperThemeCommon" parent="@style/Theme.Material3.DynamicColors.DayNight"> + <item name="android:windowAnimationStyle">@style/ShortcutHelperAnimation</item> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowNoTitle">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:backgroundDimEnabled">true</item> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + <item name="android:windowLayoutInDisplayCutoutMode">always</item> + <item name="enableEdgeToEdge">true</item> + </style> + + <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon"> + <item name="android:windowLightNavigationBar">true</item> + </style> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java index 39e1c4150167..55ccaa68c855 100644 --- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java +++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java @@ -33,12 +33,23 @@ import java.io.PrintWriter; * abstract fun bind(impl: FoobarStartable): CoreStartable * </pre> * - * If your CoreStartable depends on different CoreStartables starting before it, use a - * {@link com.android.systemui.startable.Dependencies} annotation to list out those dependencies. + * If your CoreStartable depends on different CoreStartables starting before it, you can specify + * another map binding listing out its dependencies: + * <pre> + * @Provides + * @IntoMap + * @Dependencies // Important! com.android.systemui.startable.Dependencies. + * @ClassKey(FoobarStartable::class) + * fun providesDeps(): Set<Class<out CoreStartable>> { + * return setOf(OtherStartable::class.java) + * } + * </pre> + * * * @see SystemUIApplication#startSystemUserServicesIfNeeded() */ public interface CoreStartable extends Dumpable { + String STARTABLE_DEPENDENCIES = "startable_dependencies"; /** Main entry point for implementations. Called shortly after SysUI startup. */ void start(); diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index ba869bd56f31..4b0740288844 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -44,16 +44,15 @@ import com.android.systemui.dagger.SysUIComponent; import com.android.systemui.dump.DumpManager; import com.android.systemui.process.ProcessWrapper; import com.android.systemui.res.R; -import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.NotificationChannels; import java.lang.reflect.InvocationTargetException; import java.util.ArrayDeque; -import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; import java.util.TreeMap; @@ -300,6 +299,7 @@ public class SystemUIApplication extends Application implements int serviceIndex = 0; do { + startedAny = false; queue = nextQueue; nextQueue = new ArrayDeque<>(startables.size()); @@ -307,9 +307,9 @@ public class SystemUIApplication extends Application implements Map.Entry<Class<?>, Provider<CoreStartable>> entry = queue.removeFirst(); Class<?> cls = entry.getKey(); - Dependencies dep = cls.getAnnotation(Dependencies.class); - Class<?>[] deps = (dep == null ? null : dep.value()); - if (deps == null || startedStartables.containsAll(Arrays.asList(deps))) { + Set<Class<? extends CoreStartable>> deps = + mSysUIComponent.getStartableDependencies().get(cls); + if (deps == null || startedStartables.containsAll(deps)) { String clsName = cls.getName(); int i = serviceIndex; // Copied to make lambda happy. timeInitialization( @@ -331,12 +331,12 @@ public class SystemUIApplication extends Application implements while (!nextQueue.isEmpty()) { Map.Entry<Class<?>, Provider<CoreStartable>> entry = nextQueue.removeFirst(); Class<?> cls = entry.getKey(); - Dependencies dep = cls.getAnnotation(Dependencies.class); - Class<?>[] deps = (dep == null ? null : dep.value()); + Set<Class<? extends CoreStartable>> deps = + mSysUIComponent.getStartableDependencies().get(cls); StringJoiner stringJoiner = new StringJoiner(", "); - for (int i = 0; deps != null && i < deps.length; i++) { - if (!startedStartables.contains(deps[i])) { - stringJoiner.add(deps[i].getName()); + for (Class<? extends CoreStartable> c : deps) { + if (!startedStartables.contains(c)) { + stringJoiner.add(c.getName()); } } Log.e(TAG, "Failed to start " + cls.getName() diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java index d0f08f53fb32..85aeb27261aa 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.java @@ -27,6 +27,7 @@ import android.view.InputEvent; import android.view.MotionEvent; import android.view.VelocityTracker; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEvent; @@ -94,13 +95,11 @@ public class BouncerSwipeTouchHandler implements TouchHandler { private Boolean mCapture; private Boolean mExpanded; - private boolean mBouncerInitiallyShowing; - private TouchSession mTouchSession; - private ValueAnimatorCreator mValueAnimatorCreator; + private final ValueAnimatorCreator mValueAnimatorCreator; - private VelocityTrackerFactory mVelocityTrackerFactory; + private final VelocityTrackerFactory mVelocityTrackerFactory; private final UiEventLogger mUiEventLogger; @@ -118,17 +117,12 @@ public class BouncerSwipeTouchHandler implements TouchHandler { private final GestureDetector.OnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { if (mCapture == null) { - mBouncerInitiallyShowing = mCentralSurfaces - .map(CentralSurfaces::isBouncerShowing) - .orElse(false); - if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) { mCapture = Math.abs(distanceY) > Math.abs(distanceX) - && ((distanceY < 0 && mBouncerInitiallyShowing) - || (distanceY > 0 && !mBouncerInitiallyShowing)); + && distanceY > 0; } else { // If the user scrolling favors a vertical direction, begin capturing // scrolls. @@ -146,13 +140,8 @@ public class BouncerSwipeTouchHandler implements TouchHandler { return false; } - // Don't set expansion for downward scroll when the bouncer is hidden. - if (!mBouncerInitiallyShowing && (e1.getY() < e2.getY())) { - return true; - } - - // Don't set expansion for upward scroll when the bouncer is shown. - if (mBouncerInitiallyShowing && (e1.getY() > e2.getY())) { + // Don't set expansion for downward scroll. + if (e1.getY() < e2.getY()) { return true; } @@ -176,8 +165,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { final float dragDownAmount = e2.getY() - e1.getY(); final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY()) / mTouchSession.getBounds().height(); - setPanelExpansion(mBouncerInitiallyShowing - ? screenTravelPercentage : 1 - screenTravelPercentage); + setPanelExpansion(1 - screenTravelPercentage); return true; } }; @@ -223,9 +211,9 @@ public class BouncerSwipeTouchHandler implements TouchHandler { LockPatternUtils lockPatternUtils, UserTracker userTracker, @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING) - FlingAnimationUtils flingAnimationUtils, + FlingAnimationUtils flingAnimationUtils, @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING) - FlingAnimationUtils flingAnimationUtilsClosing, + FlingAnimationUtils flingAnimationUtilsClosing, @Named(BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage, @Named(BouncerSwipeModule.MIN_BOUNCER_ZONE_SCREEN_PERCENTAGE) float minRegionPercentage, UiEventLogger uiEventLogger) { @@ -247,17 +235,13 @@ public class BouncerSwipeTouchHandler implements TouchHandler { public void getTouchInitiationRegion(Rect bounds, Region region, Rect exclusionRect) { final int width = bounds.width(); final int height = bounds.height(); - final float minBouncerHeight = height * mMinBouncerZoneScreenPercentage; final int minAllowableBottom = Math.round(height * (1 - mMinBouncerZoneScreenPercentage)); - final boolean isBouncerShowing = - mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false); - final Rect normalRegion = isBouncerShowing - ? new Rect(0, 0, width, Math.round(height * mBouncerZoneScreenPercentage)) - : new Rect(0, Math.round(height * (1 - mBouncerZoneScreenPercentage)), - width, height); + final Rect normalRegion = new Rect(0, + Math.round(height * (1 - mBouncerZoneScreenPercentage)), + width, height); - if (!isBouncerShowing && exclusionRect != null) { + if (exclusionRect != null) { int lowestBottom = Math.min(Math.max(0, exclusionRect.bottom), minAllowableBottom); normalRegion.top = Math.max(normalRegion.top, lowestBottom); } @@ -322,8 +306,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { : KeyguardBouncerConstants.EXPANSION_HIDDEN; // Log the swiping up to show Bouncer event. - if (!mBouncerInitiallyShowing - && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { + if (expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { mUiEventLogger.log(DreamEvent.DREAM_SWIPED); } @@ -335,17 +318,15 @@ public class BouncerSwipeTouchHandler implements TouchHandler { } } - private ValueAnimator createExpansionAnimator(float targetExpansion, float expansionHeight) { + private ValueAnimator createExpansionAnimator(float targetExpansion) { final ValueAnimator animator = mValueAnimatorCreator.create(mCurrentExpansion, targetExpansion); animator.addUpdateListener( animation -> { float expansionFraction = (float) animation.getAnimatedValue(); - float dragDownAmount = expansionFraction * expansionHeight; setPanelExpansion(expansionFraction); }); - if (!mBouncerInitiallyShowing - && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { + if (targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { animator.addListener( new AnimatorListenerAdapter() { @Override @@ -381,8 +362,7 @@ public class BouncerSwipeTouchHandler implements TouchHandler { final float viewHeight = mTouchSession.getBounds().height(); final float currentHeight = viewHeight * mCurrentExpansion; final float targetHeight = viewHeight * expansion; - final float expansionHeight = targetHeight - currentHeight; - final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight); + final ValueAnimator animator = createExpansionAnimator(expansion); if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) { // Hides the bouncer, i.e., fully expands the space above the bouncer. mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity, diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java index 9ef9938ab8ad..9c7fc9dd307f 100644 --- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.java @@ -23,7 +23,8 @@ import android.graphics.Region; import android.view.GestureDetector; import android.view.MotionEvent; -import com.android.systemui.shade.ShadeViewController; +import androidx.annotation.NonNull; + import com.android.systemui.statusbar.phone.CentralSurfaces; import java.util.Optional; @@ -37,29 +38,34 @@ import javax.inject.Named; */ public class ShadeTouchHandler implements TouchHandler { private final Optional<CentralSurfaces> mSurfaces; - private final ShadeViewController mShadeViewController; private final int mInitiationHeight; + /** + * Tracks whether or not we are capturing a given touch. Will be null before and after a touch. + */ + private Boolean mCapture; + @Inject ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces, - ShadeViewController shadeViewController, @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) { mSurfaces = centralSurfaces; - mShadeViewController = shadeViewController; mInitiationHeight = initiationHeight; } @Override public void onSessionStart(TouchSession session) { - if (mSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) { + if (mSurfaces.isEmpty()) { session.pop(); return; } - session.registerInputListener(ev -> { - mShadeViewController.handleExternalTouch((MotionEvent) ev); + session.registerCallback(() -> mCapture = null); + session.registerInputListener(ev -> { if (ev instanceof MotionEvent) { + if (mCapture != null && mCapture) { + mSurfaces.get().handleExternalShadeWindowTouch((MotionEvent) ev); + } if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { session.pop(); } @@ -68,15 +74,25 @@ public class ShadeTouchHandler implements TouchHandler { session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() { @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) { - return true; + if (mCapture == null) { + // Only capture swipes that are going downwards. + mCapture = Math.abs(distanceY) > Math.abs(distanceX) && distanceY < 0; + if (mCapture) { + // Send the initial touches over, as the input listener has already + // processed these touches. + mSurfaces.get().handleExternalShadeWindowTouch(e1); + mSurfaces.get().handleExternalShadeWindowTouch(e2); + } + } + return mCapture; } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { - return true; + return mCapture; } }); } diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index a667de2351d7..970699f8c623 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -28,8 +28,9 @@ import android.os.ParcelFileDescriptor import android.os.UserHandle import android.util.Log import com.android.app.tracing.traceSection -import com.android.systemui.Flags.communalHub import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED +import com.android.systemui.communal.data.backup.CommunalBackupHelper +import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.domain.backup.CommunalPrefsBackupHelper import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper @@ -59,6 +60,7 @@ open class BackupHelper : BackupAgentHelper() { "systemui.keyguard.quickaffordance.shared_preferences" private const val COMMUNAL_PREFS_BACKUP_KEY = "systemui.communal.shared_preferences" + private const val COMMUNAL_STATE_BACKUP_KEY = "systemui.communal_state" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" const val PERMISSION_SELF = "com.android.systemui.permission.SELF" @@ -89,6 +91,10 @@ open class BackupHelper : BackupAgentHelper() { userId = userHandle.identifier, ) ) + addHelper( + COMMUNAL_STATE_BACKUP_KEY, + CommunalBackupHelper(userHandle, CommunalBackupUtils(context = this)), + ) } } @@ -116,7 +122,7 @@ open class BackupHelper : BackupAgentHelper() { } private fun communalEnabled(): Boolean { - return resources.getBoolean(R.bool.config_communalServiceEnabled) && communalHub() + return resources.getBoolean(R.bool.config_communalServiceEnabled) } /** diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt new file mode 100644 index 000000000000..cdeeb6ff0b23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal + +import android.appwidget.AppWidgetManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.android.systemui.CoreStartable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.widgets.CommunalWidgetModule +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import javax.inject.Inject + +@SysUISingleton +class CommunalBackupRestoreStartable +@Inject +constructor( + private val broadcastDispatcher: BroadcastDispatcher, + private val communalInteractor: CommunalInteractor, + @CommunalLog logBuffer: LogBuffer, +) : CoreStartable, BroadcastReceiver() { + + private val logger = Logger(logBuffer, TAG) + + override fun start() { + broadcastDispatcher.registerReceiver( + receiver = this, + filter = IntentFilter(AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED), + ) + } + + override fun onReceive(context: Context?, intent: Intent?) { + if (intent == null) { + logger.w("On app widget host restored, but intent is null") + return + } + + if (intent.action != AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED) { + return + } + + val hostId = intent.getIntExtra(AppWidgetManager.EXTRA_HOST_ID, 0) + if (hostId != CommunalWidgetModule.APP_WIDGET_HOST_ID) { + return + } + + val oldIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS) + val newIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS) + + if (oldIds == null || newIds == null || oldIds.size != newIds.size) { + logger.w("On app widget host restored, but old to new ids mapping is invalid") + communalInteractor.abortRestoreWidgets() + return + } + + val oldToNewWidgetIdMap = oldIds.zip(newIds).toMap() + communalInteractor.restoreWidgets(oldToNewWidgetIdMap) + } + + companion object { + const val TAG = "CommunalBackupRestoreStartable" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt index 5a174b9d2f80..f437032d0ddb 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal import android.provider.Settings +import android.service.dreams.Flags.dreamTracksFocus import com.android.compose.animation.scene.SceneKey import com.android.systemui.CoreStartable import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -25,11 +26,13 @@ import com.android.systemui.communal.shared.model.CommunalTransitionKeys import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dock.DockManager 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.TransitionStep +import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.util.kotlin.emitOnStart import com.android.systemui.util.kotlin.sample import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -37,15 +40,18 @@ import com.android.systemui.util.settings.SystemSettings import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * A [CoreStartable] responsible for automatically navigating between communal scenes when certain @@ -61,8 +67,10 @@ constructor( private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val keyguardInteractor: KeyguardInteractor, private val systemSettings: SystemSettings, + private val notificationShadeWindowController: NotificationShadeWindowController, @Application private val applicationScope: CoroutineScope, @Background private val bgScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, ) : CoreStartable { private var screenTimeout: Int = DEFAULT_SCREEN_TIMEOUT @@ -134,6 +142,16 @@ constructor( } } } + + if (dreamTracksFocus()) { + bgScope.launch { + communalInteractor.isIdleOnCommunal.collectLatest { + withContext(mainDispatcher) { + notificationShadeWindowController.setGlanceableHubShowing(it) + } + } + } + } } private suspend fun determineSceneAfterTransition( diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 27af99ef8014..7fa091ac6455 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.communal.dagger +import android.content.Context +import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule @@ -78,5 +80,13 @@ interface CommunalModule { ) return SceneDataSourceDelegator(applicationScope, config) } + + @Provides + @SysUISingleton + fun providesCommunalBackupUtils( + @Application context: Context, + ): CommunalBackupUtils { + return CommunalBackupUtils(context) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupHelper.kt new file mode 100644 index 000000000000..b95c966e27b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupHelper.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.backup + +import android.app.backup.BackupDataInputStream +import android.app.backup.BackupDataOutput +import android.app.backup.BackupHelper +import android.os.ParcelFileDescriptor +import android.os.UserHandle +import android.util.Log +import com.android.systemui.Flags.communalHub +import com.android.systemui.communal.proto.toByteArray +import java.io.IOException + +/** Helps with backup & restore of the communal hub widgets. */ +class CommunalBackupHelper( + private val userHandle: UserHandle, + private val communalBackupUtils: CommunalBackupUtils, +) : BackupHelper { + + override fun performBackup( + oldState: ParcelFileDescriptor?, + data: BackupDataOutput?, + newState: ParcelFileDescriptor? + ) { + if (!communalHub()) { + Log.d(TAG, "Skipping backup. Communal not enabled") + return + } + + if (data == null) { + Log.e(TAG, "Backup failed. Data is null") + return + } + + if (!userHandle.isSystem) { + Log.d(TAG, "Backup skipped for non-system user") + return + } + + val state = communalBackupUtils.getCommunalHubState() + Log.i(TAG, "Backing up communal state: $state") + + val bytes = state.toByteArray() + try { + data.writeEntityHeader(ENTITY_KEY, bytes.size) + data.writeEntityData(bytes, bytes.size) + } catch (e: IOException) { + Log.e(TAG, "Backup failed while writing data: ${e.localizedMessage}") + return + } + + Log.i(TAG, "Backup complete") + } + + override fun restoreEntity(data: BackupDataInputStream?) { + if (data == null) { + Log.e(TAG, "Restore failed. Data is null") + return + } + + if (!userHandle.isSystem) { + Log.d(TAG, "Restore skipped for non-system user") + return + } + + if (data.key != ENTITY_KEY) { + Log.d(TAG, "Restore skipped due to mismatching entity key") + return + } + + val dataSize = data.size() + val bytes = ByteArray(dataSize) + try { + data.read(bytes, /* offset= */ 0, dataSize) + } catch (e: IOException) { + Log.e(TAG, "Restore failed while reading data: ${e.localizedMessage}") + return + } + + try { + communalBackupUtils.writeBytesToDisk(bytes) + } catch (e: Exception) { + Log.e(TAG, "Restore failed while writing to disk: ${e.localizedMessage}") + return + } + + Log.i(TAG, "Restore complete") + } + + override fun writeNewStateDescription(newState: ParcelFileDescriptor?) { + // Do nothing because there is no partial backup + } + + companion object { + private const val TAG = "CommunalBackupHelper" + + const val ENTITY_KEY = "communal_hub_state" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt new file mode 100644 index 000000000000..a8e5174494a1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/backup/CommunalBackupUtils.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.backup + +import android.content.Context +import androidx.annotation.WorkerThread +import com.android.systemui.communal.data.db.CommunalDatabase +import com.android.systemui.communal.nano.CommunalHubState +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking + +/** Utilities for communal backup and restore. */ +class CommunalBackupUtils( + private val context: Context, +) { + + /** + * Retrieves a communal hub state protobuf that represents the current state of the communal + * database. + */ + @WorkerThread + fun getCommunalHubState(): CommunalHubState { + val database = CommunalDatabase.getInstance(context) + val widgetsFromDb = runBlocking { database.communalWidgetDao().getWidgets().first() } + val widgetsState = mutableListOf<CommunalHubState.CommunalWidgetItem>() + widgetsFromDb.keys.forEach { rankItem -> + widgetsState.add( + CommunalHubState.CommunalWidgetItem().apply { + rank = rankItem.rank + widgetId = widgetsFromDb[rankItem]!!.widgetId + componentName = widgetsFromDb[rankItem]?.componentName + } + ) + } + return CommunalHubState().apply { widgets = widgetsState.toTypedArray() } + } + + /** + * Writes [data] to disk as a file as [FILE_NAME], overwriting existing content if any. + * + * @throws FileNotFoundException if the file exists but is a directory rather than a regular + * file, does not exist but cannot be created, or cannot be opened for any other reason. + * @throws SecurityException if write access is denied. + * @throws IOException if writing fails. + */ + @WorkerThread + fun writeBytesToDisk(data: ByteArray) { + val output = FileOutputStream(getFile()) + output.write(data) + output.close() + } + + /** + * Reads bytes from [FILE_NAME], and throws if file does not exist. + * + * @throws FileNotFoundException if file does not exist. + * @throws SecurityException if read access is denied. + * @throws IOException if reading fails. + */ + @WorkerThread + fun readBytesFromDisk(): ByteArray { + val input = FileInputStream(getFile()) + val bytes = input.readAllBytes() + input.close() + + return bytes + } + + /** + * Removes the bytes written to disk at [FILE_NAME]. + * + * @return True if and only if the file is successfully deleted + * @throws SecurityException if permission is denied + */ + @WorkerThread + fun clear(): Boolean { + return getFile().delete() + } + + /** Whether [FILE_NAME] exists. */ + @WorkerThread + fun fileExists(): Boolean { + return getFile().exists() + } + + @WorkerThread + private fun getFile(): File { + return File(context.filesDir, FILE_NAME) + } + + companion object { + private const val FILE_NAME = "communal_restore" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt index 595d3200a1e0..3ce81094b696 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt @@ -16,10 +16,53 @@ package com.android.systemui.communal.data.db +import android.content.Context +import androidx.annotation.VisibleForTesting import androidx.room.Database +import androidx.room.Room import androidx.room.RoomDatabase +import com.android.systemui.res.R @Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 1) abstract class CommunalDatabase : RoomDatabase() { abstract fun communalWidgetDao(): CommunalWidgetDao + + companion object { + private var instance: CommunalDatabase? = null + + /** + * Gets a singleton instance of the communal database. If this is called for the first time + * globally, a new instance is created. + * + * @param context The context the database is created in. Only effective when a new instance + * is created. + * @param callback An optional callback registered to the database. Only effective when a + * new instance is created. + */ + fun getInstance( + context: Context, + callback: Callback? = null, + ): CommunalDatabase { + if (instance == null) { + instance = + Room.databaseBuilder( + context, + CommunalDatabase::class.java, + context.resources.getString(R.string.config_communalDatabase) + ) + .also { builder -> + builder.fallbackToDestructiveMigration(dropAllTables = false) + callback?.let { callback -> builder.addCallback(callback) } + } + .build() + } + + return instance!! + } + + @VisibleForTesting + fun setInstance(database: CommunalDatabase) { + instance = database + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt index e7662904b37e..b8161fd07e89 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabaseModule.kt @@ -17,10 +17,8 @@ package com.android.systemui.communal.data.db import android.content.Context -import androidx.room.Room import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.res.R import dagger.Module import dagger.Provides @@ -33,14 +31,7 @@ interface CommunalDatabaseModule { @Application context: Context, defaultWidgetPopulation: DefaultWidgetPopulation, ): CommunalDatabase { - return Room.databaseBuilder( - context, - CommunalDatabase::class.java, - context.resources.getString(R.string.config_communalDatabase) - ) - .fallbackToDestructiveMigration() - .addCallback(defaultWidgetPopulation) - .build() + return CommunalDatabase.getInstance(context, defaultWidgetPopulation) } @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 9cd77c4713fa..d174fd1c97ea 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -23,6 +23,7 @@ import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.Transaction import androidx.sqlite.db.SupportSQLiteDatabase +import com.android.systemui.communal.nano.CommunalHubState import com.android.systemui.communal.widgets.CommunalWidgetHost import com.android.systemui.communal.widgets.CommunalWidgetModule.Companion.DEFAULT_WIDGETS import com.android.systemui.dagger.qualifiers.Application @@ -116,6 +117,10 @@ interface CommunalWidgetDao { @Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid") fun updateItemRank(itemUid: Long, order: Int) + @Query("DELETE FROM communal_widget_table") fun clearCommunalWidgetsTable() + + @Query("DELETE FROM communal_item_rank_table") fun clearCommunalItemRankTable() + @Transaction fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { widgetIdToPriorityMap.forEach { (id, priority) -> @@ -128,9 +133,18 @@ interface CommunalWidgetDao { @Transaction fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long { - return insertWidget( + return addWidget( widgetId = widgetId, componentName = provider.flattenToString(), + priority = priority, + ) + } + + @Transaction + fun addWidget(widgetId: Int, componentName: String, priority: Int): Long { + return insertWidget( + widgetId = widgetId, + componentName = componentName, itemId = insertItemRank(priority), ) } @@ -145,4 +159,13 @@ interface CommunalWidgetDao { deleteWidgets(widget) return true } + + /** Wipes current database and restores the snapshot represented by [state]. */ + @Transaction + fun restoreCommunalHubState(state: CommunalHubState) { + clearCommunalWidgetsTable() + clearCommunalItemRankTable() + + state.widgets.forEach { addWidget(it.widgetId, it.componentName, it.rank) } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index e395ca9bf314..1f54e70fa21b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -16,13 +16,17 @@ package com.android.systemui.communal.data.repository +import android.app.backup.BackupManager import android.appwidget.AppWidgetManager import android.content.ComponentName import android.os.UserHandle import androidx.annotation.WorkerThread +import com.android.systemui.communal.data.backup.CommunalBackupUtils import com.android.systemui.communal.data.db.CommunalItemRank import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem +import com.android.systemui.communal.nano.CommunalHubState +import com.android.systemui.communal.proto.toCommunalHubState import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.CommunalWidgetHost @@ -69,6 +73,15 @@ interface CommunalWidgetRepository { * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. */ fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} + + /** + * Restores the database by reading a state file from disk and updating the widget ids according + * to [oldToNewWidgetIdMap]. + */ + fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) + + /** Aborts the restore process and removes files from disk if necessary. */ + fun abortRestoreWidgets() } @SysUISingleton @@ -82,6 +95,8 @@ constructor( private val communalWidgetHost: CommunalWidgetHost, private val communalWidgetDao: CommunalWidgetDao, @CommunalLog logBuffer: LogBuffer, + private val backupManager: BackupManager, + private val backupUtils: CommunalBackupUtils, ) : CommunalWidgetRepository { companion object { const val TAG = "CommunalWidgetRepository" @@ -143,6 +158,7 @@ constructor( provider = provider, priority = priority, ) + backupManager.dataChanged() } else { appWidgetHost.deleteAppWidgetId(id) } @@ -155,6 +171,7 @@ constructor( if (communalWidgetDao.deleteWidgetById(widgetId)) { appWidgetHost.deleteAppWidgetId(widgetId) logger.i("Deleted widget with id $widgetId.") + backupManager.dataChanged() } } } @@ -165,6 +182,76 @@ constructor( logger.i({ "Updated the order of widget list with ids: $str1." }) { str1 = widgetIdToPriorityMap.toString() } + backupManager.dataChanged() + } + } + + override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { + bgScope.launch { + // Read restored state file from disk + val state: CommunalHubState + try { + state = backupUtils.readBytesFromDisk().toCommunalHubState() + } catch (e: Exception) { + logger.e({ "Failed reading restore data from disk: $str1" }) { + str1 = e.localizedMessage + } + abortRestoreWidgets() + return@launch + } + + val widgetsWithHost = appWidgetHost.appWidgetIds.toList() + val widgetsToRemove = widgetsWithHost.toMutableList() + + // Produce a new state to be restored, skipping invalid widgets + val newWidgets = + state.widgets.mapNotNull { restoredWidget -> + val newWidgetId = + oldToNewWidgetIdMap[restoredWidget.widgetId] ?: restoredWidget.widgetId + + // Skip if widget id is not registered with the host + if (!widgetsWithHost.contains(newWidgetId)) { + logger.d({ + "Skipped restoring widget (old:$int1 new:$int2) " + + "because it is not registered with host" + }) { + int1 = restoredWidget.widgetId + int2 = newWidgetId + } + return@mapNotNull null + } + + widgetsToRemove.remove(newWidgetId) + + CommunalHubState.CommunalWidgetItem().apply { + widgetId = newWidgetId + componentName = restoredWidget.componentName + rank = restoredWidget.rank + } + } + val newState = CommunalHubState().apply { widgets = newWidgets.toTypedArray() } + + // Restore database + logger.i("Restoring communal database $newState") + communalWidgetDao.restoreCommunalHubState(newState) + + // Delete restored state file from disk + backupUtils.clear() + + // Remove widgets from host that have not been restored + widgetsToRemove.forEach { widgetId -> + logger.i({ "Deleting widget $int1 from host since it has not been restored" }) { + int1 = widgetId + } + appWidgetHost.deleteAppWidgetId(widgetId) + } + } + } + + override fun abortRestoreWidgets() { + bgScope.launch { + logger.i("Restore widgets aborted") + backupUtils.clear() } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 619e0525acd8..7448e14895c6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -170,6 +170,23 @@ constructor( } /** + * Repopulates the communal widgets database by first reading a backed-up state from disk and + * updating the widget ids indicated by [oldToNewWidgetIdMap]. The backed-up state is removed + * from disk afterwards. + */ + fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { + widgetRepository.restoreWidgets(oldToNewWidgetIdMap) + } + + /** + * Aborts the task of restoring widgets from a backup. The backed up state stored on disk is + * removed. + */ + fun abortRestoreWidgets() { + widgetRepository.abortRestoreWidgets() + } + + /** * Updates the transition state of the hub [SceneTransitionLayout]. * * Note that you must call is with `null` when the UI is done or risk a memory leak. diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/CommunalHubStateExt.kt b/packages/SystemUI/src/com/android/systemui/communal/proto/CommunalHubStateExt.kt new file mode 100644 index 000000000000..2d661cd7d8f7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/proto/CommunalHubStateExt.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.proto + +import com.android.systemui.communal.nano.CommunalHubState +import com.google.protobuf.nano.InvalidProtocolBufferNanoException + +/** Converts a [CommunalHubState] to bytes. */ +fun CommunalHubState.toByteArray(): ByteArray { + return CommunalHubState.toByteArray(this) +} + +/** + * Converts bytes to a [CommunalHubState]. + * + * @throws InvalidProtocolBufferNanoException if parsing fails. + */ +fun ByteArray.toCommunalHubState(): CommunalHubState { + return CommunalHubState.parseFrom(this) +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto new file mode 100644 index 000000000000..08162593b4d5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/proto/communal_hub_state.proto @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +syntax = "proto3"; + +package com.android.systemui.communal; + +option java_multiple_files = true; + +// Represents the state of communal hub for backup & restore. +message CommunalHubState { + // Widgets in the communal hub. + repeated CommunalWidgetItem widgets = 1; + + // Represents a widget in the communal hub. + message CommunalWidgetItem { + // Id of the widget. + int32 widget_id = 1; + + // Component name of the widget. + string component_name = 2; + + // Rank or order of the widget in the communal hub. + int32 rank = 3; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt index 60fb8d4840cb..aa6516d54563 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.CoroutineScope @Module interface CommunalWidgetModule { companion object { - private const val APP_WIDGET_HOST_ID = 116 + const val APP_WIDGET_HOST_ID = 116 const val DEFAULT_WIDGETS = "default_widgets" @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 804c8c416335..1dd37222f29b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -25,6 +25,7 @@ import com.android.systemui.dagger.qualifiers.PerUser; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.people.PeopleProvider; +import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -47,6 +48,7 @@ import dagger.Subcomponent; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.inject.Provider; @@ -160,6 +162,11 @@ public interface SysUIComponent { @PerUser Map<Class<?>, Provider<CoreStartable>> getPerUserStartables(); /** + * Returns {@link CoreStartable} dependencies if there are any. + */ + @Dependencies Map<Class<?>, Set<Class<? extends CoreStartable>>> getStartableDependencies(); + + /** * Member injection into the supplied argument. */ void inject(SystemUIAppComponentFactoryBase factory); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 23fc8ace0223..593196c54dae 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.communal.CommunalDreamStartable +import com.android.systemui.communal.CommunalBackupRestoreStartable import com.android.systemui.communal.CommunalSceneStartable import com.android.systemui.communal.log.CommunalLoggerStartable import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable @@ -342,6 +343,13 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap + @ClassKey(CommunalBackupRestoreStartable::class) + abstract fun bindCommunalBackupRestoreStartable( + impl: CommunalBackupRestoreStartable + ): CoreStartable + + @Binds + @IntoMap @ClassKey(HomeControlsDreamStartable::class) abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 6b85d3039e15..3462164c421b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -18,6 +18,7 @@ package com.android.systemui.dagger; import android.app.INotificationManager; import android.app.Service; +import android.app.backup.BackupManager; import android.content.Context; import android.service.dreams.IDreamManager; @@ -29,6 +30,7 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.BootCompleteCache; import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.CameraProtectionModule; +import com.android.systemui.CoreStartable; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.accessibility.AccessibilityModule; import com.android.systemui.accessibility.data.repository.AccessibilityRepositoryModule; @@ -105,6 +107,7 @@ import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.shade.transition.LargeScreenShadeInterpolatorImpl; import com.android.systemui.shared.condition.Monitor; import com.android.systemui.smartspace.dagger.SmartspaceModule; +import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -162,11 +165,14 @@ import dagger.Module; import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; +import dagger.multibindings.Multibinds; import kotlinx.coroutines.CoroutineScope; import java.util.Collections; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Executor; import javax.inject.Named; @@ -270,6 +276,10 @@ import javax.inject.Named; }) public abstract class SystemUIModule { + @Multibinds + @Dependencies + abstract Map<Class<?>, Set<Class<? extends CoreStartable>>> startableDependencyMap(); + @Binds abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache); @@ -309,6 +319,13 @@ public abstract class SystemUIModule { return new Monitor(executor, Collections.singleton(systemProcessCondition), logBuffer); } + /** Provides the package name for SystemUI. */ + @SysUISingleton + @Provides + static BackupManager provideBackupManager(@Application Context context) { + return new BackupManager(context); + } + @BindsOptionalOf abstract CommandQueue optionalCommandQueue(); diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java index 5a036b17a35b..0c2709e4afed 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java @@ -22,6 +22,7 @@ import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamYPosi import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM; import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.Animator; import android.content.res.Resources; @@ -40,6 +41,8 @@ import com.android.systemui.complication.ComplicationHostViewController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.dreams.dagger.DreamOverlayModule; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.statusbar.BlurUtils; @@ -50,6 +53,8 @@ import java.util.Arrays; import javax.inject.Inject; import javax.inject.Named; +import kotlinx.coroutines.CoroutineDispatcher; + /** * View controller for {@link DreamOverlayContainerView}. */ @@ -62,6 +67,7 @@ public class DreamOverlayContainerViewController extends private final DreamOverlayAnimationsController mDreamOverlayAnimationsController; private final DreamOverlayStateController mStateController; private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; + private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; private final ComplicationHostViewController mComplicationHostViewController; @@ -81,6 +87,7 @@ public class DreamOverlayContainerViewController extends // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates). private final Handler mHandler; + private final CoroutineDispatcher mMainDispatcher; private final int mDreamOverlayMaxTranslationY; private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @@ -88,6 +95,7 @@ public class DreamOverlayContainerViewController extends private boolean mBouncerAnimating; private boolean mWakingUpFromSwipe; + private boolean mAnyBouncerShowing; private final BouncerlessScrimController mBouncerlessScrimController; @@ -170,6 +178,7 @@ public class DreamOverlayContainerViewController extends LowLightTransitionCoordinator lowLightTransitionCoordinator, BlurUtils blurUtils, @Main Handler handler, + @Main CoroutineDispatcher mainDispatcher, @Main Resources resources, @Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset, @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long @@ -178,7 +187,8 @@ public class DreamOverlayContainerViewController extends PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor, DreamOverlayAnimationsController animationsController, DreamOverlayStateController stateController, - BouncerlessScrimController bouncerlessScrimController) { + BouncerlessScrimController bouncerlessScrimController, + KeyguardTransitionInteractor keyguardTransitionInteractor) { super(containerView); mDreamOverlayContentView = contentView; mStatusBarViewController = statusBarViewController; @@ -190,6 +200,8 @@ public class DreamOverlayContainerViewController extends mBouncerlessScrimController = bouncerlessScrimController; mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback); + mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mComplicationHostViewController = complicationHostViewController; mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize( R.dimen.dream_overlay_y_offset); @@ -200,6 +212,7 @@ public class DreamOverlayContainerViewController extends ViewGroup.LayoutParams.MATCH_PARENT)); mHandler = handler; + mMainDispatcher = mainDispatcher; mMaxBurnInOffset = maxBurnInOffset; mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval; mMillisUntilFullJitter = millisUntilFullJitter; @@ -225,6 +238,12 @@ public class DreamOverlayContainerViewController extends mView.getRootSurfaceControl().setTouchableRegion(emptyRegion); emptyRegion.recycle(); + collectFlow( + mView, + mKeyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState), + isFinished -> mAnyBouncerShowing = isFinished, + mMainDispatcher); + // Start dream entry animations. Skip animations for low light clock. if (!mStateController.isLowLightActive()) { // If this is transitioning from the low light dream to the user dream, the overlay @@ -246,6 +265,10 @@ public class DreamOverlayContainerViewController extends return mView; } + boolean isBouncerShowing() { + return mAnyBouncerShowing; + } + private void updateBurnInOffsets() { // Make sure the offset starts at zero, to avoid a big jump in the overlay when it first // appears. diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 1135afeb6f83..a9ef53104c31 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -19,9 +19,11 @@ package com.android.systemui.dreams; import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE; import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_MANAGER; import static com.android.systemui.dreams.dagger.DreamModule.HOME_CONTROL_PANEL_DREAM_COMPONENT; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.graphics.drawable.ColorDrawable; import android.util.Log; import android.view.View; @@ -34,7 +36,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.LifecycleService; +import androidx.lifecycle.ServiceLifecycleDispatcher; import androidx.lifecycle.ViewModelStore; import com.android.dream.lowlight.dagger.LowLightDreamModule; @@ -45,15 +50,21 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.ambient.touch.TouchMonitor; import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent; +import com.android.systemui.ambient.touch.scrim.ScrimManager; +import com.android.systemui.communal.domain.interactor.CommunalInteractor; +import com.android.systemui.communal.shared.model.CommunalScenes; import com.android.systemui.complication.Complication; import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; import java.util.Arrays; import java.util.HashSet; +import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; @@ -63,7 +74,8 @@ import javax.inject.Named; * dream reaches directly out to the service with a Window reference (via LayoutParams), which the * service uses to insert its own child Window into the dream's parent Window. */ -public class DreamOverlayService extends android.service.dreams.DreamOverlayService { +public class DreamOverlayService extends android.service.dreams.DreamOverlayService implements + LifecycleOwner { private static final String TAG = "DreamOverlayService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -76,6 +88,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private DreamOverlayContainerViewController mDreamOverlayContainerViewController; private final DreamOverlayCallbackController mDreamOverlayCallbackController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; + private final ScrimManager mScrimManager; @Nullable private final ComponentName mLowLightDreamComponent; @Nullable @@ -93,6 +106,21 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // True if the service has been destroyed. private boolean mDestroyed = false; + /** + * True if the notification shade is open. + */ + private boolean mShadeExpanded = false; + + /** + * True if any part of the glanceable hub is visible. + */ + private boolean mCommunalVisible = false; + + /** + * True if the primary bouncer is visible. + */ + private boolean mBouncerShowing = false; + private final ComplicationComponent mComplicationComponent; private final AmbientTouchComponent mAmbientTouchComponent; @@ -102,27 +130,72 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final DreamOverlayComponent mDreamOverlayComponent; - private final DreamOverlayLifecycleOwner mLifecycleOwner; + /** + * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch + * handling, should be active. It will automatically be paused when the dream overlay is hidden + * while dreaming, such as when the notification shade, bouncer, or glanceable hub are visible. + */ private final LifecycleRegistry mLifecycleRegistry; + /** + * Drives the lifecycle exposed by this service's {@link #getLifecycle()}. + * <p> + * Used to mimic a {@link LifecycleService}, though we do not update the lifecycle in + * {@link #onBind(Intent)} since it's final in the base class. + */ + private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this); + private TouchMonitor mTouchMonitor; + private final CommunalInteractor mCommunalInteractor; + + private final SystemDialogsCloser mSystemDialogsCloser; + private final KeyguardUpdateMonitorCallback mKeyguardCallback = new KeyguardUpdateMonitorCallback() { @Override public void onShadeExpandedChanged(boolean expanded) { mExecutor.execute(() -> { - if (getCurrentStateLocked() != Lifecycle.State.RESUMED - && getCurrentStateLocked() != Lifecycle.State.STARTED) { + if (mShadeExpanded == expanded) { return; } + mShadeExpanded = expanded; - setCurrentStateLocked( - expanded ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); + updateLifecycleStateLocked(); }); } }; + private final Consumer<Boolean> mCommunalVisibleConsumer = new Consumer<>() { + @Override + public void accept(Boolean communalVisible) { + mExecutor.execute(() -> { + if (mCommunalVisible == communalVisible) { + return; + } + + mCommunalVisible = communalVisible; + + updateLifecycleStateLocked(); + }); + } + }; + + private final Consumer<Boolean> mBouncerShowingConsumer = new Consumer<>() { + @Override + public void accept(Boolean bouncerShowing) { + mExecutor.execute(() -> { + if (mBouncerShowing == bouncerShowing) { + return; + } + + mBouncerShowing = bouncerShowing; + + updateLifecycleStateLocked(); + }); + } + }; + private final DreamOverlayStateController.Callback mExitAnimationFinishedCallback = new DreamOverlayStateController.Callback() { @Override @@ -168,19 +241,24 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ AmbientTouchComponent.Factory ambientTouchComponentFactory, DreamOverlayStateController stateController, KeyguardUpdateMonitor keyguardUpdateMonitor, + ScrimManager scrimManager, + CommunalInteractor communalInteractor, + SystemDialogsCloser systemDialogsCloser, UiEventLogger uiEventLogger, @Named(DREAM_TOUCH_INSET_MANAGER) TouchInsetManager touchInsetManager, @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) - ComponentName lowLightDreamComponent, + ComponentName lowLightDreamComponent, @Nullable @Named(HOME_CONTROL_PANEL_DREAM_COMPONENT) - ComponentName homeControlPanelDreamComponent, + ComponentName homeControlPanelDreamComponent, DreamOverlayCallbackController dreamOverlayCallbackController, + KeyguardInteractor keyguardInteractor, @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) { super(executor); mContext = context; mExecutor = executor; mWindowManager = windowManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; + mScrimManager = scrimManager; mLowLightDreamComponent = lowLightDreamComponent; mHomeControlPanelDreamComponent = homeControlPanelDreamComponent; mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); @@ -188,6 +266,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mUiEventLogger = uiEventLogger; mDreamOverlayCallbackController = dreamOverlayCallbackController; mWindowTitle = windowTitle; + mCommunalInteractor = communalInteractor; + mSystemDialogsCloser = systemDialogsCloser; final ViewModelStore viewModelStore = new ViewModelStore(); final Complication.Host host = @@ -203,10 +283,32 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ new HashSet<>(Arrays.asList( mDreamComplicationComponent.getHideComplicationTouchHandler(), mDreamOverlayComponent.getCommunalTouchHandler()))); - mLifecycleOwner = lifecycleOwner; - mLifecycleRegistry = mLifecycleOwner.getRegistry(); + mLifecycleRegistry = lifecycleOwner.getRegistry(); + + mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED)); - mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED)); + collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(), + mCommunalVisibleConsumer); + collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing, + mBouncerShowingConsumer); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return mDispatcher.getLifecycle(); + } + + @Override + public void onCreate() { + mDispatcher.onServicePreSuperOnCreate(); + super.onCreate(); + } + + @Override + public void onStart(Intent intent, int startId) { + mDispatcher.onServicePreSuperOnStart(); + super.onStart(intent, startId); } @Override @@ -214,19 +316,20 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback); mExecutor.execute(() -> { - setCurrentStateLocked(Lifecycle.State.DESTROYED); + setLifecycleStateLocked(Lifecycle.State.DESTROYED); resetCurrentDreamOverlayLocked(); mDestroyed = true; }); + mDispatcher.onServicePreSuperOnDestroy(); super.onDestroy(); } @Override public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { - setCurrentStateLocked(Lifecycle.State.STARTED); + setLifecycleStateLocked(Lifecycle.State.STARTED); mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START); @@ -256,7 +359,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ return; } - setCurrentStateLocked(Lifecycle.State.RESUMED); + setLifecycleStateLocked(Lifecycle.State.RESUMED); mStateController.setOverlayActive(true); final ComponentName dreamComponent = getDreamComponent(); mStateController.setLowLightActive( @@ -276,14 +379,27 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ resetCurrentDreamOverlayLocked(); } - private Lifecycle.State getCurrentStateLocked() { + private Lifecycle.State getLifecycleStateLocked() { return mLifecycleRegistry.getCurrentState(); } - private void setCurrentStateLocked(Lifecycle.State state) { + private void setLifecycleStateLocked(Lifecycle.State state) { mLifecycleRegistry.setCurrentState(state); } + private void updateLifecycleStateLocked() { + if (getLifecycleStateLocked() != Lifecycle.State.RESUMED + && getLifecycleStateLocked() != Lifecycle.State.STARTED) { + return; + } + + // If anything is on top of the dream, we should stop touch handling. + boolean shouldPause = mShadeExpanded || mCommunalVisible || mBouncerShowing; + + setLifecycleStateLocked( + shouldPause ? Lifecycle.State.STARTED : Lifecycle.State.RESUMED); + } + @Override public void onWakeUp() { if (mDreamOverlayContainerViewController != null) { @@ -292,6 +408,28 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } } + @Override + public void onComeToFront() { + // Make sure the bouncer is closed. Expanding the shade effectively contracts the bouncer + // an equal amount. + if (mDreamOverlayContainerViewController != null + && mDreamOverlayContainerViewController.isBouncerShowing()) { + mScrimManager.getCurrentController().expand( + new ShadeExpansionChangeEvent( + /* fraction= */ 1.f, + /* expanded= */ false, + /* tracking= */ true)); + } + + // closeSystemDialogs takes care of closing anything that responds to the + // {@link Intent.ACTION_CLOSE_SYSTEM_DIALOGS} broadcast (which includes the notification + // shade). + mSystemDialogsCloser.closeSystemDialogs(); + + // Hide glanceable hub (this is a nop if glanceable hub is not open). + mCommunalInteractor.changeScene(CommunalScenes.Blank, null); + } + /** * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be * called from the main executing thread. The window attributes closely mirror those that are diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java new file mode 100644 index 000000000000..6e7239a4a98e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams; + +/** Defines an interface for a class that is responsible for closing system dialogs. */ +public interface SystemDialogsCloser { + /** Close any open system dialogs. */ + void closeSystemDialogs(); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java index 31710ac4cc4f..516b8c521ca1 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -31,6 +31,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayNotificationCountProvider; import com.android.systemui.dreams.DreamOverlayService; +import com.android.systemui.dreams.SystemDialogsCloser; import com.android.systemui.dreams.complication.dagger.ComplicationComponent; import com.android.systemui.dreams.homecontrols.DreamActivityProvider; import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl; @@ -146,6 +147,15 @@ public interface DreamModule { return Optional.empty(); } + /** + * Provides an implementation for {@link SystemDialogsCloser} that calls + * {@link Context.closeSystemDialogs}. + */ + @Provides + static SystemDialogsCloser providesSystemDialogsCloser(Context context) { + return () -> context.closeSystemDialogs(); + } + /** */ @Provides @Named(DREAM_ONLY_ENABLED_FOR_DOCK_USER) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java index 1c047ddcd3d8..fff0c58eecb8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -98,7 +98,7 @@ public class CommunalTouchHandler implements TouchHandler { // Notification shade window has its own logic to be visible if the hub is open, no need to // do anything here other than send touch events over. session.registerInputListener(ev -> { - surfaces.handleDreamTouch((MotionEvent) ev); + surfaces.handleExternalShadeWindowTouch((MotionEvent) ev); if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { var unused = session.pop(); } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt index 304fdd61a992..ca6c8da380fe 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/HapticSliderViewBinder.kt @@ -23,11 +23,11 @@ import kotlinx.coroutines.awaitCancellation object HapticSliderViewBinder { /** - * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a + * Binds a [SeekbarHapticPlugin] to a [View]. The binded view should be a * [android.widget.SeekBar] or a container of a [android.widget.SeekBar] */ @JvmStatic - fun bind(view: View?, plugin: SeekableSliderHapticPlugin) { + fun bind(view: View?, plugin: SeekbarHapticPlugin) { view?.repeatWhenAttached { plugin.startInScope(lifecycleScope) try { diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt deleted file mode 100644 index cfa5294567b7..000000000000 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderEventProducer.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.haptics.slider - -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -/** An event producer for a Seekable element such as the Android [SeekBar] */ -class SeekableSliderEventProducer : SliderEventProducer, OnSeekBarChangeListener { - - /** The current event reported by a SeekBar */ - private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f)) - - override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow() - - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - val eventType = - if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER - else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM - - _currentEvent.value = SliderEvent(eventType, normalizeProgress(seekBar, progress)) - } - - /** - * Normalize the integer progress of a SeekBar to the range from 0F to 1F. - * - * @param[seekBar] The SeekBar that reports a progress. - * @param[progress] The integer progress of the SeekBar within its min and max values. - * @return The progress in the range from 0F to 1F. - */ - private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float { - if (seekBar.max == seekBar.min) { - return 1.0f - } - val range = seekBar.max - seekBar.min - return (progress - seekBar.min) / range.toFloat() - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - _currentEvent.update { previousEvent -> - SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, previousEvent.currentProgress) - } - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - _currentEvent.update { previousEvent -> - SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, previousEvent.currentProgress) - } - } - - /** The arrow navigation that was operating the slider has stopped. */ - fun onArrowUp() { - _currentEvent.update { previousEvent -> - SliderEvent(SliderEventType.ARROW_UP, previousEvent.currentProgress) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt index ed82278a7346..2007db3448e2 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt @@ -30,12 +30,12 @@ import kotlinx.coroutines.launch /** * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback. * - * A [SeekableSliderEventProducer] is used as the producer of slider events, a + * A [SliderStateProducer] is used as the producer of slider events, a * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback - * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that + * depending on the state, and a [SliderStateTracker] is used as the state machine handler that * tracks and manipulates the slider state. */ -class SeekableSliderHapticPlugin +class SeekbarHapticPlugin @JvmOverloads constructor( vibratorHelper: VibratorHelper, @@ -46,7 +46,7 @@ constructor( private val velocityTracker = VelocityTracker.obtain() - private val sliderEventProducer = SeekableSliderEventProducer() + private val sliderEventProducer = SliderStateProducer() private val sliderHapticFeedbackProvider = SliderHapticFeedbackProvider( @@ -56,7 +56,7 @@ constructor( systemClock, ) - private var sliderTracker: SeekableSliderTracker? = null + private var sliderTracker: SliderStateTracker? = null private var pluginScope: CoroutineScope? = null @@ -86,7 +86,7 @@ constructor( fun startInScope(scope: CoroutineScope) { if (sliderTracker != null) stop() sliderTracker = - SeekableSliderTracker( + SliderStateTracker( sliderHapticFeedbackProvider, sliderEventProducer, scope, @@ -116,28 +116,52 @@ constructor( /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */ fun onStartTrackingTouch(seekBar: SeekBar) { if (isTracking) { - sliderEventProducer.onStartTrackingTouch(seekBar) + sliderEventProducer.onStartTracking(true) } } /** onProgressChanged event from the slider's [android.widget.SeekBar] */ fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (isTracking) { - sliderEventProducer.onProgressChanged(seekBar, progress, fromUser) + if (sliderTracker?.currentState == SliderState.IDLE && !fromUser) { + // This case translates to the slider starting to track program changes + sliderEventProducer.resetWithProgress(normalizeProgress(seekBar, progress)) + sliderEventProducer.onStartTracking(false) + } else { + sliderEventProducer.onProgressChanged( + fromUser, + normalizeProgress(seekBar, progress), + ) + } + } + } + + /** + * Normalize the integer progress of a SeekBar to the range from 0F to 1F. + * + * @param[seekBar] The SeekBar that reports a progress. + * @param[progress] The integer progress of the SeekBar within its min and max values. + * @return The progress in the range from 0F to 1F. + */ + private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float { + if (seekBar.max == seekBar.min) { + return 1.0f } + val range = seekBar.max - seekBar.min + return (progress - seekBar.min) / range.toFloat() } /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */ fun onStopTrackingTouch(seekBar: SeekBar) { if (isTracking) { - sliderEventProducer.onStopTrackingTouch(seekBar) + sliderEventProducer.onStopTracking(true) } } - /** onArrowUp event recorded */ - fun onArrowUp() { + /** Programmatic changes have stopped */ + private fun onStoppedTrackingProgram() { if (isTracking) { - sliderEventProducer.onArrowUp() + sliderEventProducer.onStopTracking(false) } } @@ -146,7 +170,7 @@ constructor( * * This event is used to estimate the key-up event based on a running a timer as a waiting * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event. - * Therefore, [onArrowUp] must be called after the timeout. + * Therefore, [onStoppedTrackingProgram] must be called after the timeout. */ fun onKeyDown() { if (!isTracking) return @@ -158,7 +182,7 @@ constructor( keyUpJob = pluginScope?.launch { delay(KEY_UP_TIMEOUT) - onArrowUp() + onStoppedTrackingProgram() } } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt index 4a63941b3f8c..0edef993b782 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderEventType.kt @@ -22,6 +22,8 @@ enum class SliderEventType { NOTHING, /* The slider has captured a touch input and is tracking touch events. */ STARTED_TRACKING_TOUCH, + /* The slider started tracking programmatic value changes */ + STARTED_TRACKING_PROGRAM, /* The slider progress is changing due to user touch input. */ PROGRESS_CHANGE_BY_USER, /* The slider progress is changing programmatically. */ @@ -29,5 +31,5 @@ enum class SliderEventType { /* The slider has stopped tracking touch events. */ STOPPED_TRACKING_TOUCH, /* The external (not touch) stimulus that was modifying the slider progress has stopped. */ - ARROW_UP, + STOPPED_TRACKING_PROGRAM, } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt new file mode 100644 index 000000000000..1124ab1eb1d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateProducer.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.slider + +import androidx.annotation.FloatRange +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** A stateful producer of [SliderEvent] */ +class SliderStateProducer : SliderEventProducer { + + /** The current event of a slider */ + private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f)) + + override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow() + + fun onProgressChanged(fromUser: Boolean, @FloatRange(from = 0.0, to = 1.0) progress: Float) { + val eventType = + if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER + else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM + + _currentEvent.value = SliderEvent(eventType, progress) + } + + fun onStartTracking(fromUser: Boolean) { + val eventType = + if (fromUser) SliderEventType.STARTED_TRACKING_TOUCH + else SliderEventType.STARTED_TRACKING_PROGRAM + _currentEvent.update { previousEvent -> + SliderEvent(eventType, previousEvent.currentProgress) + } + } + + fun onStopTracking(fromUser: Boolean) { + val eventType = + if (fromUser) SliderEventType.STOPPED_TRACKING_TOUCH + else SliderEventType.STOPPED_TRACKING_PROGRAM + _currentEvent.update { previousEvent -> + SliderEvent(eventType, previousEvent.currentProgress) + } + } + + fun resetWithProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) { + _currentEvent.value = SliderEvent(SliderEventType.NOTHING, progress) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt index 0af303843a45..14cf4110272f 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt @@ -25,11 +25,12 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch /** - * Slider tracker attached to a seekable slider. + * Slider tracker attached to a slider. * - * The tracker runs a state machine to execute actions on touch-based events typical of a seekable - * slider such as [android.widget.SeekBar]. Coroutines responsible for running the state machine, - * collecting slider events and maintaining waiting states are run on the provided [CoroutineScope]. + * The tracker runs a state machine to execute actions on touch-based events typical of a general + * slider (including a [android.widget.SeekBar]). Coroutines responsible for running the state + * machine, collecting slider events and maintaining waiting states are run on the provided + * [CoroutineScope]. * * @param[sliderStateListener] Listener of the slider state. * @param[sliderEventProducer] Producer of slider events arising from the slider. @@ -37,7 +38,7 @@ import kotlinx.coroutines.launch * events and the launch of timer jobs. * @property[config] Configuration parameters of the slider tracker. */ -class SeekableSliderTracker( +class SliderStateTracker( sliderStateListener: SliderStateListener, sliderEventProducer: SliderEventProducer, trackerScope: CoroutineScope, @@ -79,7 +80,7 @@ class SeekableSliderTracker( // This will disambiguate between an imprecise touch that acquires the slider handle, // and a select and jump operation in the slider track. setState(SliderState.WAIT) - } else if (newEventType == SliderEventType.PROGRESS_CHANGE_BY_PROGRAM) { + } else if (newEventType == SliderEventType.STARTED_TRACKING_PROGRAM) { val state = if (bookendReached(currentProgress)) SliderState.ARROW_HANDLE_REACHED_BOOKEND else SliderState.ARROW_HANDLE_MOVED_ONCE @@ -227,7 +228,7 @@ class SeekableSliderTracker( } SliderEventType.PROGRESS_CHANGE_BY_PROGRAM -> SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY - SliderEventType.ARROW_UP -> SliderState.IDLE + SliderEventType.STOPPED_TRACKING_PROGRAM -> SliderState.IDLE else -> SliderState.ARROW_HANDLE_MOVED_ONCE } setState(nextState) @@ -237,7 +238,7 @@ class SeekableSliderTracker( val reachedBookend = bookendReached(currentProgress) val nextState = when (newEventType) { - SliderEventType.ARROW_UP -> SliderState.IDLE + SliderEventType.STOPPED_TRACKING_PROGRAM -> SliderState.IDLE SliderEventType.STARTED_TRACKING_TOUCH -> { // Launching the timer and going to WAIT timerJob = launchTimer() diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt new file mode 100644 index 000000000000..692fbb06e88c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperActivity.kt @@ -0,0 +1,149 @@ +/* + * 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.keyboard.shortcut + +import android.graphics.Insets +import android.os.Bundle +import android.view.View +import android.view.WindowInsets +import androidx.activity.BackEventCompat +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback +import androidx.core.view.updatePadding +import com.android.systemui.res.R +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN + +/** + * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for + * small and large screen devices. + */ +class ShortcutHelperActivity : ComponentActivity() { + + private val bottomSheetContainer + get() = requireViewById<View>(R.id.shortcut_helper_sheet_container) + + private val bottomSheet + get() = requireViewById<View>(R.id.shortcut_helper_sheet) + + private val bottomSheetBehavior + get() = BottomSheetBehavior.from(bottomSheet) + + override fun onCreate(savedInstanceState: Bundle?) { + setupEdgeToEdge() + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_keyboard_shortcut_helper) + setUpBottomSheetWidth() + setUpInsets() + setUpPredictiveBack() + setUpSheetDismissListener() + setUpDismissOnTouchOutside() + } + + private fun setupEdgeToEdge() { + // Draw behind system bars + window.setDecorFitsSystemWindows(false) + } + + private fun setUpBottomSheetWidth() { + val sheetScreenWidthFraction = + resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction) + // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no + // effect. + val screenWidth = resources.displayMetrics.widthPixels + bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt() + } + + private fun setUpInsets() { + bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets -> + val safeDrawingInsets = insets.safeDrawing + // Make sure the bottom sheet is not covered by the status bar. + bottomSheetContainer.updatePadding(top = safeDrawingInsets.top) + // Make sure the contents inside of the bottom sheet are not hidden by system bars, or + // cutouts. + bottomSheet.updatePadding( + left = safeDrawingInsets.left, + right = safeDrawingInsets.right, + bottom = safeDrawingInsets.bottom + ) + // The bottom sheet has to be expanded only after setting up insets, otherwise there is + // a bug and it will not use full height. + expandBottomSheet() + + // Return CONSUMED if you don't want want the window insets to keep passing + // down to descendant views. + WindowInsets.CONSUMED + } + } + + private fun expandBottomSheet() { + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetBehavior.skipCollapsed = true + } + + private fun setUpPredictiveBack() { + val onBackPressedCallback = + object : OnBackPressedCallback(/* enabled= */ true) { + override fun handleOnBackStarted(backEvent: BackEventCompat) { + bottomSheetBehavior.startBackProgress(backEvent) + } + + override fun handleOnBackProgressed(backEvent: BackEventCompat) { + bottomSheetBehavior.updateBackProgress(backEvent) + } + + override fun handleOnBackPressed() { + bottomSheetBehavior.handleBackInvoked() + } + + override fun handleOnBackCancelled() { + bottomSheetBehavior.cancelBackProgress() + } + } + onBackPressedDispatcher.addCallback( + owner = this, + onBackPressedCallback = onBackPressedCallback + ) + } + + private fun setUpSheetDismissListener() { + bottomSheetBehavior.addBottomSheetCallback( + object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == STATE_HIDDEN) { + finish() + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + } + ) + } + + private fun setUpDismissOnTouchOutside() { + bottomSheetContainer.setOnClickListener { finish() } + } +} + +private val WindowInsets.safeDrawing + get() = + getInsets(WindowInsets.Type.systemBars()) + .union(getInsets(WindowInsets.Type.ime())) + .union(getInsets(WindowInsets.Type.displayCutout())) + +private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 7224536cfe70..d19176853387 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -226,7 +226,7 @@ constructor( val ambientIndicationVisible: Flow<Boolean> = repository.ambientIndicationVisible.asStateFlow() /** Whether the primary bouncer is showing or not. */ - val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow + @JvmField val primaryBouncerShowing: Flow<Boolean> = bouncerRepository.primaryBouncerShow /** Whether the alternate bouncer is showing or not. */ val alternateBouncerShowing: Flow<Boolean> = bouncerRepository.alternateBouncerVisible diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index 92612b824974..7d0553937f25 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -87,6 +87,7 @@ enum class KeyguardState { } /** Whether either of the bouncers are visible when we're FINISHED in the given state. */ + @JvmStatic fun isBouncerState(state: KeyguardState): Boolean { return state == PRIMARY_BOUNCER || state == ALTERNATE_BOUNCER } diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index 5dafd94f05e9..c9976177144f 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -17,6 +17,7 @@ package com.android.systemui.lifecycle +import android.os.Trace import android.view.View import android.view.ViewTreeObserver import androidx.annotation.MainThread @@ -73,7 +74,7 @@ fun View.repeatWhenAttached( Dispatchers.Main + createCoroutineTracingContext() + coroutineContext val traceName = if (Compile.IS_DEBUG && coroutineTracing()) { - traceSectionName() + inferTraceSectionName() } else { DEFAULT_TRACE_NAME } @@ -197,16 +198,21 @@ private fun isFrameInteresting(frame: StackWalker.StackFrame): Boolean = frame.className != CURRENT_CLASS_NAME && frame.className != JAVA_ADAPTER_CLASS_NAME /** Get a name for the trace section include the name of the call site. */ -private fun traceSectionName(): String { - val interestingFrame = - StackWalker.getInstance().walk { stream -> - stream.filter(::isFrameInteresting).limit(5).findFirst() +private fun inferTraceSectionName(): String { + try { + Trace.traceBegin(Trace.TRACE_TAG_APP, "RepeatWhenAttachedKt#inferTraceSectionName") + val interestingFrame = + StackWalker.getInstance().walk { stream -> + stream.filter(::isFrameInteresting).limit(5).findFirst() + } + if (interestingFrame.isPresent) { + val f = interestingFrame.get() + return "${f.className}#${f.methodName}:${f.lineNumber} [$DEFAULT_TRACE_NAME]" + } else { + return DEFAULT_TRACE_NAME } - if (interestingFrame.isPresent) { - val frame = interestingFrame.get() - return "${frame.className}#${frame.methodName}:${frame.lineNumber} [$DEFAULT_TRACE_NAME]" - } else { - return DEFAULT_TRACE_NAME + } finally { + Trace.traceEnd(Trace.TRACE_TAG_APP) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index bd3893bd2701..e6c785ef41f0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -57,6 +57,7 @@ import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.os.Process; import android.os.Trace; +import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; @@ -738,11 +739,11 @@ public class MediaControlPanel { mPackageName, mMediaViewHolder.getSeamlessButton()); } else { mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId); - // TODO: b/321969740 - Populate the userHandle parameter. The user - // handle is necessary to disambiguate the same package running on - // different users. mMediaOutputDialogManager.createAndShow( - mPackageName, true, mMediaViewHolder.getSeamlessButton(), null); + mPackageName, + /* aboveStatusBar */ true, + mMediaViewHolder.getSeamlessButton(), + UserHandle.getUserHandleForUid(mUid)); } } else { mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId); @@ -770,11 +771,11 @@ public class MediaControlPanel { Log.w(TAG, "Device pending intent is not an activity."); } } else { - // TODO: b/321969740 - Populate the userHandle parameter. The user - // handle is necessary to disambiguate the same package running on - // different users. mMediaOutputDialogManager.createAndShow( - mPackageName, true, mMediaViewHolder.getSeamlessButton(), null); + mPackageName, + /* aboveStatusBar */ true, + mMediaViewHolder.getSeamlessButton(), + UserHandle.getUserHandleForUid(mUid)); } } }); 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 9790a529a107..d5b05ef68288 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 @@ -1348,15 +1348,12 @@ public class InternetDialogController implements AccessPointController.AccessPoi mDefaultDataSubId = defaultDataSubId; } - boolean mayLaunchShareWifiSettings(WifiEntry wifiEntry) { + boolean mayLaunchShareWifiSettings(WifiEntry wifiEntry, View view) { Intent intent = getConfiguratorQrCodeGeneratorIntentOrNull(wifiEntry); if (intent == null) { return false; } - if (mCallback != null) { - mCallback.dismissDialog(); - } - mActivityStarter.startActivity(intent, false /* dismissShade */); + startActivity(intent, view); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java index 24089a710f6c..1a881b63720f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java @@ -395,7 +395,7 @@ public class InternetDialogDelegate implements }); mDoneButton.setOnClickListener(v -> dialog.dismiss()); mShareWifiButton.setOnClickListener(v -> { - if (mInternetDialogController.mayLaunchShareWifiSettings(mConnectedWifiEntry)) { + if (mInternetDialogController.mayLaunchShareWifiSettings(mConnectedWifiEntry, v)) { mUiEventLogger.log(InternetDialogEvent.SHARE_WIFI_QS_BUTTON_CLICKED); } }); diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 8397d9f1e7b9..37f2a21b145e 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -47,7 +47,9 @@ import androidx.annotation.Nullable; import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; +import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.DisplayTracker; @@ -370,10 +372,18 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mBackgroundHandler.post(new Runnable() { @Override public void run() { - mControl.setEnforcedAdmin( + int userId = mUserTracker.getUserId(); + RestrictedLockUtils.EnforcedAdmin enforcedAdmin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_CONFIG_BRIGHTNESS, - mUserTracker.getUserId())); + userId); + if (Flags.enforceBrightnessBaseUserRestriction() && enforcedAdmin == null + && RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, + UserManager.DISALLOW_CONFIG_BRIGHTNESS, + userId)) { + enforcedAdmin = new RestrictedLockUtils.EnforcedAdmin(); + } + mControl.setEnforcedAdmin(enforcedAdmin); } }); } 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 b425fb997d9e..083cee73f591 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -33,7 +33,7 @@ import com.android.settingslib.RestrictedLockUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.classifier.Classifier; import com.android.systemui.haptics.slider.HapticSliderViewBinder; -import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; +import com.android.systemui.haptics.slider.SeekbarHapticPlugin; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.res.R; @@ -65,7 +65,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final FalsingManager mFalsingManager; private final UiEventLogger mUiEventLogger; - private final SeekableSliderHapticPlugin mBrightnessSliderHapticPlugin; + private final SeekbarHapticPlugin mBrightnessSliderHapticPlugin; private final ActivityStarter mActivityStarter; private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() { @@ -89,7 +89,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV BrightnessSliderView brightnessSliderView, FalsingManager falsingManager, UiEventLogger uiEventLogger, - SeekableSliderHapticPlugin brightnessSliderHapticPlugin, + SeekbarHapticPlugin brightnessSliderHapticPlugin, ActivityStarter activityStarter) { super(brightnessSliderView); mFalsingManager = falsingManager; @@ -314,7 +314,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV int layout = getLayout(); BrightnessSliderView root = (BrightnessSliderView) LayoutInflater.from(context) .inflate(layout, viewRoot, false); - SeekableSliderHapticPlugin plugin = new SeekableSliderHapticPlugin( + SeekbarHapticPlugin plugin = new SeekbarHapticPlugin( mVibratorHelper, mSystemClock); if (hapticBrightnessSlider()) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index a8481cde8ff0..a5a547403af9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -28,10 +28,14 @@ import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.ambient.touch.TouchMonitor +import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.ui.compose.CommunalContainer @@ -45,6 +49,8 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.kotlin.collectFlow import javax.inject.Inject @@ -67,31 +73,32 @@ constructor( private val shadeInteractor: ShadeInteractor, private val powerManager: PowerManager, private val communalColors: CommunalColors, - @Communal private val dataSourceDelegator: SceneDataSourceDelegator, -) { + private val ambientTouchComponentFactory: AmbientTouchComponent.Factory, + @Communal private val dataSourceDelegator: SceneDataSourceDelegator +) : LifecycleOwner { /** The container view for the hub. This will not be initialized until [initView] is called. */ private var communalContainerView: View? = null /** - * The width of the area in which a right edge swipe can open the hub, in pixels. Read from - * resources when [initView] is called. + * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle + * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything, + * such as the notification shade or bouncer. */ - // TODO(b/320786721): support RTL layouts - private var rightEdgeSwipeRegionWidth: Int = 0 + private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) /** - * The height of the area in which a top edge swipe while the hub is open will not intercept - * touches, in pixels. This allows the top edge swipe to instead open the notification shade. - * Read from resources when [initView] is called. + * This [TouchMonitor] listens for top and bottom swipe gestures globally when the hub is open. + * When a top or bottom swipe is detected, they will be intercepted and used to open the + * notification shade/bouncer. */ - private var topEdgeSwipeRegionWidth: Int = 0 + private var touchMonitor: TouchMonitor? = null /** - * The height of the area in which a bottom edge swipe while the hub is open will not intercept - * touches, in pixels. This allows the bottom edge swipe to instead open the bouncer. Read from + * The width of the area in which a right edge swipe can open the hub, in pixels. Read from * resources when [initView] is called. */ - private var bottomEdgeSwipeRegionWidth: Int = 0 + // TODO(b/320786721): support RTL layouts + private var rightEdgeSwipeRegionWidth: Int = 0 /** * True if we are currently tracking a gesture for opening the hub that started in the edge @@ -102,9 +109,6 @@ constructor( /** True if we are currently tracking a touch on the hub while it's open. */ private var isTrackingHubTouch = false - /** True if we are tracking a top or bottom swipe gesture while the hub is open. */ - private var isTrackingHubGesture = false - /** * True if the hub UI is fully open, meaning it should receive touch input. * @@ -121,9 +125,15 @@ constructor( private var anyBouncerShowing = false /** - * True if the shade is fully expanded, meaning the hub should not receive any touch input. + * True if the shade is fully expanded and the user is not interacting with it anymore, meaning + * the hub should not receive any touch input. * - * Tracks [ShadeInteractor.isAnyFullyExpanded]. + * We need to not pause the touch handling lifecycle as soon as the shade opens because if the + * user swipes down, then back up without lifting their finger, the lifecycle will be paused + * then resumed, and resuming force-stops all active touch sessions. This means the shade will + * not receive the end of the gesture and will be stuck open. + * + * Based on [ShadeInteractor.isAnyFullyExpanded] and [ShadeInteractor.isUserInteracting]. */ private var shadeShowing = false @@ -132,8 +142,6 @@ constructor( * and just let the dream overlay's touch handling deal with them. * * Tracks [KeyguardInteractor.isDreaming]. - * - * TODO(b/328838259): figure out a proper solution for touch handling above the lock screen too */ private var isDreaming = false @@ -192,28 +200,45 @@ constructor( throw RuntimeException("Communal view has already been initialized") } + if (touchMonitor == null) { + touchMonitor = + ambientTouchComponentFactory.create(this, HashSet()).getTouchMonitor().apply { + init() + } + } + lifecycleRegistry.currentState = Lifecycle.State.CREATED + communalContainerView = containerView rightEdgeSwipeRegionWidth = containerView.resources.getDimensionPixelSize( R.dimen.communal_right_edge_swipe_region_width ) - topEdgeSwipeRegionWidth = - containerView.resources.getDimensionPixelSize( - R.dimen.communal_top_edge_swipe_region_height - ) - bottomEdgeSwipeRegionWidth = - containerView.resources.getDimensionPixelSize( - R.dimen.communal_bottom_edge_swipe_region_height - ) collectFlow( containerView, keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState), - { anyBouncerShowing = it } + { + anyBouncerShowing = it + updateLifecycleState() + } + ) + collectFlow( + containerView, + communalInteractor.isCommunalShowing, + { + hubShowing = it + updateLifecycleState() + } + ) + collectFlow( + containerView, + and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)), + { + shadeShowing = it + updateLifecycleState() + } ) - collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it }) - collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it }) collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it }) communalContainerView = containerView @@ -221,10 +246,24 @@ constructor( return containerView } + /** + * Updates the lifecycle stored by the [lifecycleRegistry] to control when the [touchMonitor] + * should listen for and intercept top and bottom swipes. + */ + private fun updateLifecycleState() { + val shouldInterceptGestures = hubShowing && !(shadeShowing || anyBouncerShowing) + if (shouldInterceptGestures) { + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + } else { + lifecycleRegistry.currentState = Lifecycle.State.STARTED + } + } + /** Removes the container view from its parent. */ fun disposeView() { communalContainerView?.let { (it.parent as ViewGroup).removeView(it) + lifecycleRegistry.currentState = Lifecycle.State.CREATED communalContainerView = null } } @@ -262,15 +301,7 @@ constructor( if (isDown && !hubOccluded) { // Only intercept down events if the hub isn't occluded by the bouncer or // notification shade. - val y = ev.rawY - val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth - val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth - - if (topSwipe || bottomSwipe) { - isTrackingHubGesture = true - } else { - isTrackingHubTouch = true - } + isTrackingHubTouch = true } if (isTrackingHubTouch) { @@ -283,19 +314,6 @@ constructor( // gesture // may return false from dispatchTouchEvent. return true - } else if (isTrackingHubGesture) { - // Tracking a top or bottom swipe on the hub UI. - if (isUp || isCancel) { - isTrackingHubGesture = false - } - - // If we're dreaming, intercept touches so the hub UI doesn't receive them, but - // don't do anything so that the dream's touch handling takes care of opening - // the bouncer or shade. - // - // If we're not dreaming, we don't intercept touches at the top/bottom edge so that - // swipes can open the notification shade and bouncer. - return isDreaming } return false @@ -347,4 +365,7 @@ constructor( 0 ) } + + override val lifecycle: Lifecycle + get() = lifecycleRegistry } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index adcb14a67983..8b7e11c4ab47 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -448,6 +448,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW || mScreenOffAnimationController.shouldIgnoreKeyguardTouches()) { mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } else if (state.glanceableHubShowing) { + mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; + mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM; } else if (state.isKeyguardShowingAndNotOccluded() || panelFocusable) { mLpChanged.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE; // Make sure to remove FLAG_ALT_FOCUSABLE_IM when keyguard needs input. @@ -611,6 +614,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW state.panelVisible, state.shadeOrQsExpanded, state.notificationShadeFocusable, + state.glanceableHubShowing, state.bouncerShowing, state.keyguardFadingAway, state.keyguardGoingAway, @@ -740,6 +744,12 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } @Override + public void setGlanceableHubShowing(boolean showing) { + mCurrentState.glanceableHubShowing = showing; + apply(mCurrentState); + } + + @Override public void setBackdropShowing(boolean showing) { mCurrentState.mediaBackdropShowing = showing; apply(mCurrentState); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index e0a98b3b7f9e..6a4b52af498c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -35,6 +35,7 @@ class NotificationShadeWindowState( @JvmField var shadeOrQsExpanded: Boolean = false, @JvmField var notificationShadeFocusable: Boolean = false, @JvmField var bouncerShowing: Boolean = false, + @JvmField var glanceableHubShowing: Boolean = false, @JvmField var keyguardFadingAway: Boolean = false, @JvmField var keyguardGoingAway: Boolean = false, @JvmField var qsExpanded: Boolean = false, @@ -79,6 +80,7 @@ class NotificationShadeWindowState( shadeOrQsExpanded.toString(), notificationShadeFocusable.toString(), bouncerShowing.toString(), + glanceableHubShowing.toString(), keyguardFadingAway.toString(), keyguardGoingAway.toString(), qsExpanded.toString(), @@ -119,6 +121,7 @@ class NotificationShadeWindowState( panelVisible: Boolean, panelExpanded: Boolean, notificationShadeFocusable: Boolean, + glanceableHubShowing: Boolean, bouncerShowing: Boolean, keyguardFadingAway: Boolean, keyguardGoingAway: Boolean, @@ -149,6 +152,7 @@ class NotificationShadeWindowState( this.panelVisible = panelVisible this.shadeOrQsExpanded = panelExpanded this.notificationShadeFocusable = notificationShadeFocusable + this.glanceableHubShowing = glanceableHubShowing this.bouncerShowing = bouncerShowing this.keyguardFadingAway = keyguardFadingAway this.keyguardGoingAway = keyguardGoingAway @@ -197,6 +201,7 @@ class NotificationShadeWindowState( "panelVisible", "panelExpanded", "notificationShadeFocusable", + "glanceableHubShowing", "bouncerShowing", "keyguardFadingAway", "keyguardGoingAway", diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 907cf5eb6886..44f86da7431e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -25,7 +25,6 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.app.StatusBarManager; import android.util.Log; import android.view.GestureDetector; -import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -74,14 +73,14 @@ import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.util.time.SystemClock; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + import java.io.PrintWriter; import java.util.Optional; import java.util.function.Consumer; import javax.inject.Inject; -import kotlinx.coroutines.ExperimentalCoroutinesApi; - /** * Controller for {@link NotificationShadeWindowView}. */ @@ -137,6 +136,11 @@ public class NotificationShadeWindowViewController implements Dumpable { private final PanelExpansionInteractor mPanelExpansionInteractor; private final ShadeExpansionStateManager mShadeExpansionStateManager; + /** + * If {@code true}, an external touch sent in {@link #handleExternalTouch(MotionEvent)} has been + * intercepted and all future touch events for the gesture should be processed by this view. + */ + private boolean mExternalTouchIntercepted = false; private boolean mIsTrackingBarGesture = false; private boolean mIsOcclusionTransitionRunning = false; private DisableSubpixelTextTransitionListener mDisableSubpixelTextTransitionListener; @@ -253,11 +257,28 @@ public class NotificationShadeWindowViewController implements Dumpable { } /** - * Handle a touch event while dreaming by forwarding the event to the content view. + * Handle a touch event while dreaming or on the hub by forwarding the event to the content + * view. + * <p> + * Since important logic for handling touches lives in the dispatch/intercept phases, we + * simulate going through all of these stages before sending onTouchEvent if intercepted. + * * @param event The event to forward. */ - public void handleDreamTouch(MotionEvent event) { - mView.dispatchTouchEvent(event); + public void handleExternalTouch(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + mExternalTouchIntercepted = false; + } + + if (!mView.dispatchTouchEvent(event)) { + return; + } + if (!mExternalTouchIntercepted) { + mExternalTouchIntercepted = mView.onInterceptTouchEvent(event); + } + if (mExternalTouchIntercepted) { + mView.onTouchEvent(event); + } } /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */ diff --git a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt index 8eed0975579d..396c5f2db605 100644 --- a/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt @@ -16,7 +16,8 @@ package com.android.systemui.startable import com.android.systemui.CoreStartable -import kotlin.reflect.KClass +import java.lang.annotation.Documented +import javax.inject.Qualifier /** * Allows a [CoreStartable] to declare that it must be started after its dependencies. @@ -24,7 +25,4 @@ import kotlin.reflect.KClass * This creates a partial, topological ordering. See [com.android.systemui.SystemUIApplication] for * how this ordering is enforced at runtime. */ -@MustBeDocumented -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -annotation class Dependencies(vararg val value: KClass<*> = []) +@Qualifier @Documented @Retention(AnnotationRetention.RUNTIME) annotation class Dependencies() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java index e6695568361e..707d59aa560d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java @@ -86,6 +86,9 @@ public interface NotificationShadeWindowController extends RemoteInputController /** Sets the state of whether the bouncer is showing or not. */ default void setBouncerShowing(boolean showing) {} + /** Sets the state of whether the glanceable hub is showing or not. */ + default void setGlanceableHubShowing(boolean showing) {} + /** Sets the state of whether the backdrop is showing or not. */ default void setBackdropShowing(boolean showing) {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java index d2fe20d9c50c..8104755b5e7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SysuiStatusBarStateController.java @@ -23,7 +23,6 @@ import android.view.View; import com.android.systemui.CoreStartable; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.startable.Dependencies; import com.android.systemui.statusbar.phone.CentralSurfaces; import java.lang.annotation.Retention; @@ -31,7 +30,6 @@ import java.lang.annotation.Retention; /** * Sends updates to {@link StateListener}s about changes to the status bar state and dozing state */ -@Dependencies(CentralSurfaces.class) public interface SysuiStatusBarStateController extends StatusBarStateController, CoreStartable { // TODO: b/115739177 (remove this explicit ordering if we can) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 2651d2eed404..7d9742849a15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -37,6 +37,7 @@ import androidx.lifecycle.LifecycleOwner; import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.AuthKeyguardMessageArea; +import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.animation.RemoteAnimationRunnerCompat; @@ -50,7 +51,7 @@ import com.android.systemui.util.Compile; import java.io.PrintWriter; /** */ -public interface CentralSurfaces extends Dumpable, LifecycleOwner { +public interface CentralSurfaces extends Dumpable, LifecycleOwner, CoreStartable { boolean MULTIUSER_DEBUG = false; // Should match the values in PhoneWindowManager String SYSTEM_DIALOG_REASON_KEY = "reason"; @@ -182,6 +183,9 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { return contextForUser.getPackageManager(); } + /** Default impl for CoreStartable. */ + default void start() {} + boolean updateIsKeyguard(); boolean updateIsKeyguard(boolean forceStateChange); @@ -279,11 +283,12 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void awakenDreams(); /** - * Handle a touch event while dreaming when the touch was initiated within a prescribed - * swipeable area. This method is provided for cases where swiping in certain areas of a dream - * should be handled by CentralSurfaces instead (e.g. swiping communal hub open). + * Handle a touch event while dreaming or on the glanceable hub when the touch was initiated + * within a prescribed swipeable area. This method is provided for cases where swiping in + * certain areas should be handled by CentralSurfaces instead (e.g. swiping hub open, opening + * the notification shade over dream or hub). */ - void handleDreamTouch(MotionEvent event); + void handleExternalShadeWindowTouch(MotionEvent event); boolean isBouncerShowing(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt index 8af7ee8389e5..d5e66ff660c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt @@ -79,7 +79,7 @@ abstract class CentralSurfacesEmptyImpl : CentralSurfaces { override fun updateScrimController() {} override fun shouldIgnoreTouch() = false override fun isDeviceInteractive() = false - override fun handleDreamTouch(event: MotionEvent?) {} + override fun handleExternalShadeWindowTouch(event: MotionEvent?) {} override fun awakenDreams() {} override fun isBouncerShowing() = false override fun isBouncerShowingScrimmed() = false 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 e9aa7aa57b1c..b2b2ceaa9017 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2928,8 +2928,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override - public void handleDreamTouch(MotionEvent event) { - getNotificationShadeWindowViewController().handleDreamTouch(event); + public void handleExternalShadeWindowTouch(MotionEvent event) { + getNotificationShadeWindowViewController().handleExternalTouch(event); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 22455417b647..cd82e1dcbb8d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -118,7 +118,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.Prefs; import com.android.systemui.dump.DumpManager; import com.android.systemui.haptics.slider.HapticSliderViewBinder; -import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; +import com.android.systemui.haptics.slider.SeekbarHapticPlugin; import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig; import com.android.systemui.media.dialog.MediaOutputDialogManager; import com.android.systemui.plugins.VolumeDialog; @@ -2640,7 +2640,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ObjectAnimator anim; // slider progress animation for non-touch-related updates private int animTargetProgress; private int lastAudibleLevel = 1; - private SeekableSliderHapticPlugin mHapticPlugin; + private SeekbarHapticPlugin mHapticPlugin; private int mProgressHapticsType = PROGRESS_HAPTICS_DISABLED; void setIcon(int iconRes, Resources.Theme theme) { @@ -2658,7 +2658,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, com.android.systemui.util.time.SystemClock systemClock) { if (mHapticPlugin != null) return; - mHapticPlugin = new SeekableSliderHapticPlugin( + mHapticPlugin = new SeekbarHapticPlugin( vibratorHelper, systemClock, sSliderHapticFeedbackConfig); diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt index eebb6fb42488..22c053099ac5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt @@ -21,7 +21,7 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.media.dialog.MediaOutputDialogManager -import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlayback +import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlaybackState import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject @@ -33,10 +33,10 @@ constructor( private val mediaOutputDialogManager: MediaOutputDialogManager, ) { - fun onBarClick(sessionWithPlayback: SessionWithPlayback?, expandable: Expandable) { - if (sessionWithPlayback?.playback?.isActive == true) { + fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable) { + if (sessionWithPlaybackState?.isPlaybackActive == true) { mediaOutputDialogManager.createAndShowWithController( - sessionWithPlayback.session.packageName, + sessionWithPlaybackState.session.packageName, false, expandable.dialogController() ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index 41ad0358eae2..83b80294cfb6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -94,7 +94,7 @@ constructor( /** Currently connected [MediaDevice]. */ val currentConnectedDevice: Flow<MediaDevice?> = - localMediaRepository.flatMapLatest { it.currentConnectedDevice } + localMediaRepository.flatMapLatest { it.currentConnectedDevice }.distinctUntilChanged() private suspend fun getApplicationLabel(packageName: String): CharSequence? { return try { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlayback.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlaybackState.kt index c4476fc26113..bac969d040e2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlayback.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlaybackState.kt @@ -16,9 +16,7 @@ package com.android.systemui.volume.panel.component.mediaoutput.shared.model -import android.media.session.PlaybackState - -data class SessionWithPlayback( +data class SessionWithPlaybackState( val session: MediaDeviceSession, - val playback: PlaybackState, + val isPlaybackActive: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index f19fa20bd999..d60d981a3be1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -25,7 +25,7 @@ import com.android.systemui.res.R import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlayback +import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlaybackState import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.shared.model.Result import com.android.systemui.volume.panel.ui.VolumePanelUiEvent @@ -35,10 +35,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn /** Models the UI of the Media Output Volume Panel component. */ @@ -55,20 +54,19 @@ constructor( private val uiEventLogger: UiEventLogger, ) { - private val sessionWithPlayback: StateFlow<Result<SessionWithPlayback?>> = + private val sessionWithPlaybackState: StateFlow<Result<SessionWithPlaybackState?>> = interactor.defaultActiveMediaSession .flatMapLatest { session -> if (session == null) { - flowOf(Result.Data<SessionWithPlayback?>(null)) + flowOf(Result.Data<SessionWithPlaybackState?>(null)) } else { - mediaDeviceSessionInteractor - .playbackState(session) - .map { playback -> - playback?.let { - Result.Data<SessionWithPlayback?>(SessionWithPlayback(session, it)) - } + mediaDeviceSessionInteractor.playbackState(session).mapNotNull { playback -> + playback?.let { + Result.Data<SessionWithPlaybackState?>( + SessionWithPlaybackState(session, playback.isActive()) + ) } - .filterNotNull() + } } } .stateIn( @@ -78,14 +76,14 @@ constructor( ) val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> = - combine(sessionWithPlayback, interactor.currentConnectedDevice) { + combine(sessionWithPlaybackState, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> if (mediaDeviceSession !is Result.Data) { return@combine null } ConnectedDeviceViewModel( - if (mediaDeviceSession.data?.playback?.isActive == true) { + if (mediaDeviceSession.data?.isPlaybackActive == true) { context.getString( R.string.media_output_label_title, mediaDeviceSession.data.session.appLabel @@ -103,19 +101,16 @@ constructor( ) val deviceIconViewModel: StateFlow<DeviceIconViewModel?> = - combine(sessionWithPlayback, interactor.currentConnectedDevice) { + combine(sessionWithPlaybackState, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> if (mediaDeviceSession !is Result.Data) { return@combine null } - if (mediaDeviceSession.data?.playback?.isActive == true) { - val icon = - currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) } - ?: Icon.Resource( - com.android.internal.R.drawable.ic_bt_headphones_a2dp, - null - ) + val icon: Icon = + currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) } + ?: Icon.Resource(R.drawable.ic_media_home_devices, null) + if (mediaDeviceSession.data?.isPlaybackActive == true) { DeviceIconViewModel.IsPlaying( icon = icon, iconColor = @@ -125,7 +120,7 @@ constructor( ) } else { DeviceIconViewModel.IsNotPlaying( - icon = Icon.Resource(R.drawable.ic_media_home_devices, null), + icon = icon, iconColor = Color.Attribute( com.android.internal.R.attr.materialColorOnSurfaceVariant @@ -143,7 +138,7 @@ constructor( fun onBarClick(expandable: Expandable) { uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED) - val result = sessionWithPlayback.value + val result = sessionWithPlaybackState.value actionsInteractor.onBarClick((result as? Result.Data)?.data, expandable) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt index e157fc508f87..4f7610ab7d72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/SystemUIApplicationTest.kt @@ -27,7 +27,6 @@ import com.android.systemui.dump.dumpManager import com.android.systemui.flags.systemPropertiesHelper import com.android.systemui.kosmos.Kosmos import com.android.systemui.process.processWrapper -import com.android.systemui.startable.Dependencies import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import javax.inject.Provider @@ -56,6 +55,19 @@ class SystemUIApplicationTest : SysuiTestCase() { @Mock private lateinit var bootCompleteCache: BootCompleteCacheImpl @Mock private lateinit var initController: InitController + class StartableA : TestableStartable() + class StartableB : TestableStartable() + class StartableC : TestableStartable() + class StartableD : TestableStartable() + class StartableE : TestableStartable() + + val dependencyMap: Map<Class<*>, Set<Class<out CoreStartable>>> = + mapOf( + StartableC::class.java to setOf(StartableA::class.java), + StartableD::class.java to setOf(StartableA::class.java, StartableB::class.java), + StartableE::class.java to setOf(StartableD::class.java, StartableB::class.java), + ) + private val startableA = StartableA() private val startableB = StartableB() private val startableC = StartableC() @@ -76,6 +88,7 @@ class SystemUIApplicationTest : SysuiTestCase() { whenever(sysuiComponent.provideBootCacheImpl()).thenReturn(bootCompleteCache) whenever(sysuiComponent.createDumpManager()).thenReturn(kosmos.dumpManager) whenever(sysuiComponent.initController).thenReturn(initController) + whenever(sysuiComponent.startableDependencies).thenReturn(dependencyMap) kosmos.processWrapper.systemUser = true app.setContextAvailableCallback(contextAvailableCallback) @@ -168,13 +181,4 @@ class SystemUIApplicationTest : SysuiTestCase() { startOrder++ } } - - class StartableA : TestableStartable() - class StartableB : TestableStartable() - - @Dependencies(StartableA::class) class StartableC : TestableStartable() - - @Dependencies(StartableA::class, StartableB::class) class StartableD : TestableStartable() - - @Dependencies(StartableD::class, StartableB::class) class StartableE : TestableStartable() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt new file mode 100644 index 000000000000..7094848e3127 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.backup + +import android.app.backup.BackupDataInput +import android.app.backup.BackupDataInputStream +import android.app.backup.BackupDataOutput +import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.backup.CommunalBackupUtilsTest.Companion.represents +import com.android.systemui.communal.data.backup.CommunalBackupUtilsTest.FakeWidgetMetadata +import com.android.systemui.communal.data.db.CommunalDatabase +import com.android.systemui.communal.data.db.CommunalWidgetDao +import com.android.systemui.communal.proto.toCommunalHubState +import com.android.systemui.lifecycle.InstantTaskExecutorRule +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalBackupHelperTest : SysuiTestCase() { + @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() + + private lateinit var database: CommunalDatabase + private lateinit var dao: CommunalWidgetDao + private lateinit var backupUtils: CommunalBackupUtils + + // Temporary file used for storing backed-up data. + private lateinit var backupDataFile: File + + private lateinit var underTest: CommunalBackupHelper + + @Before + fun setup() { + database = + Room.inMemoryDatabaseBuilder(context, CommunalDatabase::class.java) + .allowMainThreadQueries() + .build() + CommunalDatabase.setInstance(database) + + dao = database.communalWidgetDao() + backupUtils = CommunalBackupUtils(context) + + backupDataFile = File(context.cacheDir, "backup_data_file") + + underTest = CommunalBackupHelper(UserHandle.SYSTEM, backupUtils) + } + + @After + fun teardown() { + backupDataFile.delete() + database.close() + } + + @Test + @EnableFlags(Flags.FLAG_COMMUNAL_HUB) + fun backupAndRestoreCommunalHub() { + val expectedWidgets = setUpDatabase() + + underTest.performBackup(oldState = null, data = getBackupDataOutput(), newState = null) + underTest.restoreEntity(getBackupDataInputStream()) + + // Verify restored state matches backed-up state + val restoredState = backupUtils.readBytesFromDisk().toCommunalHubState() + val restoredWidgets = restoredState.widgets.toList() + assertThat(restoredWidgets) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(expectedWidgets) + } + + @Test + @DisableFlags(Flags.FLAG_COMMUNAL_HUB) + fun backup_skippedWhenCommunalDisabled() { + setUpDatabase() + + underTest.performBackup(oldState = null, data = getBackupDataOutput(), newState = null) + + // Verify nothing written to the backup + assertThat(backupDataFile.length()).isEqualTo(0) + } + + @Test + @EnableFlags(Flags.FLAG_COMMUNAL_HUB) + fun backup_skippedForNonSystemUser() { + setUpDatabase() + + val helper = CommunalBackupHelper(UserHandle.CURRENT, backupUtils) + helper.performBackup(oldState = null, data = getBackupDataOutput(), newState = null) + + // Verify nothing written to the backup + assertThat(backupDataFile.length()).isEqualTo(0) + } + + private fun setUpDatabase(): List<FakeWidgetMetadata> { + return listOf( + FakeWidgetMetadata(11, "com.android.fakePackage1/fakeWidget1", 3), + FakeWidgetMetadata(12, "com.android.fakePackage2/fakeWidget2", 2), + FakeWidgetMetadata(13, "com.android.fakePackage3/fakeWidget3", 1), + ) + .onEach { dao.addWidget(it.widgetId, it.componentName, it.rank) } + } + + private fun getBackupDataInputStream(): BackupDataInputStream { + val input = BackupDataInput(FileInputStream(backupDataFile).fd).apply { readNextHeader() } + + // Construct BackupDataInputStream using reflection because its constructor is package + // private + val inputStream = BackupDataInputStream::class.constructors.first().call(input) + + // Set key + with(inputStream.javaClass.getDeclaredField("key")) { + isAccessible = true + set(inputStream, input.key) + } + + // Set dataSize + with(inputStream.javaClass.getDeclaredField("dataSize")) { + isAccessible = true + set(inputStream, input.dataSize) + } + + return inputStream + } + + private fun getBackupDataOutput(): BackupDataOutput { + return BackupDataOutput(FileOutputStream(backupDataFile).fd) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt new file mode 100644 index 000000000000..c3849f989b1d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.backup + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.db.CommunalDatabase +import com.android.systemui.communal.data.db.CommunalWidgetDao +import com.android.systemui.communal.nano.CommunalHubState +import com.android.systemui.lifecycle.InstantTaskExecutorRule +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import java.io.FileNotFoundException +import java.nio.charset.Charset +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalBackupUtilsTest : SysuiTestCase() { + @JvmField @Rule val instantTaskExecutor = InstantTaskExecutorRule() + + private lateinit var database: CommunalDatabase + private lateinit var dao: CommunalWidgetDao + private lateinit var underTest: CommunalBackupUtils + + @Before + fun setup() { + database = + Room.inMemoryDatabaseBuilder(context, CommunalDatabase::class.java) + .allowMainThreadQueries() + .build() + CommunalDatabase.setInstance(database) + + dao = database.communalWidgetDao() + underTest = CommunalBackupUtils(context) + } + + @After + fun teardown() { + database.close() + underTest.clear() + } + + @Test + fun getCommunalHubState_returnsExpectedWidgets() { + // Set up database + val expectedWidgets = + listOf( + FakeWidgetMetadata(11, "com.android.fakePackage1/fakeWidget1", 3), + FakeWidgetMetadata(12, "com.android.fakePackage2/fakeWidget2", 2), + FakeWidgetMetadata(13, "com.android.fakePackage3/fakeWidget3", 1), + ) + expectedWidgets.forEach { dao.addWidget(it.widgetId, it.componentName, it.rank) } + + // Get communal hub state + val state = underTest.getCommunalHubState() + val actualWidgets = state.widgets.toList() + + // Verify the state contains widgets as expected + assertThat(actualWidgets) + .comparingElementsUsing(represents) + .containsExactlyElementsIn(expectedWidgets) + } + + @Test + fun write_existingContentIsOverwritten() { + // Write old data + val dataToWrite = "I am old data. Erase me." + underTest.writeBytesToDisk(dataToWrite.toByteArray(Charset.defaultCharset())) + + // Verify old data written + var dataRead = underTest.readBytesFromDisk().toString(Charset.defaultCharset()) + assertThat(dataRead).isEqualTo(dataToWrite) + + // Write new data + val newDataToWrite = "I am new data." + underTest.writeBytesToDisk(newDataToWrite.toByteArray(Charset.defaultCharset())) + + // Verify new data overwrites old + dataRead = underTest.readBytesFromDisk().toString(Charset.defaultCharset()) + assertThat(dataRead).isEqualTo(newDataToWrite) + } + + @Test(expected = FileNotFoundException::class) + fun read_fileNotFoundException() { + underTest.readBytesFromDisk() + } + + @Test(expected = FileNotFoundException::class) + fun clear_returnsTrueWhenFileDeleted() { + // Write bytes to disk + underTest.writeBytesToDisk(byteArrayOf(1, 2, 3)) + + assertThat(underTest.clear()).isTrue() + + // Verify a read after that throws a FileNotFoundException + underTest.readBytesFromDisk() + } + + @Test + fun clear_returnsFalseWhenFileDoesNotExist() { + assertThat(underTest.clear()).isFalse() + } + + @Test + fun fileExists() { + assertThat(underTest.fileExists()).isFalse() + + underTest.writeBytesToDisk(byteArrayOf(1, 2, 3)) + assertThat(underTest.fileExists()).isTrue() + + underTest.clear() + assertThat(underTest.fileExists()).isFalse() + } + + data class FakeWidgetMetadata(val widgetId: Int, val componentName: String, val rank: Int) + + companion object { + /** + * A comparator for whether a [CommunalHubState.CommunalWidgetItem] represents a + * [FakeWidgetMetadata] + */ + val represents: Correspondence<CommunalHubState.CommunalWidgetItem, FakeWidgetMetadata> = + Correspondence.from( + { actual, expected -> + actual?.widgetId == expected?.widgetId && + actual?.componentName == expected?.componentName && + actual?.rank == expected?.rank + }, + "represents", + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt index 20dd913550d3..f77c7a672ae3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt @@ -21,6 +21,7 @@ import androidx.room.Room import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.nano.CommunalHubState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.lifecycle.InstantTaskExecutorRule import com.google.common.truth.Truth.assertThat @@ -224,6 +225,42 @@ class CommunalWidgetDaoTest : SysuiTestCase() { .inOrder() } + @Test + fun restoreCommunalHubState() = + testScope.runTest { + // Set up db + listOf(widgetInfo1, widgetInfo2, widgetInfo3).forEach { addWidget(it) } + + // Restore db to fake state + communalWidgetDao.restoreCommunalHubState(fakeState) + + // Verify db matches new state + val expected = mutableMapOf<CommunalItemRank, CommunalWidgetItem>() + fakeState.widgets.forEachIndexed { index, fakeWidget -> + // Auto-generated uid continues after the initial 3 widgets and starts at 4 + val uid = index + 4L + val rank = CommunalItemRank(uid = uid, rank = fakeWidget.rank) + val widget = + CommunalWidgetItem( + uid = uid, + widgetId = fakeWidget.widgetId, + componentName = fakeWidget.componentName, + itemId = rank.uid, + ) + expected[rank] = widget + } + val widgets by collectLastValue(communalWidgetDao.getWidgets()) + assertThat(widgets).containsExactlyEntriesIn(expected) + } + + private fun addWidget(metadata: FakeWidgetMetadata, priority: Int? = null) { + communalWidgetDao.addWidget( + widgetId = metadata.widgetId, + provider = metadata.provider, + priority = priority ?: metadata.priority, + ) + } + data class FakeWidgetMetadata( val widgetId: Int, val provider: ComponentName, @@ -273,5 +310,22 @@ class CommunalWidgetDaoTest : SysuiTestCase() { componentName = widgetInfo3.provider.flattenToString(), itemId = communalItemRankEntry3.uid, ) + val fakeState = + CommunalHubState().apply { + widgets = + listOf( + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 1 + componentName = "pk_name/fake_widget_1" + rank = 1 + }, + CommunalHubState.CommunalWidgetItem().apply { + widgetId = 2 + componentName = "pk_name/fake_widget_2" + rank = 2 + }, + ) + .toTypedArray() + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt deleted file mode 100644 index c22d35cb214f..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderEventProducerTest.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.haptics.slider - -import android.widget.SeekBar -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import junit.framework.Assert.assertEquals -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class SeekableSliderEventProducerTest : SysuiTestCase() { - - private val seekBar = SeekBar(mContext) - private val eventProducer = SeekableSliderEventProducer() - private val eventFlow = eventProducer.produceEvents() - - @Test - fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest { - val latest by collectLastValue(eventFlow) - - eventProducer.onStartTrackingTouch(seekBar) - - assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest) - } - - @Test - fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest { - val latest by collectLastValue(eventFlow) - - eventProducer.onStopTrackingTouch(seekBar) - - assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest) - } - - @Test - fun onProgressChangeByUser_changeByUserEventProduced_withNormalizedProgress() = runTest { - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, true) - - assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5F), latest) - } - - @Test - fun onProgressChangeByUser_zeroWidthSlider_changeByUserEventProduced_withMaxProgress() = - runTest { - // No-width slider where the min and max values are the same - seekBar.min = 100 - seekBar.max = 100 - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, true) - - assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 1.0F), latest) - } - - @Test - fun onProgressChangeByProgram_changeByProgramEventProduced_withNormalizedProgress() = runTest { - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, false) - - assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 0.5F), latest) - } - - @Test - fun onProgressChangeByProgram_zeroWidthSlider_changeByProgramEventProduced_withMaxProgress() = - runTest { - // No-width slider where the min and max values are the same - seekBar.min = 100 - seekBar.max = 100 - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, false) - - assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 1.0F), latest) - } - - @Test - fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced_withNormalizedProgress() = - runTest { - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, true) - eventProducer.onStartTrackingTouch(seekBar) - - assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5F), latest) - } - - @Test - fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced_withNormalizedProgress() = - runTest { - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, true) - eventProducer.onStopTrackingTouch(seekBar) - - assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5F), latest) - } - - @Test - fun onArrowUp_afterStartTrackingTouch_ArrowUpProduced() = runTest { - val latest by collectLastValue(eventFlow) - - eventProducer.onStartTrackingTouch(seekBar) - eventProducer.onArrowUp() - - assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0f), latest) - } - - @Test - fun onArrowUp_afterChangeByProgram_ArrowUpProduced_withProgress() = runTest { - val progress = 50 - val latest by collectLastValue(eventFlow) - - eventProducer.onProgressChanged(seekBar, progress, false) - eventProducer.onArrowUp() - - assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0.5f), latest) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt index 796d6d9c3359..a09d34579e2f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt @@ -38,11 +38,11 @@ import org.mockito.MockitoAnnotations @SmallTest @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) -class SeekableSliderTrackerTest : SysuiTestCase() { +class SliderStateTrackerTest : SysuiTestCase() { @Mock private lateinit var sliderStateListener: SliderStateListener private val sliderEventProducer = FakeSliderEventProducer() - private lateinit var mSeekableSliderTracker: SeekableSliderTracker + private lateinit var mSliderStateTracker: SliderStateTracker @Before fun setup() { @@ -55,7 +55,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // THEN the tracker job is active - assertThat(mSeekableSliderTracker.isTracking).isTrue() + assertThat(mSliderStateTracker.isTracking).isTrue() } @Test @@ -65,14 +65,14 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a state in the state machine - mSeekableSliderTracker.setState(it) + mSliderStateTracker.setState(it) // WHEN the tracker stops tracking the state and listening to events - mSeekableSliderTracker.stopTracking() + mSliderStateTracker.stopTracking() // THEN The state is idle and the tracker is not active - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) - assertThat(mSeekableSliderTracker.isTracking).isFalse() + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.isTracking).isFalse() } } @@ -83,7 +83,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // THEN The state is idle and the listener is not called to play haptics - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyZeroInteractions(sliderStateListener) } @@ -96,9 +96,9 @@ class SeekableSliderTrackerTest : SysuiTestCase() { sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) // THEN the tracker moves to the wait state and the timer job begins - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT) verifyZeroInteractions(sliderStateListener) - assertThat(mSeekableSliderTracker.isWaiting).isTrue() + assertThat(mSliderStateTracker.isWaiting).isTrue() } // Tests on the WAIT state @@ -117,9 +117,9 @@ class SeekableSliderTrackerTest : SysuiTestCase() { advanceTimeBy(config.waitTimeMillis + 10L) // THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) - assertThat(mSeekableSliderTracker.isWaiting).isFalse() + assertThat(mSliderStateTracker.isWaiting).isFalse() verify(sliderStateListener).onHandleAcquiredByTouch() verifyNoMoreInteractions(sliderStateListener) } @@ -142,9 +142,9 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to the DRAG_HANDLE_ACQUIRED_BY_TOUCH state without the timer job // being complete - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) - assertThat(mSeekableSliderTracker.isWaiting).isFalse() + assertThat(mSliderStateTracker.isWaiting).isFalse() verify(sliderStateListener).onHandleAcquiredByTouch() verifyNoMoreInteractions(sliderStateListener) } @@ -166,9 +166,9 @@ class SeekableSliderTrackerTest : SysuiTestCase() { ) // THEN the tracker moves to the jump-track location selected state - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.JUMP_TRACK_LOCATION_SELECTED) - assertThat(mSeekableSliderTracker.isWaiting).isFalse() + assertThat(mSliderStateTracker.isWaiting).isFalse() verify(sliderStateListener).onProgressJump(anyFloat()) verifyNoMoreInteractions(sliderStateListener) } @@ -190,8 +190,8 @@ class SeekableSliderTrackerTest : SysuiTestCase() { ) // THEN the tracker moves to the jump-track location selected state - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED) - assertThat(mSeekableSliderTracker.isWaiting).isFalse() + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED) + assertThat(mSliderStateTracker.isWaiting).isFalse() verifyNoMoreInteractions(sliderStateListener) } @@ -212,8 +212,8 @@ class SeekableSliderTrackerTest : SysuiTestCase() { ) // THEN the tracker moves to the JUMP_TRACK_LOCATION_SELECTED state - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED) - assertThat(mSeekableSliderTracker.isWaiting).isFalse() + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.JUMP_BOOKEND_SELECTED) + assertThat(mSliderStateTracker.isWaiting).isFalse() verifyNoMoreInteractions(sliderStateListener) } @@ -225,15 +225,15 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // GIVEN a start of tracking touch event that moves the tracker to WAIT at the middle of the // slider sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5f)) - assertThat(mSeekableSliderTracker.isWaiting).isTrue() + assertThat(mSliderStateTracker.isWaiting).isTrue() // GIVEN that the tracker stops tracking the state and listening to events - mSeekableSliderTracker.stopTracking() + mSliderStateTracker.stopTracking() // THEN the tracker moves to the IDLE state without the timer job being complete - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) - assertThat(mSeekableSliderTracker.isWaiting).isFalse() - assertThat(mSeekableSliderTracker.isTracking).isFalse() + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.isWaiting).isFalse() + assertThat(mSliderStateTracker.isTracking).isFalse() verifyNoMoreInteractions(sliderStateListener) } @@ -244,13 +244,13 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_TRACK_LOCATION_SELECTED state - mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) + mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) // GIVEN a progress event due to dragging the handle sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f)) // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) verify(sliderStateListener).onProgress(anyFloat()) verifyNoMoreInteractions(sliderStateListener) } @@ -260,14 +260,14 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_TRACK_LOCATION_SELECTED state - mSeekableSliderTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) + mSliderStateTracker.setState(SliderState.JUMP_TRACK_LOCATION_SELECTED) // GIVEN that the slider stopped tracking touch sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state verify(sliderStateListener).onHandleReleasedFromTouch() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyNoMoreInteractions(sliderStateListener) } @@ -276,13 +276,13 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_BOOKEND_SELECTED state - mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) + mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) // GIVEN that the slider stopped tracking touch sliderEventProducer.sendEvent(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5f)) // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) verify(sliderStateListener).onProgress(anyFloat()) verifyNoMoreInteractions(sliderStateListener) } @@ -292,14 +292,14 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a JUMP_BOOKEND_SELECTED state - mSeekableSliderTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) + mSliderStateTracker.setState(SliderState.JUMP_BOOKEND_SELECTED) // GIVEN that the slider stopped tracking touch sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state verify(sliderStateListener).onHandleReleasedFromTouch() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyNoMoreInteractions(sliderStateListener) } @@ -310,7 +310,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) // GIVEN a progress change by the user val progress = 0.5f @@ -320,7 +320,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state verify(sliderStateListener).onProgress(progress) - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) verifyNoMoreInteractions(sliderStateListener) } @@ -329,14 +329,14 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_ACQUIRED_BY_TOUCH state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_ACQUIRED_BY_TOUCH) // GIVEN that the handle stops tracking touch sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state verify(sliderStateListener).onHandleReleasedFromTouch() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyNoMoreInteractions(sliderStateListener) } @@ -348,7 +348,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_DRAGGING state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) // GIVEN a progress change by the user outside of bookend bounds val progress = 0.5f @@ -357,8 +357,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { ) // THEN the tracker does not change state and executes the onProgress call - assertThat(mSeekableSliderTracker.currentState) - .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) verify(sliderStateListener).onProgress(progress) verifyNoMoreInteractions(sliderStateListener) } @@ -370,7 +369,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_DRAGGING state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) // GIVEN a progress change by the user reaching the lower bookend val progress = config.lowerBookendThreshold - 0.01f @@ -380,7 +379,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the // corresponding callback - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) verify(sliderStateListener).onLowerBookend() verifyNoMoreInteractions(sliderStateListener) @@ -393,7 +392,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_DRAGGING state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) // GIVEN a progress change by the user reaching the upper bookend val progress = config.upperBookendThreshold + 0.01f @@ -403,7 +402,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to the DRAG_HANDLE_REACHED_BOOKEND state and executes the // corresponding callback - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) verify(sliderStateListener).onUpperBookend() verifyNoMoreInteractions(sliderStateListener) @@ -414,14 +413,14 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_DRAGGING state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_DRAGGING) // GIVEN that the slider stops tracking touch sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state verify(sliderStateListener).onHandleReleasedFromTouch() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyNoMoreInteractions(sliderStateListener) } @@ -434,7 +433,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) // GIVEN a progress event that falls outside of the lower bookend range val progress = config.lowerBookendThreshold + 0.01f @@ -444,8 +443,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly verify(sliderStateListener).onProgress(progress) - assertThat(mSeekableSliderTracker.currentState) - .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) verifyNoMoreInteractions(sliderStateListener) } @@ -455,7 +453,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) // GIVEN a progress event that falls inside of the lower bookend range val progress = config.lowerBookendThreshold - 0.01f @@ -465,7 +463,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker stays in the current state and executes accordingly verify(sliderStateListener).onLowerBookend() - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) verifyNoMoreInteractions(sliderStateListener) } @@ -477,7 +475,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) // GIVEN a progress event that falls outside of the upper bookend range val progress = config.upperBookendThreshold - 0.01f @@ -487,8 +485,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to the DRAG_HANDLE_DRAGGING state and executes accordingly verify(sliderStateListener).onProgress(progress) - assertThat(mSeekableSliderTracker.currentState) - .isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.DRAG_HANDLE_DRAGGING) verifyNoMoreInteractions(sliderStateListener) } @@ -498,7 +495,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) // GIVEN a progress event that falls inside of the upper bookend range val progress = config.upperBookendThreshold + 0.01f @@ -508,7 +505,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker stays in the current state and executes accordingly verify(sliderStateListener).onUpperBookend() - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.DRAG_HANDLE_REACHED_BOOKEND) verifyNoMoreInteractions(sliderStateListener) } @@ -518,37 +515,36 @@ class SeekableSliderTrackerTest : SysuiTestCase() { initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a DRAG_HANDLE_REACHED_BOOKEND state - mSeekableSliderTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) + mSliderStateTracker.setState(SliderState.DRAG_HANDLE_REACHED_BOOKEND) // GIVEN that the handle stops tracking touch sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5f)) // THEN the tracker executes on onHandleReleasedFromTouch before moving to the IDLE state verify(sliderStateListener).onHandleReleasedFromTouch() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyNoMoreInteractions(sliderStateListener) } @Test - fun onProgressChangeByProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest { + fun onStartedTrackingProgram_atTheMiddle_onIdle_movesToArrowHandleMovedOnce() = runTest { // GIVEN an initialized tracker in the IDLE state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) // GIVEN a progress due to an external source that lands at the middle of the slider val progress = 0.5f sliderEventProducer.sendEvent( - SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) + SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress) ) // THEN the state moves to ARROW_HANDLE_MOVED_ONCE and the listener is called to play // haptics - assertThat(mSeekableSliderTracker.currentState) - .isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) verify(sliderStateListener).onSelectAndArrow(progress) } @Test - fun onProgressChangeByProgram_atUpperBookend_onIdle_movesToIdle() = runTest { + fun onStartedTrackingProgram_atUpperBookend_onIdle_movesToIdle() = runTest { // GIVEN an initialized tracker in the IDLE state val config = SeekableSliderTrackerConfig() initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) @@ -556,16 +552,16 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // GIVEN a progress due to an external source that lands at the upper bookend val progress = config.upperBookendThreshold + 0.01f sliderEventProducer.sendEvent( - SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) + SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress) ) // THEN the tracker executes upper bookend haptics before moving back to IDLE verify(sliderStateListener).onUpperBookend() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) } @Test - fun onProgressChangeByProgram_atLowerBookend_onIdle_movesToIdle() = runTest { + fun onStartedTrackingProgram_atLowerBookend_onIdle_movesToIdle() = runTest { // GIVEN an initialized tracker in the IDLE state val config = SeekableSliderTrackerConfig() initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) @@ -573,26 +569,28 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // WHEN a progress is recorded due to an external source that lands at the lower bookend val progress = config.lowerBookendThreshold - 0.01f sliderEventProducer.sendEvent( - SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress) + SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress) ) // THEN the tracker executes lower bookend haptics before moving to IDLE verify(sliderStateListener).onLowerBookend() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) } @Test fun onArrowUp_onArrowMovedOnce_movesToIdle() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) // WHEN the external stimulus is released val progress = 0.5f - sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress)) + sliderEventProducer.sendEvent( + SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress) + ) // THEN the tracker moves back to IDLE and there are no haptics - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyZeroInteractions(sliderStateListener) } @@ -600,7 +598,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onStartTrackingTouch_onArrowMovedOnce_movesToWait() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) // WHEN the slider starts tracking touch val progress = 0.5f @@ -608,8 +606,8 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves back to WAIT and starts the waiting job. Also, there are no // haptics - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT) - assertThat(mSeekableSliderTracker.isWaiting).isTrue() + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT) + assertThat(mSliderStateTracker.isWaiting).isTrue() verifyZeroInteractions(sliderStateListener) } @@ -617,7 +615,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onProgressChangeByProgram_onArrowMovedOnce_movesToArrowMovesContinuously() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVED_ONCE state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVED_ONCE) // WHEN the slider gets an external progress change val progress = 0.5f @@ -627,7 +625,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker moves to ARROW_HANDLE_MOVES_CONTINUOUSLY and calls the appropriate // haptics - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) verify(sliderStateListener).onProgress(progress) } @@ -636,14 +634,16 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onArrowUp_onArrowMovesContinuously_movesToIdle() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the external stimulus is released val progress = 0.5f - sliderEventProducer.sendEvent(SliderEvent(SliderEventType.ARROW_UP, progress)) + sliderEventProducer.sendEvent( + SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress) + ) // THEN the tracker moves to IDLE and no haptics are played - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) verifyZeroInteractions(sliderStateListener) } @@ -651,15 +651,15 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onStartTrackingTouch_onArrowMovesContinuously_movesToWait() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider starts tracking touch val progress = 0.5f sliderEventProducer.sendEvent(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress)) // THEN the tracker moves to WAIT and the wait job starts. - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.WAIT) - assertThat(mSeekableSliderTracker.isWaiting).isTrue() + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.WAIT) + assertThat(mSliderStateTracker.isWaiting).isTrue() verifyZeroInteractions(sliderStateListener) } @@ -667,7 +667,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { fun onProgressChangeByProgram_onArrowMovesContinuously_preservesState() = runTest { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler))) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider changes progress programmatically at the middle val progress = 0.5f @@ -676,7 +676,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { ) // THEN the tracker stays in the same state and haptics are delivered appropriately - assertThat(mSeekableSliderTracker.currentState) + assertThat(mSliderStateTracker.currentState) .isEqualTo(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) verify(sliderStateListener).onProgress(progress) } @@ -686,7 +686,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state val config = SeekableSliderTrackerConfig() initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider reaches the lower bookend programmatically val progress = config.lowerBookendThreshold - 0.01f @@ -696,7 +696,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker executes lower bookend haptics before moving to IDLE verify(sliderStateListener).onLowerBookend() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) } @Test @@ -704,7 +704,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // GIVEN an initialized tracker in the ARROW_HANDLE_MOVES_CONTINUOUSLY state val config = SeekableSliderTrackerConfig() initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)), config) - mSeekableSliderTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) + mSliderStateTracker.setState(SliderState.ARROW_HANDLE_MOVES_CONTINUOUSLY) // WHEN the slider reaches the lower bookend programmatically val progress = config.upperBookendThreshold + 0.01f @@ -714,7 +714,7 @@ class SeekableSliderTrackerTest : SysuiTestCase() { // THEN the tracker executes upper bookend haptics before moving to IDLE verify(sliderStateListener).onUpperBookend() - assertThat(mSeekableSliderTracker.currentState).isEqualTo(SliderState.IDLE) + assertThat(mSliderStateTracker.currentState).isEqualTo(SliderState.IDLE) } @OptIn(ExperimentalCoroutinesApi::class) @@ -722,8 +722,8 @@ class SeekableSliderTrackerTest : SysuiTestCase() { scope: CoroutineScope, config: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), ) { - mSeekableSliderTracker = - SeekableSliderTracker(sliderStateListener, sliderEventProducer, scope, config) - mSeekableSliderTracker.startTracking() + mSliderStateTracker = + SliderStateTracker(sliderStateListener, sliderEventProducer, scope, config) + mSliderStateTracker.startTracking() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt index 6a22d8648d91..fb91c78b9041 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt @@ -24,7 +24,7 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.settingslib.RestrictedLockUtils import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingManagerFake -import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin +import com.android.systemui.haptics.slider.SeekbarHapticPlugin import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.BrightnessMirrorController @@ -93,7 +93,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { brightnessSliderView, mFalsingManager, uiEventLogger, - SeekableSliderHapticPlugin(vibratorHelper, systemClock), + SeekbarHapticPlugin(vibratorHelper, systemClock), activityStarter, ) mController.init() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index fd9daf862190..03f5ecfa92d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -25,23 +25,24 @@ import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.widget.FrameLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.ambient.touch.TouchHandler +import com.android.systemui.ambient.touch.TouchMonitor +import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.fakeCommunalRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.setCommunalAvailable import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.util.CommunalColors import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository 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.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -51,7 +52,6 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.shade.data.repository.fakeShadeRepository -import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.testKosmos @@ -60,7 +60,6 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertThrows @@ -87,16 +86,14 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Mock private lateinit var communalViewModel: CommunalViewModel @Mock private lateinit var powerManager: PowerManager @Mock private lateinit var dialogFactory: SystemUIDialogFactory + @Mock private lateinit var touchMonitor: TouchMonitor @Mock private lateinit var communalColors: CommunalColors - private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor - private lateinit var shadeInteractor: ShadeInteractor - private lateinit var keyguardInteractor: KeyguardInteractor + private lateinit var ambientTouchComponentFactory: AmbientTouchComponent.Factory private lateinit var parentView: FrameLayout private lateinit var containerView: View private lateinit var testableLooper: TestableLooper - private lateinit var communalInteractor: CommunalInteractor private lateinit var communalRepository: FakeCommunalRepository private lateinit var underTest: GlanceableHubContainerController @@ -104,32 +101,37 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - communalInteractor = kosmos.communalInteractor communalRepository = kosmos.fakeCommunalRepository - keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor - keyguardInteractor = kosmos.keyguardInteractor - shadeInteractor = kosmos.shadeInteractor - - underTest = - GlanceableHubContainerController( - communalInteractor, - communalViewModel, - dialogFactory, - keyguardTransitionInteractor, - keyguardInteractor, - shadeInteractor, - powerManager, - communalColors, - kosmos.sceneDataSourceDelegator, - ) + + ambientTouchComponentFactory = + object : AmbientTouchComponent.Factory { + override fun create( + lifecycleOwner: LifecycleOwner, + touchHandlers: Set<TouchHandler> + ): AmbientTouchComponent = + object : AmbientTouchComponent { + override fun getTouchMonitor(): TouchMonitor = touchMonitor + } + } + + with(kosmos) { + underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + dialogFactory, + keyguardTransitionInteractor, + keyguardInteractor, + shadeInteractor, + powerManager, + communalColors, + ambientTouchComponentFactory, + kosmos.sceneDataSourceDelegator, + ) + } testableLooper = TestableLooper.get(this) overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH) - overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH) - overrideResource( - R.dimen.communal_bottom_edge_swipe_region_height, - BOTTOM_SWIPE_REGION_WIDTH - ) // Make communal available so that communalInteractor.desiredScene accurately reflects // scene changes instead of just returning Blank. @@ -161,6 +163,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { shadeInteractor, powerManager, communalColors, + ambientTouchComponentFactory, kosmos.sceneDataSourceDelegator, ) @@ -215,63 +218,137 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test - fun onTouchEvent_topSwipeWhenCommunalOpen_doesNotIntercept() = + fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Touch event in the top swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + // Bouncer is visible. + fakeKeyguardTransitionRepository.sendTransitionSteps( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.PRIMARY_BOUNCER, + testScope + ) + testableLooper.processAllMessages() + + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + // User activity is not sent to PowerManager. + verify(powerManager, times(0)).userActivity(any(), any(), any()) } } @Test - fun onTouchEvent_bottomSwipeWhenCommunalOpen_doesNotIntercept() = + fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Touch event in the bottom swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse() + // Shade shows up. + fakeShadeRepository.setQsExpansion(1.0f) + testableLooper.processAllMessages() + + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() } } @Test - fun onTouchEvent_topSwipeWhenDreaming_doesNotIntercept() = + fun onTouchEvent_containerViewDisposed_doesNotIntercept() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Device is dreaming. - fakeKeyguardRepository.setDreaming(true) - runCurrent() + // Touch events are intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + + // Container view disposed. + underTest.disposeView() - // Touch event in the top swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() } } @Test - fun onTouchEvent_bottomSwipeWhenDreaming_doesNotIntercept() = + fun lifecycle_initializedAfterConstruction() = + with(kosmos) { + val underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + dialogFactory, + keyguardTransitionInteractor, + keyguardInteractor, + shadeInteractor, + powerManager, + communalColors, + ambientTouchComponentFactory, + kosmos.sceneDataSourceDelegator, + ) + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED) + } + + @Test + fun lifecycle_createdAfterViewCreated() = + with(kosmos) { + val underTest = + GlanceableHubContainerController( + communalInteractor, + communalViewModel, + dialogFactory, + keyguardTransitionInteractor, + keyguardInteractor, + shadeInteractor, + powerManager, + communalColors, + ambientTouchComponentFactory, + kosmos.sceneDataSourceDelegator, + ) + + // Only initView without attaching a view as we don't want the flows to start collecting + // yet. + underTest.initView(View(context)) + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun lifecycle_startedAfterFlowsUpdate() { + // Flows start collecting due to test setup, causing the state to advance to STARTED. + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) + } + + @Test + fun lifecycle_resumedAfterCommunalShows() { + // Communal is open. + goToScene(CommunalScenes.Communal) + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + } + + @Test + fun lifecycle_startedAfterCommunalCloses() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Device is dreaming. - fakeKeyguardRepository.setDreaming(true) - runCurrent() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED) + + // Communal closes. + goToScene(CommunalScenes.Blank) - // Touch event in the bottom swipe region is not intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @Test - fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() = + fun lifecycle_startedAfterPrimaryBouncerShows() = with(kosmos) { testScope.runTest { // Communal is open. @@ -285,44 +362,49 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ) testableLooper.processAllMessages() - // Touch events are not intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() - // User activity is not sent to PowerManager. - verify(powerManager, times(0)).userActivity(any(), any(), any()) + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @Test - fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() = + fun lifecycle_startedAfterAlternateBouncerShows() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Shade shows up. - fakeShadeRepository.setQsExpansion(1.0f) + // Bouncer is visible. + fakeKeyguardTransitionRepository.sendTransitionSteps( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.ALTERNATE_BOUNCER, + testScope + ) testableLooper.processAllMessages() - // Touch events are not intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @Test - fun onTouchEvent_containerViewDisposed_doesNotIntercept() = + fun lifecycle_createdAfterDisposeView() { + // Container view disposed. + underTest.disposeView() + + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED) + } + + @Test + fun lifecycle_startedAfterShadeShows() = with(kosmos) { testScope.runTest { // Communal is open. goToScene(CommunalScenes.Communal) - // Touch events are intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() - - // Container view disposed. - underTest.disposeView() + // Shade shows up. + fakeShadeRepository.setQsExpansion(1.0f) + testableLooper.processAllMessages() - // Touch events are not intercepted. - assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED) } } @@ -371,8 +453,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { private const val CONTAINER_WIDTH = 100 private const val CONTAINER_HEIGHT = 100 private const val RIGHT_SWIPE_REGION_WIDTH = 20 - private const val TOP_SWIPE_REGION_WIDTH = 20 - private const val BOTTOM_SWIPE_REGION_WIDTH = 20 /** * A touch down event right in the middle of the screen, to avoid being in any of the swipe @@ -389,17 +469,6 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { ) private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0) - private val DOWN_IN_TOP_SWIPE_REGION_EVENT = - MotionEvent.obtain( - 0L, - 0L, - MotionEvent.ACTION_DOWN, - 0f, - TOP_SWIPE_REGION_WIDTH.toFloat(), - 0 - ) - private val DOWN_IN_BOTTOM_SWIPE_REGION_EVENT = - MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, CONTAINER_HEIGHT.toFloat(), 0) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index da09579e1bde..d95cc2efc868 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -500,6 +500,46 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } @Test + fun handleExternalTouch_intercepted_sendsOnTouch() { + // Accept dispatch and also intercept. + whenever(view.dispatchTouchEvent(any())).thenReturn(true) + whenever(view.onInterceptTouchEvent(any())).thenReturn(true) + + underTest.handleExternalTouch(DOWN_EVENT) + underTest.handleExternalTouch(MOVE_EVENT) + + // Once intercepted, both events are sent to the view. + verify(view).onTouchEvent(DOWN_EVENT) + verify(view).onTouchEvent(MOVE_EVENT) + } + + @Test + fun handleExternalTouch_notDispatched_interceptNotCalled() { + // Don't accept dispatch + whenever(view.dispatchTouchEvent(any())).thenReturn(false) + + underTest.handleExternalTouch(DOWN_EVENT) + + // Interception is not offered. + verify(view, never()).onInterceptTouchEvent(any()) + } + + @Test + fun handleExternalTouch_notIntercepted_onTouchNotSent() { + // Accept dispatch, but don't dispatch + whenever(view.dispatchTouchEvent(any())).thenReturn(true) + whenever(view.onInterceptTouchEvent(any())).thenReturn(false) + + underTest.handleExternalTouch(DOWN_EVENT) + underTest.handleExternalTouch(MOVE_EVENT) + + // Interception offered for both events, but onTouchEvent is never called. + verify(view).onInterceptTouchEvent(DOWN_EVENT) + verify(view).onInterceptTouchEvent(MOVE_EVENT) + verify(view, never()).onTouchEvent(any()) + } + + @Test fun testGetKeyguardMessageArea() = testScope.runTest { underTest.keyguardMessageArea diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 4ed6fe27338a..329c0f1ab5b4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -46,6 +46,10 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId } } + override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) {} + + override fun abortRestoreWidgets() {} + private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt index 4e24233a6681..e2a1fe4e45bf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerFlagParameterization.kt @@ -58,3 +58,8 @@ fun FlagsParameterization.andSceneContainer(): Sequence<FlagsParameterization> = */ fun List<FlagsParameterization>.andSceneContainer(): List<FlagsParameterization> = flatMap { it.andSceneContainer() }.toList() + +/** Parameterizes only the scene container flag. */ +fun parameterizeSceneContainerFlag(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() +} 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 162f278b7a8f..d4b793720328 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 @@ -22,6 +22,7 @@ import android.content.applicationContext import android.os.fakeExecutorHandler import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.bouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.falsingCollector import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository @@ -30,8 +31,8 @@ import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor -import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor import com.android.systemui.haptics.qs.qsLongPressEffect import com.android.systemui.jank.interactionJankMonitor @@ -41,6 +42,7 @@ import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInterac import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.model.sceneContainerPlugin import com.android.systemui.plugins.statusbar.statusBarStateController @@ -50,6 +52,8 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.model.sceneDataSource import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shadeController import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.statusbar.phone.screenOffAnimationController @@ -69,7 +73,6 @@ class KosmosJavaAdapter( val testDispatcher by lazy { kosmos.testDispatcher } val testScope by lazy { kosmos.testScope } - val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic } val fakeExecutor by lazy { kosmos.fakeExecutor } val fakeExecutorHandler by lazy { kosmos.fakeExecutorHandler } val configurationRepository by lazy { kosmos.fakeConfigurationRepository } @@ -77,6 +80,8 @@ class KosmosJavaAdapter( val bouncerRepository by lazy { kosmos.bouncerRepository } val communalRepository by lazy { kosmos.fakeCommunalRepository } val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository } + val keyguardInteractor by lazy { kosmos.keyguardInteractor } val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } val powerRepository by lazy { kosmos.fakePowerRepository } @@ -91,6 +96,7 @@ class KosmosJavaAdapter( val falsingCollector by lazy { kosmos.falsingCollector } val powerInteractor by lazy { kosmos.powerInteractor } val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } + val deviceEntryUdfpsInteractor by lazy { kosmos.deviceEntryUdfpsInteractor } val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } val communalInteractor by lazy { kosmos.communalInteractor } val sceneContainerPlugin by lazy { kosmos.sceneContainerPlugin } @@ -110,6 +116,8 @@ class KosmosJavaAdapter( val brightnessMirrorShowingInteractor by lazy { kosmos.brightnessMirrorShowingInteractor } val qsLongPressEffect by lazy { kosmos.qsLongPressEffect } val shadeController by lazy { kosmos.shadeController } + val shadeRepository by lazy { kosmos.shadeRepository } + val shadeInteractor by lazy { kosmos.shadeInteractor } init { kosmos.applicationContext = testCase.context diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt index 63a05d77c3c1..6ac3a62cc108 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index 9b4d378cc7b7..243e22457c8e 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -266,6 +266,8 @@ android.telephony.CellSignalStrength android.telephony.ModemActivityInfo android.telephony.ServiceState +android.os.connectivity.WifiActivityEnergyInfo + com.android.server.LocalServices com.android.internal.util.BitUtils diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index 5e566aa98215..522aa6703b51 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -202,6 +202,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku // Default max API calls per reset interval for generated preview API rate limiting. private static final int DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL = 2; + private static final String PENDING_DELETED_IDS_ATTR = "pending_deleted_ids"; private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -228,6 +229,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku onPackageBroadcastReceived(intent, getSendingUserId()); updateWidgetPackageSuspensionMaskedState(intent, false, getSendingUserId()); break; + case Intent.ACTION_PACKAGE_RESTARTED: + case Intent.ACTION_PACKAGE_UNSTOPPED: + if (!android.content.pm.Flags.stayStopped()) return; + updateWidgetPackageStoppedMaskedState(intent); + break; default: onPackageBroadcastReceived(intent, getSendingUserId()); break; @@ -396,7 +402,10 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); packageFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); + packageFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + packageFilter.addAction(Intent.ACTION_PACKAGE_UNSTOPPED); packageFilter.addDataScheme("package"); + packageFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); mContext.registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, packageFilter, null, mCallbackHandler); @@ -581,14 +590,19 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku changed |= provider.setMaskedByQuietProfileLocked(quietProfile); try { boolean suspended; + boolean stopped; try { suspended = mPackageManager.isPackageSuspendedForUser( provider.id.componentName.getPackageName(), provider.getUserId()); + stopped = mPackageManager.isPackageStoppedForUser( + provider.id.componentName.getPackageName(), provider.getUserId()); } catch (IllegalArgumentException ex) { // Package not found. suspended = false; + stopped = false; } changed |= provider.setMaskedBySuspendedPackageLocked(suspended); + changed |= provider.setMaskedByStoppedPackageLocked(stopped); } catch (RemoteException e) { Slog.e(TAG, "Failed to query application info", e); } @@ -636,6 +650,82 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } /** + * Update the masked state for a stopped or unstopped package. + */ + private void updateWidgetPackageStoppedMaskedState(@NonNull Intent intent) { + final int providerUid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); + final Uri uri = intent.getData(); + if (providerUid == Process.INVALID_UID || uri == null) { + return; + } + + final String packageName = uri.getSchemeSpecificPart(); + if (packageName == null) { + return; + } + + boolean isStopped; + try { + isStopped = mPackageManager.isPackageStoppedForUser(packageName, + UserHandle.getUserId(providerUid)); + } catch (Exception e) { + Slog.e(TAG, "Failed to query package stopped state", e); + return; + } + + if (DEBUG) { + Slog.i(TAG, "Updating package stopped masked state for uid " + providerUid + " package " + + packageName + " isStopped " + isStopped); + } + synchronized (mLock) { + final int count = mProviders.size(); + for (int i = 0; i < count; i++) { + Provider provider = mProviders.get(i); + if (providerUid != provider.id.uid + || !packageName.equals(provider.id.componentName.getPackageName())) { + continue; + } + if (provider.setMaskedByStoppedPackageLocked(isStopped)) { + if (provider.isMaskedLocked()) { + maskWidgetsViewsLocked(provider, null); + cancelBroadcastsLocked(provider); + } else { + unmaskWidgetsViewsLocked(provider); + final int widgetCount = provider.widgets.size(); + if (widgetCount > 0) { + final int[] widgetIds = new int[widgetCount]; + for (int j = 0; j < widgetCount; j++) { + widgetIds[j] = provider.widgets.get(j).appWidgetId; + } + registerForBroadcastsLocked(provider, widgetIds); + sendUpdateIntentLocked(provider, widgetIds, /* interactive= */ false); + } + + final int pendingIdsCount = provider.pendingDeletedWidgetIds.size(); + if (pendingIdsCount > 0) { + if (DEBUG) { + Slog.i(TAG, "Sending missed deleted broadcasts for " + + provider.id.componentName + " " + + provider.pendingDeletedWidgetIds); + } + for (int j = 0; j < pendingIdsCount; j++) { + sendDeletedIntentLocked(provider.id.componentName, + provider.id.getProfile(), + provider.pendingDeletedWidgetIds.get(j)); + } + provider.pendingDeletedWidgetIds.clear(); + if (widgetCount == 0) { + sendDisabledIntentLocked(provider); + } + saveGroupStateAsync(provider.id.getProfile().getIdentifier()); + } + } + } + } + } + } + + /** * Mask the target widget belonging to the specified provider, or all active widgets * of the provider if target widget == null. */ @@ -648,11 +738,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku R.layout.work_widget_mask_view); ApplicationInfo appInfo = provider.info.providerInfo.applicationInfo; final int appUserId = provider.getUserId(); - boolean showBadge; + boolean showBadge = false; final long identity = Binder.clearCallingIdentity(); try { - final Intent onClickIntent; + Intent onClickIntent = null; if (provider.maskedByQuietProfile) { showBadge = true; @@ -676,7 +766,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku appInfo.packageName, suspendingPackage, dialogInfo, null, null, appUserId); } - } else /* provider.maskedByLockedProfile */ { + } else if (provider.maskedByLockedProfile) { showBadge = true; onClickIntent = mKeyguardManager .createConfirmDeviceCredentialIntent(null, null, appUserId); @@ -684,6 +774,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku onClickIntent.setFlags( FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); } + } else if (provider.maskedByStoppedPackage) { + showBadge = mUserManager.hasBadge(appUserId); } Icon icon = appInfo.icon != 0 @@ -697,7 +789,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku for (int j = 0; j < widgetCount; j++) { Widget widget = provider.widgets.get(j); if (targetWidget != null && targetWidget != widget) continue; - if (onClickIntent != null) { + if (provider.maskedByStoppedPackage) { + Intent intent = createUpdateIntentLocked(provider, + new int[] { widget.appWidgetId }); + views.setOnClickPendingIntent(android.R.id.background, + PendingIntent.getBroadcast(mContext, widget.appWidgetId, + intent, PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_IMMUTABLE)); + } else if (onClickIntent != null) { views.setOnClickPendingIntent(android.R.id.background, PendingIntent.getActivity(mContext, widget.appWidgetId, onClickIntent, PendingIntent.FLAG_UPDATE_CURRENT @@ -1950,15 +2049,23 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (provider != null) { provider.widgets.remove(widget); if (!provider.zombie) { - // send the broacast saying that this appWidgetId has been deleted - sendDeletedIntentLocked(widget); + // If the package is not stopped, send the broadcast saying that this appWidgetId + // has been deleted. Otherwise, save the ID and send the broadcast when the package + // is unstopped. + if (!provider.maskedByStoppedPackage) { + sendDeletedIntentLocked(widget); + } else { + provider.pendingDeletedWidgetIds.add(widget.appWidgetId); + } if (provider.widgets.isEmpty()) { // cancel the future updates cancelBroadcastsLocked(provider); - // send the broacast saying that the provider is not in use any more - sendDisabledIntentLocked(provider); + // send the broadcast saying that the provider is not in use any more + if (!provider.maskedByStoppedPackage) { + sendDisabledIntentLocked(provider); + } } } } @@ -2033,8 +2140,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku final HashSet<Integer> ids = mRemoteViewsServicesAppWidgets.get(key); if (ids.remove(widget.appWidgetId)) { // If we have removed the last app widget referencing this service, then we - // should destroy it and remove it from this set - if (ids.isEmpty()) { + // should destroy it and remove it from this set. This is skipped for widgets whose + // provider is in a stopped package, to avoid waking up the package. + if (ids.isEmpty() && !widget.provider.maskedByStoppedPackage) { destroyRemoteViewsService(key.second.getIntent(), widget); it.remove(); } @@ -2544,18 +2652,29 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku private void sendUpdateIntentLocked(Provider provider, int[] appWidgetIds, boolean interactive) { + Intent intent = createUpdateIntentLocked(provider, appWidgetIds); + sendBroadcastAsUser(intent, provider.id.getProfile(), interactive); + } + + private Intent createUpdateIntentLocked(Provider provider, int[] appWidgetIds) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); intent.setComponent(provider.id.componentName); - sendBroadcastAsUser(intent, provider.id.getProfile(), interactive); + return intent; } private void sendDeletedIntentLocked(Widget widget) { + sendDeletedIntentLocked(widget.provider.id.componentName, widget.provider.id.getProfile(), + widget.appWidgetId); + } + + private void sendDeletedIntentLocked(ComponentName provider, UserHandle profile, + int appWidgetId) { Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_DELETED); - intent.setComponent(widget.provider.id.componentName); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widget.appWidgetId); + intent.setComponent(provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); // Cleanup after deletion isn't an interactive UX case - sendBroadcastAsUser(intent, widget.provider.id.getProfile(), false); + sendBroadcastAsUser(intent, profile, false); } private void sendDisabledIntentLocked(Provider provider) { @@ -2684,6 +2803,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (persistsProviderInfo && p.mInfoParsed) { AppWidgetXmlUtil.writeAppWidgetProviderInfoLocked(out, p.info); } + final int pendingIdsCount = p.pendingDeletedWidgetIds.size(); + if (pendingIdsCount > 0) { + final List<String> idStrings = new ArrayList<>(); + for (int i = 0; i < pendingIdsCount; i++) { + idStrings.add(String.valueOf(p.pendingDeletedWidgetIds.get(i))); + } + out.attribute(null, PENDING_DELETED_IDS_ATTR, String.join(",", idStrings)); + } out.endTag(null, "p"); } @@ -3022,7 +3149,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku continue; } - if (provider.widgets.size() > 0) { + if (provider.widgets.size() > 0 && !provider.maskedByStoppedPackage) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "appwidget init " + provider.id.componentName.getPackageName()); provider.widgets.forEach(widget -> { @@ -3440,6 +3567,16 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku legacyProviderIndex); provider.tag = providerTag; provider.infoTag = parser.getAttributeValue(null, "info_tag"); + + final String pendingDeletedIds = parser.getAttributeValue(null, + PENDING_DELETED_IDS_ATTR); + if (pendingDeletedIds != null && !pendingDeletedIds.isEmpty()) { + final String[] idStrings = pendingDeletedIds.split(","); + for (int i = 0; i < idStrings.length; i++) { + provider.pendingDeletedWidgetIds.add( + Integer.parseInt(idStrings[i])); + } + } } else if ("h".equals(tag)) { legacyHostIndex++; Host host = new Host(); @@ -4443,6 +4580,11 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku boolean maskedByLockedProfile; boolean maskedByQuietProfile; boolean maskedBySuspendedPackage; + // This provider's package has been stopped + boolean maskedByStoppedPackage; + // Widget IDs for which we haven't yet sent DELETED broadcasts because the package was + // stopped. + IntArray pendingDeletedWidgetIds = new IntArray(); boolean mInfoParsed = false; @@ -4598,8 +4740,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku return masked != oldState; } + public boolean setMaskedByStoppedPackageLocked(boolean masked) { + boolean oldState = maskedByStoppedPackage; + maskedByStoppedPackage = masked; + return masked != oldState; + } + public boolean isMaskedLocked() { - return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage; + return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage + || maskedByStoppedPackage; } public boolean shouldBePersisted() { diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java index 7f64786c947e..9a73a2d75419 100644 --- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java +++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java @@ -30,7 +30,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static com.android.server.contextualsearch.flags.Flags.enableExcludePersistentUi; - import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT; import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE; @@ -65,6 +64,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ShellCallback; +import android.os.SystemClock; import android.util.Log; import android.util.Slog; import android.view.IWindowManager; @@ -246,6 +246,9 @@ public class ContextualSearchManagerService extends SystemService { if (DEBUG_USER) Log.d(TAG, "Launch component: " + launchIntent.getComponent()); launchIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION | FLAG_ACTIVITY_NO_USER_ACTION); + launchIntent.putExtra( + ContextualSearchManager.EXTRA_INVOCATION_TIME_MS, + SystemClock.uptimeMillis()); launchIntent.putExtra(ContextualSearchManager.EXTRA_ENTRYPOINT, entrypoint); launchIntent.putExtra(ContextualSearchManager.EXTRA_TOKEN, mToken); boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed(); diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index 6c7546eca50b..64bca33569cc 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -17,6 +17,7 @@ package com.android.server; import static android.permission.flags.Flags.sensitiveContentImprovements; +import static android.permission.flags.Flags.sensitiveContentMetricsBugfix; import static android.permission.flags.Flags.sensitiveNotificationAppProtection; import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS; import static android.view.flags.Flags.sensitiveContentAppProtection; @@ -93,7 +94,7 @@ public final class SensitiveContentProtectionManagerService extends SystemServic private boolean mProjectionActive = false; private static class MediaProjectionSession { - private final int mUid; + private final int mUid; // UID of app that started projection session private final long mSessionId; private final boolean mIsExempted; private final ArraySet<String> mAllSeenNotificationKeys = new ArraySet<>(); @@ -320,6 +321,12 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } mProjectionActive = true; + + if (sensitiveContentMetricsBugfix()) { + mWindowManager.setBlockScreenCaptureForAppsSessionId( + mMediaProjectionSession.mSessionId); + } + if (sensitiveNotificationAppProtection()) { updateAppsThatShouldBlockScreenCapture(); } diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index bd67cf42014a..e1710649f925 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -425,6 +425,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private int[] mSCBMReason; private boolean[] mSCBMStarted; + private boolean[] mCarrierRoamingNtnMode = null; + /** * Per-phone map of precise data connection state. The key of the map is the pair of transport * type and APN setting. This is the cache to prevent redundant callbacks to the listeners. @@ -723,6 +725,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mECBMStarted = copyOf(mECBMStarted, mNumPhones); mSCBMReason = copyOf(mSCBMReason, mNumPhones); mSCBMStarted = copyOf(mSCBMStarted, mNumPhones); + mCarrierRoamingNtnMode = copyOf(mCarrierRoamingNtnMode, mNumPhones); // ds -> ss switch. if (mNumPhones < oldNumPhones) { cutListToSize(mCellInfo, mNumPhones); @@ -781,6 +784,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mECBMStarted[i] = false; mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN; mSCBMStarted[i] = false; + mCarrierRoamingNtnMode[i] = false; } } } @@ -854,6 +858,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mECBMStarted = new boolean[numPhones]; mSCBMReason = new int[numPhones]; mSCBMStarted = new boolean[numPhones]; + mCarrierRoamingNtnMode = new boolean[numPhones]; for (int i = 0; i < numPhones; i++) { mCallState[i] = TelephonyManager.CALL_STATE_IDLE; @@ -897,6 +902,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mECBMStarted[i] = false; mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN; mSCBMStarted[i] = false; + mCarrierRoamingNtnMode[i] = false; } mAppOps = mContext.getSystemService(AppOpsManager.class); @@ -1523,6 +1529,14 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } + if (events.contains(TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED)) { + try { + r.callback.onCarrierRoamingNtnModeChanged( + mCarrierRoamingNtnMode[r.phoneId]); + } catch (RemoteException ex) { + remove(r.binder); + } + } } } } @@ -3512,6 +3526,41 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { handleRemoveListLocked(); } + /** + * Notify external listeners that carrier roaming non-terrestrial network mode changed. + * @param subId subscription ID. + * @param active {@code true} If the device is connected to carrier roaming + * non-terrestrial network or was connected within the + * {CarrierConfigManager#KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT} + * duration, {code false} otherwise. + */ + public void notifyCarrierRoamingNtnModeChanged(int subId, boolean active) { + if (!checkNotifyPermission("notifyCarrierRoamingNtnModeChanged")) { + return; + } + + if (VDBG) { + log("notifyCarrierRoamingNtnModeChanged: subId=" + subId + " active=" + active); + } + + synchronized (mRecords) { + int phoneId = getPhoneIdFromSubId(subId); + mCarrierRoamingNtnMode[phoneId] = active; + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent( + TelephonyCallback.EVENT_CARRIER_ROAMING_NTN_MODE_CHANGED) + && idMatch(r, subId, phoneId)) { + try { + r.callback.onCarrierRoamingNtnModeChanged(active); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } + } + } + handleRemoveListLocked(); + } + } + @NeverCompile // Avoid size overhead of debugging code. @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index c3a3bf7c4ce2..eec22c92b5dc 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -2422,8 +2422,6 @@ public final class ActiveServices { getTimeLimitedFgsType(foregroundServiceType); final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType); if (fgsTypeInfo != null) { - // TODO(b/330399444): check to see if all time book-keeping for - // time limited types should use elapsedRealtime instead of uptime final long before24Hr = Math.max(0, SystemClock.elapsedRealtime() - (24 * 60 * 60 * 1000)); final long lastTimeOutAt = fgsTypeInfo.getTimeLimitExceededAt(); @@ -3757,7 +3755,7 @@ public final class ActiveServices { } traceInstant("FGS start: ", sr); - final long nowUptime = SystemClock.uptimeMillis(); + final long nowRealtime = SystemClock.elapsedRealtime(); // Fetch/create/update the fgs info for the time-limited type. SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid); @@ -3768,10 +3766,10 @@ public final class ActiveServices { final int timeLimitedFgsType = getTimeLimitedFgsType(sr.foregroundServiceType); TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType); if (fgsTypeInfo == null) { - fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowUptime); + fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowRealtime); fgsInfo.put(timeLimitedFgsType, fgsTypeInfo); } - fgsTypeInfo.setLastFgsStartTime(nowUptime); + fgsTypeInfo.setLastFgsStartTime(nowRealtime); // We'll cancel the previous ANR timer and start a fresh one below. mFGSAnrTimer.cancel(sr); @@ -3845,14 +3843,14 @@ public final class ActiveServices { final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(fgsType); if (fgsTypeInfo != null) { // Update total runtime for the time-limited fgs type and mark it as timed out. - final long nowUptime = SystemClock.uptimeMillis(); + final long nowRealtime = SystemClock.elapsedRealtime(); fgsTypeInfo.updateTotalRuntime(); - fgsTypeInfo.setTimeLimitExceededAt(nowUptime); + fgsTypeInfo.setTimeLimitExceededAt(nowRealtime); logFGSStateChangeLocked(sr, FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT, - nowUptime > fgsTypeInfo.getLastFgsStartTime() - ? (int) (nowUptime - fgsTypeInfo.getLastFgsStartTime()) : 0, + nowRealtime > fgsTypeInfo.getLastFgsStartTime() + ? (int) (nowRealtime - fgsTypeInfo.getLastFgsStartTime()) : 0, FGS_STOP_REASON_UNKNOWN, FGS_TYPE_POLICY_CHECK_UNKNOWN, FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA, diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index f98799dd3723..4f841497b201 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -133,6 +133,7 @@ import com.android.server.power.stats.PowerStatsScheduler; import com.android.server.power.stats.PowerStatsStore; import com.android.server.power.stats.PowerStatsUidResolver; import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes; +import com.android.server.power.stats.WifiPowerStatsProcessor; import com.android.server.power.stats.wakeups.CpuWakeupStats; import java.io.File; @@ -412,6 +413,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub com.android.internal.R.integer.config_defaultPowerStatsThrottlePeriodCpu); final long powerStatsThrottlePeriodMobileRadio = context.getResources().getInteger( com.android.internal.R.integer.config_defaultPowerStatsThrottlePeriodMobileRadio); + final long powerStatsThrottlePeriodWifi = context.getResources().getInteger( + com.android.internal.R.integer.config_defaultPowerStatsThrottlePeriodWifi); mBatteryStatsConfig = new BatteryStatsImpl.BatteryStatsConfig.Builder() .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel) @@ -422,6 +425,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub .setPowerStatsThrottlePeriodMillis( BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, powerStatsThrottlePeriodMobileRadio) + .setPowerStatsThrottlePeriodMillis( + BatteryConsumer.POWER_COMPONENT_WIFI, + powerStatsThrottlePeriodWifi) .build(); mPowerStatsUidResolver = new PowerStatsUidResolver(); mStats = new BatteryStatsImpl(mBatteryStatsConfig, Clock.SYSTEM_CLOCK, mMonotonicClock, @@ -480,6 +486,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub AggregatedPowerStatsConfig.STATE_PROCESS_STATE) .setProcessor( new CpuPowerStatsProcessor(mPowerProfile, mCpuScalingPolicies)); + config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO) .trackDeviceStates( AggregatedPowerStatsConfig.STATE_POWER, @@ -490,9 +497,21 @@ public final class BatteryStatsService extends IBatteryStats.Stub AggregatedPowerStatsConfig.STATE_PROCESS_STATE) .setProcessor( new MobileRadioPowerStatsProcessor(mPowerProfile)); + config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_PHONE, BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO) .setProcessor(new PhoneCallPowerStatsProcessor()); + + config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_WIFI) + .trackDeviceStates( + AggregatedPowerStatsConfig.STATE_POWER, + AggregatedPowerStatsConfig.STATE_SCREEN) + .trackUidStates( + AggregatedPowerStatsConfig.STATE_POWER, + AggregatedPowerStatsConfig.STATE_SCREEN, + AggregatedPowerStatsConfig.STATE_PROCESS_STATE) + .setProcessor( + new WifiPowerStatsProcessor(mPowerProfile)); return config; } @@ -518,14 +537,22 @@ public final class BatteryStatsService extends IBatteryStats.Stub public void systemServicesReady() { mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU, Flags.streamlinedBatteryStats()); - mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, - Flags.streamlinedConnectivityBatteryStats()); mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( BatteryConsumer.POWER_COMPONENT_CPU, Flags.streamlinedBatteryStats()); + + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, + Flags.streamlinedConnectivityBatteryStats()); mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, Flags.streamlinedConnectivityBatteryStats()); + + mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_WIFI, + Flags.streamlinedConnectivityBatteryStats()); + mBatteryUsageStatsProvider.setPowerStatsExporterEnabled( + BatteryConsumer.POWER_COMPONENT_WIFI, + Flags.streamlinedConnectivityBatteryStats()); + mWorker.systemServicesReady(); mStats.systemServicesReady(mContext); mCpuWakeupStats.systemServicesReady(); diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index fa6b54bb05d8..211f952551d9 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -2371,6 +2371,14 @@ public final class CachedAppOptimizer { return; } + if (opt.shouldNotFreeze()) { + if (DEBUG_FREEZER) { + Slog.d(TAG_AM, "Skipping freeze because process is marked " + + "should not be frozen"); + } + return; + } + if (pid == 0 || opt.isFrozen()) { // Already frozen or not a real process, either one being // launched or one being killed diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 045d137edf43..065f3bd6b1e6 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -30,9 +30,9 @@ import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_ import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; +import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.UptimeMillisLong; import android.app.BackgroundStartPrivileges; import android.app.IApplicationThread; import android.app.Notification; @@ -677,46 +677,46 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN * Data container class to help track certain fgs info for time-restricted types. */ static class TimeLimitedFgsInfo { - @UptimeMillisLong + @ElapsedRealtimeLong private long mFirstFgsStartTime; - @UptimeMillisLong + @ElapsedRealtimeLong private long mLastFgsStartTime; - @UptimeMillisLong + @ElapsedRealtimeLong private long mTimeLimitExceededAt = Long.MIN_VALUE; private long mTotalRuntime = 0; - TimeLimitedFgsInfo(@UptimeMillisLong long startTime) { + TimeLimitedFgsInfo(@ElapsedRealtimeLong long startTime) { mFirstFgsStartTime = startTime; mLastFgsStartTime = startTime; } - @UptimeMillisLong + @ElapsedRealtimeLong public long getFirstFgsStartTime() { return mFirstFgsStartTime; } - public void setLastFgsStartTime(@UptimeMillisLong long startTime) { + public void setLastFgsStartTime(@ElapsedRealtimeLong long startTime) { mLastFgsStartTime = startTime; } - @UptimeMillisLong + @ElapsedRealtimeLong public long getLastFgsStartTime() { return mLastFgsStartTime; } public void updateTotalRuntime() { - mTotalRuntime += SystemClock.uptimeMillis() - mLastFgsStartTime; + mTotalRuntime += SystemClock.elapsedRealtime() - mLastFgsStartTime; } public long getTotalRuntime() { return mTotalRuntime; } - public void setTimeLimitExceededAt(@UptimeMillisLong long timeLimitExceededAt) { + public void setTimeLimitExceededAt(@ElapsedRealtimeLong long timeLimitExceededAt) { mTimeLimitExceededAt = timeLimitExceededAt; } - @UptimeMillisLong + @ElapsedRealtimeLong public long getTimeLimitExceededAt() { return mTimeLimitExceededAt; } @@ -1858,8 +1858,8 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN /** * Called when a time-limited FGS starts. */ - public TimeLimitedFgsInfo createTimeLimitedFgsInfo(long nowUptime) { - return new TimeLimitedFgsInfo(nowUptime); + public TimeLimitedFgsInfo createTimeLimitedFgsInfo(@ElapsedRealtimeLong long nowRealtime) { + return new TimeLimitedFgsInfo(nowRealtime); } /** diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index b8bfedae91ae..e59de6aceb04 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -2773,15 +2773,15 @@ public class AppOpsService extends IAppOpsService.Stub { } code = AppOpsManager.opToSwitch(code); UidState uidState = getUidStateLocked(uid, false); - if (uidState != null - && mAppOpsCheckingService.getUidMode( - uidState.uid, getPersistentId(virtualDeviceId), code) - != AppOpsManager.opToDefaultMode(code)) { - final int rawMode = - mAppOpsCheckingService.getUidMode( - uidState.uid, getPersistentId(virtualDeviceId), code); - return raw ? rawMode : uidState.evalMode(code, rawMode); + if (uidState != null) { + int rawUidMode = mAppOpsCheckingService.getUidMode( + uidState.uid, getPersistentId(virtualDeviceId), code); + + if (rawUidMode != AppOpsManager.opToDefaultMode(code)) { + return raw ? rawUidMode : uidState.evalMode(code, rawUidMode); + } } + Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false); if (op == null) { return AppOpsManager.opToDefaultMode(code); @@ -3682,26 +3682,24 @@ public class AppOpsService extends IAppOpsService.Stub { isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, virtualDeviceId, pvr.bypass, false); final int switchCode = AppOpsManager.opToSwitch(code); + + int rawUidMode; if (isOpAllowedForUid(uid)) { // Op is always allowed for the UID, do nothing. // If there is a non-default per UID policy (we set UID op mode only if // non-default) it takes over, otherwise use the per package policy. - } else if (mAppOpsCheckingService.getUidMode( - uidState.uid, getPersistentId(virtualDeviceId), switchCode) + } else if ((rawUidMode = + mAppOpsCheckingService.getUidMode( + uidState.uid, getPersistentId(virtualDeviceId), switchCode)) != AppOpsManager.opToDefaultMode(switchCode)) { - final int uidMode = - uidState.evalMode( - code, - mAppOpsCheckingService.getUidMode( - uidState.uid, - getPersistentId(virtualDeviceId), - switchCode)); + final int uidMode = uidState.evalMode(code, rawUidMode); if (!shouldStartForMode(uidMode, startIfModeDefault)) { if (DEBUG) { Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code " + switchCode + " (" + code + ") uid " + uid + " package " - + packageName + " flags: " + AppOpsManager.flagsToString(flags)); + + packageName + " flags: " + + AppOpsManager.flagsToString(flags)); } attributedOp.rejected(uidState.getState(), flags); scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, @@ -3710,8 +3708,8 @@ public class AppOpsService extends IAppOpsService.Stub { return new SyncNotedAppOp(uidMode, code, attributionTag, packageName); } } else { - final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true) - : op; + final Op switchOp = + switchCode != code ? getOpLocked(ops, switchCode, uid, true) : op; final int mode = switchOp.uidState.evalMode( switchOp.op, @@ -3721,9 +3719,12 @@ public class AppOpsService extends IAppOpsService.Stub { UserHandle.getUserId(switchOp.uid))); if (mode != AppOpsManager.MODE_ALLOWED && (!startIfModeDefault || mode != MODE_DEFAULT)) { - if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code " - + switchCode + " (" + code + ") uid " + uid + " package " - + packageName + " flags: " + AppOpsManager.flagsToString(flags)); + if (DEBUG) { + Slog.d(TAG, "startOperation: reject #" + mode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + + packageName + " flags: " + + AppOpsManager.flagsToString(flags)); + } attributedOp.rejected(uidState.getState(), flags); scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, virtualDeviceId, flags, mode, startType, attributionFlags, @@ -3731,6 +3732,7 @@ public class AppOpsService extends IAppOpsService.Stub { return new SyncNotedAppOp(mode, code, attributionTag, packageName); } } + if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid + " package " + packageName + " restricted: " + isRestricted + " flags: " + AppOpsManager.flagsToString(flags)); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index acc086bd6be6..00a59114ad94 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -35,6 +35,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcqu import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.common.ICancellationSignal; +import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.events.AuthenticationAcquiredInfo; import android.hardware.biometrics.events.AuthenticationErrorInfo; import android.hardware.biometrics.events.AuthenticationFailedInfo; @@ -344,6 +345,12 @@ public class FingerprintAuthenticationClient if (session.hasContextMethods()) { try { session.getSession().onContextChanged(ctx); + // TODO(b/317414324): Deprecate setIgnoreDisplayTouches + if (ctx.operationState != null && ctx.operationState.getTag() + == OperationState.fingerprintOperationState) { + session.getSession().setIgnoreDisplayTouches(ctx.operationState + .getFingerprintOperationState().isHardwareIgnoringTouches); + } } catch (RemoteException e) { Slog.e(TAG, "Unable to notify context changed", e); } diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java index e3e3be2e5188..baa154d6d7b9 100644 --- a/services/core/java/com/android/server/display/DisplayBrightnessState.java +++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java @@ -45,6 +45,7 @@ public final class DisplayBrightnessState { private final float mCustomAnimationRate; private final BrightnessEvent mBrightnessEvent; + private final int mBrightnessAdjustmentFlag; private DisplayBrightnessState(Builder builder) { mBrightness = builder.getBrightness(); @@ -58,6 +59,7 @@ public final class DisplayBrightnessState { mCustomAnimationRate = builder.getCustomAnimationRate(); mShouldUpdateScreenBrightnessSetting = builder.shouldUpdateScreenBrightnessSetting(); mBrightnessEvent = builder.getBrightnessEvent(); + mBrightnessAdjustmentFlag = builder.getBrightnessAdjustmentFlag(); } /** @@ -138,6 +140,14 @@ public final class DisplayBrightnessState { return mBrightnessEvent; } + /** + * Gets the flag representing the reason for the brightness adjustment. This can be + * automatic(e.g. because of the change in the lux), or user initiated(e.g. moving the slider) + */ + public int getBrightnessAdjustmentFlag() { + return mBrightnessAdjustmentFlag; + } + @Override public String toString() { StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:"); @@ -157,6 +167,7 @@ public final class DisplayBrightnessState { .append(mShouldUpdateScreenBrightnessSetting); stringBuilder.append("\n mBrightnessEvent:") .append(Objects.toString(mBrightnessEvent, "null")); + stringBuilder.append("\n mBrightnessAdjustmentFlag:").append(mBrightnessAdjustmentFlag); return stringBuilder.toString(); } @@ -187,7 +198,8 @@ public final class DisplayBrightnessState { && mCustomAnimationRate == otherState.getCustomAnimationRate() && mShouldUpdateScreenBrightnessSetting == otherState.shouldUpdateScreenBrightnessSetting() - && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent()); + && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent()) + && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag(); } @Override @@ -195,7 +207,7 @@ public final class DisplayBrightnessState { return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason, mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness, mCustomAnimationRate, - mShouldUpdateScreenBrightnessSetting, mBrightnessEvent); + mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag); } /** @@ -222,6 +234,8 @@ public final class DisplayBrightnessState { private BrightnessEvent mBrightnessEvent; + public int mBrightnessAdjustmentFlag = 0; + /** * Create a builder starting with the values from the specified {@link * DisplayBrightnessState}. @@ -242,6 +256,7 @@ public final class DisplayBrightnessState { builder.setShouldUpdateScreenBrightnessSetting( state.shouldUpdateScreenBrightnessSetting()); builder.setBrightnessEvent(state.getBrightnessEvent()); + builder.setBrightnessAdjustmentFlag(state.getBrightnessAdjustmentFlag()); return builder; } @@ -418,7 +433,6 @@ public final class DisplayBrightnessState { return new DisplayBrightnessState(this); } - /** * This is used to get the BrightnessEvent object from its builder */ @@ -434,5 +448,21 @@ public final class DisplayBrightnessState { mBrightnessEvent = brightnessEvent; return this; } + + /** + * This is used to get the brightness adjustment flag from its builder + */ + public int getBrightnessAdjustmentFlag() { + return mBrightnessAdjustmentFlag; + } + + + /** + * This is used to set the brightness adjustment flag + */ + public Builder setBrightnessAdjustmentFlag(int brightnessAdjustmentFlag) { + mBrightnessAdjustmentFlag = brightnessAdjustmentFlag; + return this; + } } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index e88ace1df9bb..c5d8686e3a52 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1345,6 +1345,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call boolean slowChange = displayBrightnessState.isSlowChange(); // custom transition duration float customAnimationRate = displayBrightnessState.getCustomAnimationRate(); + int brightnessAdjustmentFlags = displayBrightnessState.getBrightnessAdjustmentFlag(); final boolean userSetBrightnessChanged = mDisplayBrightnessController.getIsUserSetScreenBrightnessUpdated(); if (displayBrightnessState.getBrightnessEvent() != null) { @@ -1392,15 +1393,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call displayBrightnessState.shouldUpdateScreenBrightnessSetting(); float currentBrightnessSetting = mDisplayBrightnessController.getCurrentBrightness(); // Apply auto-brightness. - int brightnessAdjustmentFlags = 0; // All the conditions inside this if block will be moved to AutomaticBrightnessStrategy if (mFlags.isRefactorDisplayPowerControllerEnabled() && displayBrightnessState.getBrightnessReason().getReason() == BrightnessReason.REASON_AUTOMATIC) { - brightnessAdjustmentFlags = - mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags(); - updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState; - mBrightnessReasonTemp.setReason(BrightnessReason.REASON_AUTOMATIC); if (mScreenOffBrightnessSensorController != null) { mScreenOffBrightnessSensorController.setLightSensorEnabled(false); } diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java index 8b3e4a44bde4..6a88a766fa07 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java @@ -146,7 +146,8 @@ public final class DisplayBrightnessController { synchronized (mLock) { mDisplayBrightnessStrategy = mDisplayBrightnessStrategySelector.selectStrategy( constructStrategySelectionRequest(displayPowerRequest, targetDisplayState)); - state = mDisplayBrightnessStrategy.updateBrightness(displayPowerRequest); + state = mDisplayBrightnessStrategy + .updateBrightness(constructStrategyExecutionRequest(displayPowerRequest)); } // This is a temporary measure until AutomaticBrightnessStrategy works as a traditional @@ -550,4 +551,10 @@ public final class DisplayBrightnessController { return new StrategySelectionRequest(displayPowerRequest, targetDisplayState, lastUserSetScreenBrightness, userSetBrightnessChanged); } + + private StrategyExecutionRequest constructStrategyExecutionRequest( + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + float currentScreenBrightness = getCurrentBrightness(); + return new StrategyExecutionRequest(displayPowerRequest, currentScreenBrightness); + } } diff --git a/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java b/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java new file mode 100644 index 000000000000..82c0bbfde47e --- /dev/null +++ b/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java @@ -0,0 +1,60 @@ +/* + * 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.display.brightness; + +import android.hardware.display.DisplayManagerInternal; + +import java.util.Objects; + +/** + * A wrapper class to encapsulate the request to execute the selected strategy + */ +public final class StrategyExecutionRequest { + // The request to change the associated display's state and brightness + private final DisplayManagerInternal.DisplayPowerRequest mDisplayPowerRequest; + + private final float mCurrentScreenBrightness; + + public StrategyExecutionRequest(DisplayManagerInternal.DisplayPowerRequest displayPowerRequest, + float currentScreenBrightness) { + mDisplayPowerRequest = displayPowerRequest; + mCurrentScreenBrightness = currentScreenBrightness; + } + + public DisplayManagerInternal.DisplayPowerRequest getDisplayPowerRequest() { + return mDisplayPowerRequest; + } + + public float getCurrentScreenBrightness() { + return mCurrentScreenBrightness; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StrategyExecutionRequest)) { + return false; + } + StrategyExecutionRequest other = (StrategyExecutionRequest) obj; + return Objects.equals(mDisplayPowerRequest, other.getDisplayPowerRequest()) + && mCurrentScreenBrightness == other.getCurrentScreenBrightness(); + } + + @Override + public int hashCode() { + return Objects.hash(mDisplayPowerRequest, mCurrentScreenBrightness); + } +} diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java index 4be7332f64e8..5c4fa842c383 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java @@ -20,7 +20,6 @@ import static android.hardware.display.DisplayManagerInternal.DisplayPowerReques import android.annotation.Nullable; import android.content.Context; import android.hardware.display.BrightnessConfiguration; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import android.os.UserHandle; import android.provider.Settings; @@ -32,6 +31,7 @@ import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessEvent; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -96,13 +96,21 @@ public class AutomaticBrightnessStrategy extends AutomaticBrightnessStrategy2 // want to re-evaluate the auto-brightness state private boolean mIsConfigured; - public AutomaticBrightnessStrategy(Context context, int displayId) { + private Injector mInjector; + + @VisibleForTesting + AutomaticBrightnessStrategy(Context context, int displayId, Injector injector) { super(context, displayId); mContext = context; mDisplayId = displayId; mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting(); mPendingAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; mTemporaryAutoBrightnessAdjustment = PowerManager.BRIGHTNESS_INVALID_FLOAT; + mInjector = (injector == null) ? new RealInjector() : injector; + } + + public AutomaticBrightnessStrategy(Context context, int displayId) { + this(context, displayId, null); } /** @@ -252,10 +260,10 @@ public class AutomaticBrightnessStrategy extends AutomaticBrightnessStrategy2 @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { BrightnessReason brightnessReason = new BrightnessReason(); brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC); - BrightnessEvent brightnessEvent = new BrightnessEvent(mDisplayId); + BrightnessEvent brightnessEvent = mInjector.getBrightnessEvent(mDisplayId); float brightness = getAutomaticScreenBrightness(brightnessEvent); return new DisplayBrightnessState.Builder() .setBrightness(brightness) @@ -265,6 +273,9 @@ public class AutomaticBrightnessStrategy extends AutomaticBrightnessStrategy2 .setIsSlowChange(hasAppliedAutoBrightness() && !getAutoBrightnessAdjustmentChanged()) .setBrightnessEvent(brightnessEvent) + .setBrightnessAdjustmentFlag(mAutoBrightnessAdjustmentReasonsFlags) + .setShouldUpdateScreenBrightnessSetting( + brightness != strategyExecutionRequest.getCurrentScreenBrightness()) .build(); } @@ -360,13 +371,6 @@ public class AutomaticBrightnessStrategy extends AutomaticBrightnessStrategy2 } /** - * Gets the auto-brightness adjustment flag change reason - */ - public int getAutoBrightnessAdjustmentReasonsFlags() { - return mAutoBrightnessAdjustmentReasonsFlags; - } - - /** * Returns if the auto brightness has been applied */ public boolean hasAppliedAutoBrightness() { @@ -497,4 +501,15 @@ public class AutomaticBrightnessStrategy extends AutomaticBrightnessStrategy2 Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); return Float.isNaN(adj) ? 0.0f : BrightnessUtils.clampBrightnessAdjustment(adj); } + + @VisibleForTesting + interface Injector { + BrightnessEvent getBrightnessEvent(int displayId); + } + + static class RealInjector implements Injector { + public BrightnessEvent getBrightnessEvent(int displayId) { + return new BrightnessEvent(displayId); + } + } } diff --git a/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java index 9c1aceab410c..009a47ad8ade 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java @@ -16,12 +16,12 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -37,7 +37,7 @@ public class BoostBrightnessStrategy implements DisplayBrightnessStrategy { // Set the brightness to the maximum value when display brightness boost is requested @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { // Todo(b/241308599): Introduce a validator class and add validations before setting // the brightness DisplayBrightnessState displayBrightnessState = diff --git a/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java index 61dd6d5bb98c..e96b83a4e11b 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java @@ -17,9 +17,9 @@ package com.android.server.display.brightness.strategy; import android.annotation.NonNull; -import android.hardware.display.DisplayManagerInternal; import com.android.server.display.DisplayBrightnessState; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -33,10 +33,10 @@ public interface DisplayBrightnessStrategy { /** * Decides the DisplayBrightnessState that the system should change to. * - * @param displayPowerRequest The request to evaluate the updated brightness + * @param strategyExecutionRequest The request to evaluate the updated brightness */ DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest); + StrategyExecutionRequest strategyExecutionRequest); /** * Returns the name of the Strategy diff --git a/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java index 1f7efd11009e..2b493f3a181f 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java @@ -16,11 +16,10 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; - import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -32,11 +31,12 @@ public class DozeBrightnessStrategy implements DisplayBrightnessStrategy { @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { // Todo(b/241308599): Introduce a validator class and add validations before setting // the brightness return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_DOZE, - displayPowerRequest.dozeScreenBrightness, displayPowerRequest.dozeScreenBrightness, + strategyExecutionRequest.getDisplayPowerRequest().dozeScreenBrightness, + strategyExecutionRequest.getDisplayPowerRequest().dozeScreenBrightness, getName()); } diff --git a/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java index baac2769f3b5..5a07ce2a719d 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java @@ -16,12 +16,12 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -49,7 +49,7 @@ public class FollowerBrightnessStrategy implements DisplayBrightnessStrategy { @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { // Todo(b/241308599): Introduce a validator class and add validations before setting // the brightness return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_FOLLOWER, diff --git a/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java index dfd47a383fe7..9dc6cffe5c1e 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java @@ -16,12 +16,12 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -32,7 +32,7 @@ import java.io.PrintWriter; public class InvalidBrightnessStrategy implements DisplayBrightnessStrategy { @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_UNKNOWN, PowerManager.BRIGHTNESS_INVALID_FLOAT, PowerManager.BRIGHTNESS_INVALID_FLOAT, getName()); diff --git a/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java index 60e5eb0dbcac..b46873aec814 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java @@ -16,11 +16,11 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import com.android.server.display.feature.DisplayManagerFlags; @@ -42,7 +42,7 @@ public class OffloadBrightnessStrategy implements DisplayBrightnessStrategy { @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { float offloadBrightness = mOffloadScreenBrightness; if (mDisplayManagerFlags.isRefactorDisplayPowerControllerEnabled()) { // We reset the offload brightness to invalid so that there is no stale value lingering diff --git a/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java index 9605a8823141..a2982b16454f 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java @@ -16,11 +16,10 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; - import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -31,12 +30,13 @@ import java.io.PrintWriter; public class OverrideBrightnessStrategy implements DisplayBrightnessStrategy { @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { // Todo(b/241308599): Introduce a validator class and add validations before setting // the brightness return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_OVERRIDE, - displayPowerRequest.screenBrightnessOverride, - displayPowerRequest.screenBrightnessOverride, getName()); + strategyExecutionRequest.getDisplayPowerRequest().screenBrightnessOverride, + strategyExecutionRequest.getDisplayPowerRequest() + .screenBrightnessOverride, getName()); } @Override diff --git a/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java index c9dc29889e31..6a3162c75dc5 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java @@ -16,12 +16,12 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -32,7 +32,7 @@ import java.io.PrintWriter; public class ScreenOffBrightnessStrategy implements DisplayBrightnessStrategy { @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { // Todo(b/241308599): Introduce a validator class and add validations before setting // the brightness return BrightnessUtils.constructDisplayBrightnessState(BrightnessReason.REASON_SCREEN_OFF, diff --git a/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java index 6a691d19c1bd..6b8817a3b62a 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java @@ -16,12 +16,12 @@ package com.android.server.display.brightness.strategy; -import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import java.io.PrintWriter; @@ -43,7 +43,7 @@ public class TemporaryBrightnessStrategy implements DisplayBrightnessStrategy { // WindowManager or based on the display state. @Override public DisplayBrightnessState updateBrightness( - DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) { + StrategyExecutionRequest strategyExecutionRequest) { // Todo(b/241308599): Introduce a validator class and add validations before setting // the brightness DisplayBrightnessState displayBrightnessState = diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java index 0e8a5fb050dd..a818eabe9597 100644 --- a/services/core/java/com/android/server/dreams/DreamController.java +++ b/services/core/java/com/android/server/dreams/DreamController.java @@ -249,6 +249,16 @@ final class DreamController { mCurrentDream.mAppTask = appTask; } + void setDreamHasFocus(boolean hasFocus) { + if (mCurrentDream != null) { + mCurrentDream.mDreamHasFocus = hasFocus; + } + } + + boolean dreamHasFocus() { + return mCurrentDream != null && mCurrentDream.mDreamHasFocus; + } + /** * Sends a user activity signal to PowerManager to stop the screen from turning off immediately * if there hasn't been any user interaction in a while. @@ -271,6 +281,21 @@ final class DreamController { stopDreamInstance(immediate, reason, mCurrentDream); } + public boolean bringDreamToFront() { + if (mCurrentDream == null || mCurrentDream.mService == null) { + return false; + } + + try { + mCurrentDream.mService.comeToFront(); + return true; + } catch (RemoteException e) { + Slog.e(TAG, "Error asking dream to come to the front", e); + } + + return false; + } + /** * Stops the given dream instance. * @@ -426,6 +451,7 @@ final class DreamController { private String mStopReason; private long mDreamStartTime; public boolean mWakingGently; + public boolean mDreamHasFocus; private final Runnable mStopPreviousDreamsIfNeeded = this::stopPreviousDreamsIfNeeded; private final Runnable mReleaseWakeLockIfNeeded = this::releaseWakeLockIfNeeded; diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 42c9e082cf70..fc63494d3c99 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.BIND_DREAM_SERVICE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.service.dreams.Flags.dreamTracksFocus; import static com.android.server.wm.ActivityInterceptorCallback.DREAM_MANAGER_ORDERED_ID; @@ -406,8 +407,10 @@ public final class DreamManagerService extends SystemService { /** Whether dreaming can start given user settings and the current dock/charge state. */ private boolean canStartDreamingInternal(boolean isScreenOn) { synchronized (mLock) { - // Can't start dreaming if we are already dreaming. - if (isScreenOn && isDreamingInternal()) { + // Can't start dreaming if we are already dreaming and the dream has focus. If we are + // dreaming but the dream does not have focus, then the dream can be brought to the + // front so it does have focus. + if (isScreenOn && isDreamingInternal() && dreamHasFocus()) { return false; } @@ -442,11 +445,20 @@ public final class DreamManagerService extends SystemService { } } + private boolean dreamHasFocus() { + // Dreams always had focus before they were able to track it. + return !dreamTracksFocus() || mController.dreamHasFocus(); + } + protected void requestStartDreamFromShell() { requestDreamInternal(); } private void requestDreamInternal() { + if (isDreamingInternal() && !dreamHasFocus() && mController.bringDreamToFront()) { + return; + } + // Ask the power manager to nap. It will eventually call back into // startDream() if/when it is appropriate to start dreaming. // Because napping could cause the screen to turn off immediately if the dream @@ -1128,6 +1140,16 @@ public final class DreamManagerService extends SystemService { }); } + @Override + public void onDreamFocusChanged(boolean hasFocus) { + final long ident = Binder.clearCallingIdentity(); + try { + mController.setDreamHasFocus(hasFocus); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + boolean canLaunchDreamActivity(String dreamPackageName, String packageName, int callingUid) { if (dreamPackageName == null || packageName == null) { diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java index b18871cbf809..a0dbfa082978 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java @@ -28,6 +28,7 @@ import java.util.ArrayDeque; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Random; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -77,9 +78,12 @@ import java.util.concurrent.atomic.AtomicInteger; private final AtomicInteger mNextAvailableId = new AtomicInteger(); /** - * The next available message sequence number + * The next available message sequence number. We choose a random + * number to start with to avoid collisions and limit the bound to + * half of the max value to avoid overflow. */ - private final AtomicInteger mNextAvailableMessageSequenceNumber = new AtomicInteger(); + private final AtomicInteger mNextAvailableMessageSequenceNumber = + new AtomicInteger(new Random().nextInt(Integer.MAX_VALUE / 2)); /* * An executor and the future object for scheduling timeout timers diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index e75d0a3285e7..38c95f771601 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -502,8 +502,8 @@ public final class NotificationRecord { pw.println(prefix + "uid=" + getSbn().getUid() + " userId=" + getSbn().getUserId()); pw.println(prefix + "opPkg=" + getSbn().getOpPkg()); pw.println(prefix + "icon=" + notification.getSmallIcon()); - pw.println(prefix + "flags=0x" + Integer.toHexString(notification.flags)); - pw.println(prefix + "originalFlags=0x" + Integer.toHexString(mOriginalFlags)); + pw.println(prefix + "flags=" + Notification.flagsToString(notification.flags)); + pw.println(prefix + "originalFlags=" + Notification.flagsToString(mOriginalFlags)); pw.println(prefix + "pri=" + notification.priority); pw.println(prefix + "key=" + getSbn().getKey()); pw.println(prefix + "seen=" + mStats.hasSeen()); @@ -538,8 +538,7 @@ public final class NotificationRecord { pw.println(prefix + "mInterruptionTimeMs=" + mInterruptionTimeMs); pw.println(prefix + "mSuppressedVisualEffects= " + mSuppressedVisualEffects); if (mPreChannelsNotification) { - pw.println(prefix + String.format("defaults=0x%08x flags=0x%08x", - notification.defaults, notification.flags)); + pw.println(prefix + "defaults=" + Notification.defaultsToString(notification.defaults)); pw.println(prefix + "n.sound=" + notification.sound); pw.println(prefix + "n.audioStreamType=" + notification.audioStreamType); pw.println(prefix + "n.audioAttributes=" + notification.audioAttributes); diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 8f6aa955c0b1..99401a17f83c 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -207,8 +207,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { return; } ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); mRemoteOnDeviceIntelligenceService.run( - service -> service.getFeature(Binder.getCallingUid(), id, featureCallback)); + service -> service.getFeature(callerUid, id, featureCallback)); } @Override @@ -227,8 +228,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { return; } ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); mRemoteOnDeviceIntelligenceService.run( - service -> service.listFeatures(Binder.getCallingUid(), + service -> service.listFeatures(callerUid, listFeaturesCallback)); } @@ -250,8 +252,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { return; } ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); mRemoteOnDeviceIntelligenceService.run( - service -> service.getFeatureDetails(Binder.getCallingUid(), feature, + service -> service.getFeatureDetails(callerUid, feature, featureDetailsCallback)); } @@ -272,8 +275,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteIntelligenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); mRemoteOnDeviceIntelligenceService.run( - service -> service.requestFeatureDownload(Binder.getCallingUid(), feature, + service -> service.requestFeatureDownload(callerUid, feature, wrapCancellationFuture(cancellationSignalFuture), downloadCallback)); } @@ -301,9 +305,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteInferenceServiceInitialized(); - + int callerUid = Binder.getCallingUid(); result = mRemoteInferenceService.post( - service -> service.requestTokenInfo(Binder.getCallingUid(), feature, + service -> service.requestTokenInfo(callerUid, feature, request, wrapCancellationFuture(cancellationSignalFuture), wrapWithValidation(tokenInfoCallback))); @@ -340,8 +344,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteInferenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); result = mRemoteInferenceService.post( - service -> service.processRequest(Binder.getCallingUid(), feature, + service -> service.processRequest(callerUid, feature, request, requestType, wrapCancellationFuture(cancellationSignalFuture), @@ -379,8 +384,9 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteInferenceServiceInitialized(); + int callerUid = Binder.getCallingUid(); result = mRemoteInferenceService.post( - service -> service.processRequestStreaming(Binder.getCallingUid(), + service -> service.processRequestStreaming(callerUid, feature, request, requestType, wrapCancellationFuture(cancellationSignalFuture), diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 20c5b5f8a308..28254d0d863e 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -462,8 +462,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { } @Override - public int getNumRegisteredAttributionSources(int uid) { - return mAttributionSourceRegistry.getNumRegisteredAttributionSources(uid); + public int getRegisteredAttributionSourceCount(int uid) { + return mAttributionSourceRegistry.getRegisteredAttributionSourceCount(uid); } @Override @@ -943,7 +943,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { } } - public int getNumRegisteredAttributionSources(int uid) { + public int getRegisteredAttributionSourceCount(int uid) { mContext.enforceCallingOrSelfPermission(UPDATE_APP_OPS_STATS, "getting the number of registered AttributionSources requires " + "UPDATE_APP_OPS_STATS"); @@ -952,14 +952,13 @@ public class PermissionManagerService extends IPermissionManager.Stub { System.gc(); System.gc(); synchronized (mLock) { - int[] numForUid = { 0 }; - mAttributions.forEach((key, value) -> { - if (value.getUid() == uid) { - numForUid[0]++; + int numForUid = 0; + for (Map.Entry<IBinder, AttributionSource> entry : mAttributions.entrySet()) { + if (entry.getValue().getUid() == uid) { + numForUid++; } - - }); - return numForUid[0]; + } + return numForUid; } } diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp index d7dd902dbabc..6dadf8f16390 100644 --- a/services/core/java/com/android/server/power/hint/Android.bp +++ b/services/core/java/com/android/server/power/hint/Android.bp @@ -11,3 +11,10 @@ java_aconfig_library { name: "power_hint_flags_lib", aconfig_declarations: "power_hint_flags", } + +java_aconfig_library { + name: "power_hint_flags_lib_host", + aconfig_declarations: "power_hint_flags", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 54cb9c9a9a9b..49c4000d7308 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -19,6 +19,7 @@ package com.android.server.power.stats; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.os.BatteryStats.Uid.NUM_PROCESS_STATE; +import static android.os.BatteryStats.Uid.NUM_WIFI_BATCHED_SCAN_BINS; import static android.os.BatteryStatsManager.NUM_WIFI_STATES; import static android.os.BatteryStatsManager.NUM_WIFI_SUPPL_STATES; @@ -292,7 +293,25 @@ public class BatteryStatsImpl extends BatteryStats { private int[] mCpuPowerBracketMap; private final CpuPowerStatsCollector mCpuPowerStatsCollector; private final MobileRadioPowerStatsCollector mMobileRadioPowerStatsCollector; + private final WifiPowerStatsCollector mWifiPowerStatsCollector; private final SparseBooleanArray mPowerStatsCollectorEnabled = new SparseBooleanArray(); + private final WifiPowerStatsCollector.WifiStatsRetriever mWifiStatsRetriever = + new WifiPowerStatsCollector.WifiStatsRetriever() { + @Override + public void retrieveWifiScanTimes(Callback callback) { + synchronized (BatteryStatsImpl.this) { + retrieveWifiScanTimesLocked(callback); + } + } + + @Override + public long getWifiActiveDuration() { + synchronized (BatteryStatsImpl.this) { + return getGlobalWifiRunningTime(mClock.elapsedRealtime() * 1000, + STATS_SINCE_CHARGED) / 1000; + } + } + }; public LongSparseArray<SamplingTimer> getKernelMemoryStats() { return mKernelMemoryStats; @@ -501,6 +520,8 @@ public class BatteryStatsImpl extends BatteryStats { TimeUnit.MINUTES.toMillis(1)); setPowerStatsThrottlePeriodMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, TimeUnit.HOURS.toMillis(1)); + setPowerStatsThrottlePeriodMillis(BatteryConsumer.POWER_COMPONENT_WIFI, + TimeUnit.HOURS.toMillis(1)); } /** @@ -1885,11 +1906,12 @@ public class BatteryStatsImpl extends BatteryStats { } private class PowerStatsCollectorInjector implements CpuPowerStatsCollector.Injector, - MobileRadioPowerStatsCollector.Injector { + MobileRadioPowerStatsCollector.Injector, WifiPowerStatsCollector.Injector { private PackageManager mPackageManager; private PowerStatsCollector.ConsumedEnergyRetriever mConsumedEnergyRetriever; private NetworkStatsManager mNetworkStatsManager; private TelephonyManager mTelephonyManager; + private WifiManager mWifiManager; void setContext(Context context) { mPackageManager = context.getPackageManager(); @@ -1897,6 +1919,7 @@ public class BatteryStatsImpl extends BatteryStats { LocalServices.getService(PowerStatsInternal.class)); mNetworkStatsManager = context.getSystemService(NetworkStatsManager.class); mTelephonyManager = context.getSystemService(TelephonyManager.class); + mWifiManager = context.getSystemService(WifiManager.class); } @Override @@ -1950,11 +1973,26 @@ public class BatteryStatsImpl extends BatteryStats { } @Override + public Supplier<NetworkStats> getWifiNetworkStatsSupplier() { + return () -> readWifiNetworkStatsLocked(mNetworkStatsManager); + } + + @Override + public WifiPowerStatsCollector.WifiStatsRetriever getWifiStatsRetriever() { + return mWifiStatsRetriever; + } + + @Override public TelephonyManager getTelephonyManager() { return mTelephonyManager; } @Override + public WifiManager getWifiManager() { + return mWifiManager; + } + + @Override public LongSupplier getCallDurationSupplier() { return () -> mPhoneOnTimer.getTotalTimeLocked(mClock.elapsedRealtime() * 1000, STATS_SINCE_CHARGED); @@ -6354,7 +6392,11 @@ public class BatteryStatsImpl extends BatteryStats { HistoryItem.STATE2_WIFI_ON_FLAG); mWifiOn = true; mWifiOnTimer.startRunningLocked(elapsedRealtimeMs); - scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI); + if (mWifiPowerStatsCollector.isEnabled()) { + mWifiPowerStatsCollector.schedule(); + } else { + scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI); + } } } @@ -6365,7 +6407,11 @@ public class BatteryStatsImpl extends BatteryStats { HistoryItem.STATE2_WIFI_ON_FLAG); mWifiOn = false; mWifiOnTimer.stopRunningLocked(elapsedRealtimeMs); - scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI); + if (mWifiPowerStatsCollector.isEnabled()) { + mWifiPowerStatsCollector.schedule(); + } else { + scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI); + } } } @@ -6757,8 +6803,11 @@ public class BatteryStatsImpl extends BatteryStats { .noteWifiRunningLocked(elapsedRealtimeMs); } } - - scheduleSyncExternalStatsLocked("wifi-running", ExternalStatsSync.UPDATE_WIFI); + if (mWifiPowerStatsCollector.isEnabled()) { + mWifiPowerStatsCollector.schedule(); + } else { + scheduleSyncExternalStatsLocked("wifi-running", ExternalStatsSync.UPDATE_WIFI); + } } else { Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running"); } @@ -6827,7 +6876,11 @@ public class BatteryStatsImpl extends BatteryStats { } } - scheduleSyncExternalStatsLocked("wifi-stopped", ExternalStatsSync.UPDATE_WIFI); + if (mWifiPowerStatsCollector.isEnabled()) { + mWifiPowerStatsCollector.schedule(); + } else { + scheduleSyncExternalStatsLocked("wifi-stopped", ExternalStatsSync.UPDATE_WIFI); + } } else { Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running"); } @@ -6842,7 +6895,11 @@ public class BatteryStatsImpl extends BatteryStats { } mWifiState = wifiState; mWifiStateTimer[wifiState].startRunningLocked(elapsedRealtimeMs); - scheduleSyncExternalStatsLocked("wifi-state", ExternalStatsSync.UPDATE_WIFI); + if (mWifiPowerStatsCollector.isEnabled()) { + mWifiPowerStatsCollector.schedule(); + } else { + scheduleSyncExternalStatsLocked("wifi-state", ExternalStatsSync.UPDATE_WIFI); + } } } @@ -6965,6 +7022,25 @@ public class BatteryStatsImpl extends BatteryStats { .noteWifiBatchedScanStoppedLocked(elapsedRealtimeMs); } + private void retrieveWifiScanTimesLocked( + WifiPowerStatsCollector.WifiStatsRetriever.Callback callback) { + long elapsedTimeUs = mClock.elapsedRealtime() * 1000; + for (int i = mUidStats.size() - 1; i >= 0; i--) { + int uid = mUidStats.keyAt(i); + Uid uidStats = mUidStats.valueAt(i); + long scanTimeUs = uidStats.getWifiScanTime(elapsedTimeUs, STATS_SINCE_CHARGED); + long batchScanTimeUs = 0; + for (int bucket = 0; bucket < NUM_WIFI_BATCHED_SCAN_BINS; bucket++) { + batchScanTimeUs += uidStats.getWifiBatchedScanTime(bucket, elapsedTimeUs, + STATS_SINCE_CHARGED); + } + if (scanTimeUs != 0 || batchScanTimeUs != 0) { + callback.onWifiScanTime(uid, (scanTimeUs + 500) / 1000, + (batchScanTimeUs + 500) / 1000); + } + } + } + private int mWifiMulticastNesting = 0; @GuardedBy("this") @@ -11101,6 +11177,11 @@ public class BatteryStatsImpl extends BatteryStats { BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO)); mMobileRadioPowerStatsCollector.addConsumer(this::recordPowerStats); + mWifiPowerStatsCollector = new WifiPowerStatsCollector( + mPowerStatsCollectorInjector, mBatteryStatsConfig.getPowerStatsThrottlePeriod( + BatteryConsumer.POWER_COMPONENT_WIFI)); + mWifiPowerStatsCollector.addConsumer(this::recordPowerStats); + mStartCount++; initTimersAndCounters(); mOnBattery = mOnBatteryInternal = false; @@ -12095,10 +12176,10 @@ public class BatteryStatsImpl extends BatteryStats { } } if (lastEntry != null) { - delta.mRxBytes = entry.getRxBytes() - lastEntry.getRxBytes(); - delta.mRxPackets = entry.getRxPackets() - lastEntry.getRxPackets(); - delta.mTxBytes = entry.getTxBytes() - lastEntry.getTxBytes(); - delta.mTxPackets = entry.getTxPackets() - lastEntry.getTxPackets(); + delta.mRxBytes = Math.max(0, entry.getRxBytes() - lastEntry.getRxBytes()); + delta.mRxPackets = Math.max(0, entry.getRxPackets() - lastEntry.getRxPackets()); + delta.mTxBytes = Math.max(0, entry.getTxBytes() - lastEntry.getTxBytes()); + delta.mTxPackets = Math.max(0, entry.getTxPackets() - lastEntry.getTxPackets()); } else { delta.mRxBytes = entry.getRxBytes(); delta.mRxPackets = entry.getRxPackets(); @@ -12119,6 +12200,10 @@ public class BatteryStatsImpl extends BatteryStats { public void updateWifiState(@Nullable final WifiActivityEnergyInfo info, final long consumedChargeUC, long elapsedRealtimeMs, long uptimeMs, @NonNull NetworkStatsManager networkStatsManager) { + if (mWifiPowerStatsCollector.isEnabled()) { + return; + } + if (DEBUG_ENERGY) { synchronized (mWifiNetworkLock) { Slog.d(TAG, "Updating wifi stats: " + Arrays.toString(mWifiIfaces)); @@ -14507,6 +14592,10 @@ public class BatteryStatsImpl extends BatteryStats { mPowerStatsCollectorEnabled.get(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO)); mMobileRadioPowerStatsCollector.schedule(); + mWifiPowerStatsCollector.setEnabled( + mPowerStatsCollectorEnabled.get(BatteryConsumer.POWER_COMPONENT_WIFI)); + mWifiPowerStatsCollector.schedule(); + mSystemReady = true; } @@ -14521,6 +14610,8 @@ public class BatteryStatsImpl extends BatteryStats { return mCpuPowerStatsCollector; case BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO: return mMobileRadioPowerStatsCollector; + case BatteryConsumer.POWER_COMPONENT_WIFI: + return mWifiPowerStatsCollector; } return null; } @@ -16056,6 +16147,7 @@ public class BatteryStatsImpl extends BatteryStats { public void schedulePowerStatsSampleCollection() { mCpuPowerStatsCollector.forceSchedule(); mMobileRadioPowerStatsCollector.forceSchedule(); + mWifiPowerStatsCollector.forceSchedule(); } /** @@ -16074,6 +16166,7 @@ public class BatteryStatsImpl extends BatteryStats { public void dumpStatsSample(PrintWriter pw) { mCpuPowerStatsCollector.collectAndDump(pw); mMobileRadioPowerStatsCollector.collectAndDump(pw); + mWifiPowerStatsCollector.collectAndDump(pw); } private final Runnable mWriteAsyncRunnable = () -> { diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java index 97f09865beeb..0d5eabc5ed47 100644 --- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java +++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java @@ -87,7 +87,9 @@ public class BatteryUsageStatsProvider { mPowerCalculators.add(new PhonePowerCalculator(mPowerProfile)); } } - mPowerCalculators.add(new WifiPowerCalculator(mPowerProfile)); + if (!mPowerStatsExporterEnabled.get(BatteryConsumer.POWER_COMPONENT_WIFI)) { + mPowerCalculators.add(new WifiPowerCalculator(mPowerProfile)); + } mPowerCalculators.add(new BluetoothPowerCalculator(mPowerProfile)); mPowerCalculators.add(new SensorPowerCalculator( mContext.getSystemService(SensorManager.class))); diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java new file mode 100644 index 000000000000..632105352ad2 --- /dev/null +++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsCollector.java @@ -0,0 +1,328 @@ +/* + * 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.power.stats; + +import android.content.pm.PackageManager; +import android.hardware.power.stats.EnergyConsumerType; +import android.net.NetworkStats; +import android.net.wifi.WifiManager; +import android.os.BatteryConsumer; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.connectivity.WifiActivityEnergyInfo; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.os.Clock; +import com.android.internal.os.PowerStats; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class WifiPowerStatsCollector extends PowerStatsCollector { + private static final String TAG = "WifiPowerStatsCollector"; + + private static final long WIFI_ACTIVITY_REQUEST_TIMEOUT = 20000; + + private static final long ENERGY_UNSPECIFIED = -1; + + interface WifiStatsRetriever { + interface Callback { + void onWifiScanTime(int uid, long scanTimeMs, long batchScanTimeMs); + } + + void retrieveWifiScanTimes(Callback callback); + long getWifiActiveDuration(); + } + + interface Injector { + Handler getHandler(); + Clock getClock(); + PowerStatsUidResolver getUidResolver(); + PackageManager getPackageManager(); + ConsumedEnergyRetriever getConsumedEnergyRetriever(); + IntSupplier getVoltageSupplier(); + Supplier<NetworkStats> getWifiNetworkStatsSupplier(); + WifiManager getWifiManager(); + WifiStatsRetriever getWifiStatsRetriever(); + } + + private final Injector mInjector; + + private WifiPowerStatsLayout mLayout; + private boolean mIsInitialized; + private boolean mPowerReportingSupported; + + private PowerStats mPowerStats; + private long[] mDeviceStats; + private volatile WifiManager mWifiManager; + private volatile Supplier<NetworkStats> mNetworkStatsSupplier; + private volatile WifiStatsRetriever mWifiStatsRetriever; + private ConsumedEnergyRetriever mConsumedEnergyRetriever; + private IntSupplier mVoltageSupplier; + private int[] mEnergyConsumerIds = new int[0]; + private WifiActivityEnergyInfo mLastWifiActivityInfo = + new WifiActivityEnergyInfo(0, 0, 0, 0, 0, 0); + private NetworkStats mLastNetworkStats; + private long[] mLastConsumedEnergyUws; + private int mLastVoltageMv; + + private static class WifiScanTimes { + public long basicScanTimeMs; + public long batchedScanTimeMs; + } + private final WifiScanTimes mScanTimes = new WifiScanTimes(); + private final SparseArray<WifiScanTimes> mLastScanTimes = new SparseArray<>(); + private long mLastWifiActiveDuration; + + public WifiPowerStatsCollector(Injector injector, long throttlePeriodMs) { + super(injector.getHandler(), throttlePeriodMs, injector.getUidResolver(), + injector.getClock()); + mInjector = injector; + } + + @Override + public void setEnabled(boolean enabled) { + if (enabled) { + PackageManager packageManager = mInjector.getPackageManager(); + super.setEnabled(packageManager != null + && packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)); + } else { + super.setEnabled(false); + } + } + + private boolean ensureInitialized() { + if (mIsInitialized) { + return true; + } + + if (!isEnabled()) { + return false; + } + + mConsumedEnergyRetriever = mInjector.getConsumedEnergyRetriever(); + mVoltageSupplier = mInjector.getVoltageSupplier(); + mWifiManager = mInjector.getWifiManager(); + mNetworkStatsSupplier = mInjector.getWifiNetworkStatsSupplier(); + mWifiStatsRetriever = mInjector.getWifiStatsRetriever(); + mPowerReportingSupported = + mWifiManager != null && mWifiManager.isEnhancedPowerReportingSupported(); + + mEnergyConsumerIds = mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.WIFI); + mLastConsumedEnergyUws = new long[mEnergyConsumerIds.length]; + Arrays.fill(mLastConsumedEnergyUws, ENERGY_UNSPECIFIED); + + mLayout = new WifiPowerStatsLayout(); + mLayout.addDeviceWifiActivity(mPowerReportingSupported); + mLayout.addDeviceSectionEnergyConsumers(mEnergyConsumerIds.length); + mLayout.addUidNetworkStats(); + mLayout.addDeviceSectionUsageDuration(); + mLayout.addDeviceSectionPowerEstimate(); + mLayout.addUidSectionPowerEstimate(); + + PersistableBundle extras = new PersistableBundle(); + mLayout.toExtras(extras); + PowerStats.Descriptor powerStatsDescriptor = new PowerStats.Descriptor( + BatteryConsumer.POWER_COMPONENT_WIFI, mLayout.getDeviceStatsArrayLength(), + null, 0, mLayout.getUidStatsArrayLength(), + extras); + mPowerStats = new PowerStats(powerStatsDescriptor); + mDeviceStats = mPowerStats.stats; + + mIsInitialized = true; + return true; + } + + @Override + protected PowerStats collectStats() { + if (!ensureInitialized()) { + return null; + } + + if (mPowerReportingSupported) { + collectWifiActivityInfo(); + } else { + collectWifiActivityStats(); + } + collectNetworkStats(); + collectWifiScanTime(); + + if (mEnergyConsumerIds.length != 0) { + collectEnergyConsumers(); + } + + return mPowerStats; + } + + private void collectWifiActivityInfo() { + CompletableFuture<WifiActivityEnergyInfo> immediateFuture = new CompletableFuture<>(); + mWifiManager.getWifiActivityEnergyInfoAsync(Runnable::run, + immediateFuture::complete); + + WifiActivityEnergyInfo activityInfo; + try { + activityInfo = immediateFuture.get(WIFI_ACTIVITY_REQUEST_TIMEOUT, + TimeUnit.MILLISECONDS); + } catch (Exception e) { + Slog.e(TAG, "Cannot acquire WifiActivityEnergyInfo", e); + activityInfo = null; + } + + if (activityInfo == null) { + return; + } + + long rxDuration = activityInfo.getControllerRxDurationMillis() + - mLastWifiActivityInfo.getControllerRxDurationMillis(); + long txDuration = activityInfo.getControllerTxDurationMillis() + - mLastWifiActivityInfo.getControllerTxDurationMillis(); + long scanDuration = activityInfo.getControllerScanDurationMillis() + - mLastWifiActivityInfo.getControllerScanDurationMillis(); + long idleDuration = activityInfo.getControllerIdleDurationMillis() + - mLastWifiActivityInfo.getControllerIdleDurationMillis(); + + mLayout.setDeviceRxTime(mDeviceStats, rxDuration); + mLayout.setDeviceTxTime(mDeviceStats, txDuration); + mLayout.setDeviceScanTime(mDeviceStats, scanDuration); + mLayout.setDeviceIdleTime(mDeviceStats, idleDuration); + + mPowerStats.durationMs = rxDuration + txDuration + scanDuration + idleDuration; + + mLastWifiActivityInfo = activityInfo; + } + + private void collectWifiActivityStats() { + long duration = mWifiStatsRetriever.getWifiActiveDuration(); + mLayout.setDeviceActiveTime(mDeviceStats, Math.max(0, duration - mLastWifiActiveDuration)); + mLastWifiActiveDuration = duration; + mPowerStats.durationMs = duration; + } + + private void collectNetworkStats() { + mPowerStats.uidStats.clear(); + + NetworkStats networkStats = mNetworkStatsSupplier.get(); + if (networkStats == null) { + return; + } + + List<BatteryStatsImpl.NetworkStatsDelta> delta = + BatteryStatsImpl.computeDelta(networkStats, mLastNetworkStats); + mLastNetworkStats = networkStats; + for (int i = delta.size() - 1; i >= 0; i--) { + BatteryStatsImpl.NetworkStatsDelta uidDelta = delta.get(i); + long rxBytes = uidDelta.getRxBytes(); + long txBytes = uidDelta.getTxBytes(); + long rxPackets = uidDelta.getRxPackets(); + long txPackets = uidDelta.getTxPackets(); + if (rxBytes == 0 && txBytes == 0 && rxPackets == 0 && txPackets == 0) { + continue; + } + + int uid = mUidResolver.mapUid(uidDelta.getUid()); + long[] stats = mPowerStats.uidStats.get(uid); + if (stats == null) { + stats = new long[mLayout.getUidStatsArrayLength()]; + mPowerStats.uidStats.put(uid, stats); + mLayout.setUidRxBytes(stats, rxBytes); + mLayout.setUidTxBytes(stats, txBytes); + mLayout.setUidRxPackets(stats, rxPackets); + mLayout.setUidTxPackets(stats, txPackets); + } else { + mLayout.setUidRxBytes(stats, mLayout.getUidRxBytes(stats) + rxBytes); + mLayout.setUidTxBytes(stats, mLayout.getUidTxBytes(stats) + txBytes); + mLayout.setUidRxPackets(stats, mLayout.getUidRxPackets(stats) + rxPackets); + mLayout.setUidTxPackets(stats, mLayout.getUidTxPackets(stats) + txPackets); + } + } + } + + private void collectWifiScanTime() { + mScanTimes.basicScanTimeMs = 0; + mScanTimes.batchedScanTimeMs = 0; + mWifiStatsRetriever.retrieveWifiScanTimes((uid, scanTimeMs, batchScanTimeMs) -> { + WifiScanTimes lastScanTimes = mLastScanTimes.get(uid); + if (lastScanTimes == null) { + lastScanTimes = new WifiScanTimes(); + mLastScanTimes.put(uid, lastScanTimes); + } + + long scanTimeDelta = Math.max(0, scanTimeMs - lastScanTimes.basicScanTimeMs); + long batchScanTimeDelta = Math.max(0, + batchScanTimeMs - lastScanTimes.batchedScanTimeMs); + if (scanTimeDelta != 0 || batchScanTimeDelta != 0) { + mScanTimes.basicScanTimeMs += scanTimeDelta; + mScanTimes.batchedScanTimeMs += batchScanTimeDelta; + uid = mUidResolver.mapUid(uid); + long[] stats = mPowerStats.uidStats.get(uid); + if (stats == null) { + stats = new long[mLayout.getUidStatsArrayLength()]; + mPowerStats.uidStats.put(uid, stats); + mLayout.setUidScanTime(stats, scanTimeDelta); + mLayout.setUidBatchScanTime(stats, batchScanTimeDelta); + } else { + mLayout.setUidScanTime(stats, mLayout.getUidScanTime(stats) + scanTimeDelta); + mLayout.setUidBatchScanTime(stats, + mLayout.getUidBatchedScanTime(stats) + batchScanTimeDelta); + } + } + lastScanTimes.basicScanTimeMs = scanTimeMs; + lastScanTimes.batchedScanTimeMs = batchScanTimeMs; + }); + + mLayout.setDeviceBasicScanTime(mDeviceStats, mScanTimes.basicScanTimeMs); + mLayout.setDeviceBatchedScanTime(mDeviceStats, mScanTimes.batchedScanTimeMs); + } + + private void collectEnergyConsumers() { + int voltageMv = mVoltageSupplier.getAsInt(); + if (voltageMv <= 0) { + Slog.wtf(TAG, "Unexpected battery voltage (" + voltageMv + + " mV) when querying energy consumers"); + return; + } + + int averageVoltage = mLastVoltageMv != 0 ? (mLastVoltageMv + voltageMv) / 2 : voltageMv; + mLastVoltageMv = voltageMv; + + long[] energyUws = mConsumedEnergyRetriever.getConsumedEnergyUws(mEnergyConsumerIds); + if (energyUws == null) { + return; + } + + for (int i = energyUws.length - 1; i >= 0; i--) { + long energyDelta = mLastConsumedEnergyUws[i] != ENERGY_UNSPECIFIED + ? energyUws[i] - mLastConsumedEnergyUws[i] : 0; + if (energyDelta < 0) { + // Likely, restart of powerstats HAL + energyDelta = 0; + } + mLayout.setConsumedEnergy(mPowerStats.stats, i, uJtoUc(energyDelta, averageVoltage)); + mLastConsumedEnergyUws[i] = energyUws[i]; + } + } + + @Override + protected void onUidRemoved(int uid) { + super.onUidRemoved(uid); + mLastScanTimes.remove(uid); + } +} diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsLayout.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsLayout.java new file mode 100644 index 000000000000..0fa6ec65c4bc --- /dev/null +++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsLayout.java @@ -0,0 +1,241 @@ +/* + * 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.power.stats; + +import android.annotation.NonNull; +import android.os.PersistableBundle; + +import com.android.internal.os.PowerStats; + +public class WifiPowerStatsLayout extends PowerStatsLayout { + private static final String TAG = "WifiPowerStatsLayout"; + private static final int UNSPECIFIED = -1; + private static final String EXTRA_POWER_REPORTING_SUPPORTED = "prs"; + private static final String EXTRA_DEVICE_RX_TIME_POSITION = "dt-rx"; + private static final String EXTRA_DEVICE_TX_TIME_POSITION = "dt-tx"; + private static final String EXTRA_DEVICE_SCAN_TIME_POSITION = "dt-scan"; + private static final String EXTRA_DEVICE_BASIC_SCAN_TIME_POSITION = "dt-basic-scan"; + private static final String EXTRA_DEVICE_BATCHED_SCAN_TIME_POSITION = "dt-batch-scan"; + private static final String EXTRA_DEVICE_IDLE_TIME_POSITION = "dt-idle"; + private static final String EXTRA_DEVICE_ACTIVE_TIME_POSITION = "dt-on"; + private static final String EXTRA_UID_RX_BYTES_POSITION = "urxb"; + private static final String EXTRA_UID_TX_BYTES_POSITION = "utxb"; + private static final String EXTRA_UID_RX_PACKETS_POSITION = "urxp"; + private static final String EXTRA_UID_TX_PACKETS_POSITION = "utxp"; + private static final String EXTRA_UID_SCAN_TIME_POSITION = "ut-scan"; + private static final String EXTRA_UID_BATCH_SCAN_TIME_POSITION = "ut-bscan"; + + private boolean mPowerReportingSupported; + private int mDeviceRxTimePosition; + private int mDeviceTxTimePosition; + private int mDeviceIdleTimePosition; + private int mDeviceScanTimePosition; + private int mDeviceBasicScanTimePosition; + private int mDeviceBatchedScanTimePosition; + private int mDeviceActiveTimePosition; + private int mUidRxBytesPosition; + private int mUidTxBytesPosition; + private int mUidRxPacketsPosition; + private int mUidTxPacketsPosition; + private int mUidScanTimePosition; + private int mUidBatchScanTimePosition; + + WifiPowerStatsLayout() { + } + + WifiPowerStatsLayout(@NonNull PowerStats.Descriptor descriptor) { + super(descriptor); + } + + void addDeviceWifiActivity(boolean powerReportingSupported) { + mPowerReportingSupported = powerReportingSupported; + if (mPowerReportingSupported) { + mDeviceActiveTimePosition = UNSPECIFIED; + mDeviceRxTimePosition = addDeviceSection(1); + mDeviceTxTimePosition = addDeviceSection(1); + mDeviceIdleTimePosition = addDeviceSection(1); + mDeviceScanTimePosition = addDeviceSection(1); + } else { + mDeviceActiveTimePosition = addDeviceSection(1); + mDeviceRxTimePosition = UNSPECIFIED; + mDeviceTxTimePosition = UNSPECIFIED; + mDeviceIdleTimePosition = UNSPECIFIED; + mDeviceScanTimePosition = UNSPECIFIED; + } + mDeviceBasicScanTimePosition = addDeviceSection(1); + mDeviceBatchedScanTimePosition = addDeviceSection(1); + } + + void addUidNetworkStats() { + mUidRxBytesPosition = addUidSection(1); + mUidTxBytesPosition = addUidSection(1); + mUidRxPacketsPosition = addUidSection(1); + mUidTxPacketsPosition = addUidSection(1); + mUidScanTimePosition = addUidSection(1); + mUidBatchScanTimePosition = addUidSection(1); + } + + public boolean isPowerReportingSupported() { + return mPowerReportingSupported; + } + + public void setDeviceRxTime(long[] stats, long durationMillis) { + stats[mDeviceRxTimePosition] = durationMillis; + } + + public long getDeviceRxTime(long[] stats) { + return stats[mDeviceRxTimePosition]; + } + + public void setDeviceTxTime(long[] stats, long durationMillis) { + stats[mDeviceTxTimePosition] = durationMillis; + } + + public long getDeviceTxTime(long[] stats) { + return stats[mDeviceTxTimePosition]; + } + + public void setDeviceScanTime(long[] stats, long durationMillis) { + stats[mDeviceScanTimePosition] = durationMillis; + } + + public long getDeviceScanTime(long[] stats) { + return stats[mDeviceScanTimePosition]; + } + + public void setDeviceBasicScanTime(long[] stats, long durationMillis) { + stats[mDeviceBasicScanTimePosition] = durationMillis; + } + + public long getDeviceBasicScanTime(long[] stats) { + return stats[mDeviceBasicScanTimePosition]; + } + + public void setDeviceBatchedScanTime(long[] stats, long durationMillis) { + stats[mDeviceBatchedScanTimePosition] = durationMillis; + } + + public long getDeviceBatchedScanTime(long[] stats) { + return stats[mDeviceBatchedScanTimePosition]; + } + + public void setDeviceIdleTime(long[] stats, long durationMillis) { + stats[mDeviceIdleTimePosition] = durationMillis; + } + + public long getDeviceIdleTime(long[] stats) { + return stats[mDeviceIdleTimePosition]; + } + + public void setDeviceActiveTime(long[] stats, long durationMillis) { + stats[mDeviceActiveTimePosition] = durationMillis; + } + + public long getDeviceActiveTime(long[] stats) { + return stats[mDeviceActiveTimePosition]; + } + + public void setUidRxBytes(long[] stats, long count) { + stats[mUidRxBytesPosition] = count; + } + + public long getUidRxBytes(long[] stats) { + return stats[mUidRxBytesPosition]; + } + + public void setUidTxBytes(long[] stats, long count) { + stats[mUidTxBytesPosition] = count; + } + + public long getUidTxBytes(long[] stats) { + return stats[mUidTxBytesPosition]; + } + + public void setUidRxPackets(long[] stats, long count) { + stats[mUidRxPacketsPosition] = count; + } + + public long getUidRxPackets(long[] stats) { + return stats[mUidRxPacketsPosition]; + } + + public void setUidTxPackets(long[] stats, long count) { + stats[mUidTxPacketsPosition] = count; + } + + public long getUidTxPackets(long[] stats) { + return stats[mUidTxPacketsPosition]; + } + + public void setUidScanTime(long[] stats, long count) { + stats[mUidScanTimePosition] = count; + } + + public long getUidScanTime(long[] stats) { + return stats[mUidScanTimePosition]; + } + + public void setUidBatchScanTime(long[] stats, long count) { + stats[mUidBatchScanTimePosition] = count; + } + + public long getUidBatchedScanTime(long[] stats) { + return stats[mUidBatchScanTimePosition]; + } + + /** + * Copies the elements of the stats array layout into <code>extras</code> + */ + public void toExtras(PersistableBundle extras) { + super.toExtras(extras); + extras.putBoolean(EXTRA_POWER_REPORTING_SUPPORTED, mPowerReportingSupported); + extras.putInt(EXTRA_DEVICE_RX_TIME_POSITION, mDeviceRxTimePosition); + extras.putInt(EXTRA_DEVICE_TX_TIME_POSITION, mDeviceTxTimePosition); + extras.putInt(EXTRA_DEVICE_SCAN_TIME_POSITION, mDeviceScanTimePosition); + extras.putInt(EXTRA_DEVICE_BASIC_SCAN_TIME_POSITION, mDeviceBasicScanTimePosition); + extras.putInt(EXTRA_DEVICE_BATCHED_SCAN_TIME_POSITION, mDeviceBatchedScanTimePosition); + extras.putInt(EXTRA_DEVICE_IDLE_TIME_POSITION, mDeviceIdleTimePosition); + extras.putInt(EXTRA_DEVICE_ACTIVE_TIME_POSITION, mDeviceActiveTimePosition); + extras.putInt(EXTRA_UID_RX_BYTES_POSITION, mUidRxBytesPosition); + extras.putInt(EXTRA_UID_TX_BYTES_POSITION, mUidTxBytesPosition); + extras.putInt(EXTRA_UID_RX_PACKETS_POSITION, mUidRxPacketsPosition); + extras.putInt(EXTRA_UID_TX_PACKETS_POSITION, mUidTxPacketsPosition); + extras.putInt(EXTRA_UID_SCAN_TIME_POSITION, mUidScanTimePosition); + extras.putInt(EXTRA_UID_BATCH_SCAN_TIME_POSITION, mUidBatchScanTimePosition); + } + + /** + * Retrieves elements of the stats array layout from <code>extras</code> + */ + public void fromExtras(PersistableBundle extras) { + super.fromExtras(extras); + mPowerReportingSupported = extras.getBoolean(EXTRA_POWER_REPORTING_SUPPORTED); + mDeviceRxTimePosition = extras.getInt(EXTRA_DEVICE_RX_TIME_POSITION); + mDeviceTxTimePosition = extras.getInt(EXTRA_DEVICE_TX_TIME_POSITION); + mDeviceScanTimePosition = extras.getInt(EXTRA_DEVICE_SCAN_TIME_POSITION); + mDeviceBasicScanTimePosition = extras.getInt(EXTRA_DEVICE_BASIC_SCAN_TIME_POSITION); + mDeviceBatchedScanTimePosition = extras.getInt(EXTRA_DEVICE_BATCHED_SCAN_TIME_POSITION); + mDeviceIdleTimePosition = extras.getInt(EXTRA_DEVICE_IDLE_TIME_POSITION); + mDeviceActiveTimePosition = extras.getInt(EXTRA_DEVICE_ACTIVE_TIME_POSITION); + mUidRxBytesPosition = extras.getInt(EXTRA_UID_RX_BYTES_POSITION); + mUidTxBytesPosition = extras.getInt(EXTRA_UID_TX_BYTES_POSITION); + mUidRxPacketsPosition = extras.getInt(EXTRA_UID_RX_PACKETS_POSITION); + mUidTxPacketsPosition = extras.getInt(EXTRA_UID_TX_PACKETS_POSITION); + mUidScanTimePosition = extras.getInt(EXTRA_UID_SCAN_TIME_POSITION); + mUidBatchScanTimePosition = extras.getInt(EXTRA_UID_BATCH_SCAN_TIME_POSITION); + } +} diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java new file mode 100644 index 000000000000..5e9cc4092029 --- /dev/null +++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java @@ -0,0 +1,425 @@ +/* + * 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.power.stats; + +import android.util.Slog; + +import com.android.internal.os.PowerProfile; +import com.android.internal.os.PowerStats; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class WifiPowerStatsProcessor extends PowerStatsProcessor { + private static final String TAG = "WifiPowerStatsProcessor"; + private static final boolean DEBUG = false; + + private final UsageBasedPowerEstimator mRxPowerEstimator; + private final UsageBasedPowerEstimator mTxPowerEstimator; + private final UsageBasedPowerEstimator mIdlePowerEstimator; + + private final UsageBasedPowerEstimator mActivePowerEstimator; + private final UsageBasedPowerEstimator mScanPowerEstimator; + private final UsageBasedPowerEstimator mBatchedScanPowerEstimator; + + private PowerStats.Descriptor mLastUsedDescriptor; + private WifiPowerStatsLayout mStatsLayout; + // Sequence of steps for power estimation and intermediate results. + private PowerEstimationPlan mPlan; + + private long[] mTmpDeviceStatsArray; + private long[] mTmpUidStatsArray; + private boolean mHasWifiPowerController; + + public WifiPowerStatsProcessor(PowerProfile powerProfile) { + mRxPowerEstimator = new UsageBasedPowerEstimator( + powerProfile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX)); + mTxPowerEstimator = new UsageBasedPowerEstimator( + powerProfile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX)); + mIdlePowerEstimator = new UsageBasedPowerEstimator( + powerProfile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE)); + mActivePowerEstimator = new UsageBasedPowerEstimator( + powerProfile.getAveragePower(PowerProfile.POWER_WIFI_ACTIVE)); + mScanPowerEstimator = new UsageBasedPowerEstimator( + powerProfile.getAveragePower(PowerProfile.POWER_WIFI_SCAN)); + mBatchedScanPowerEstimator = new UsageBasedPowerEstimator( + powerProfile.getAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN)); + } + + private static class Intermediates { + /** + * Estimated power for the RX state. + */ + public double rxPower; + /** + * Estimated power for the TX state. + */ + public double txPower; + /** + * Estimated power in the SCAN state + */ + public double scanPower; + /** + * Estimated power for IDLE, SCAN states. + */ + public double idlePower; + /** + * Number of received packets + */ + public long rxPackets; + /** + * Number of transmitted packets + */ + public long txPackets; + /** + * Total duration of unbatched scans across all UIDs. + */ + public long basicScanDuration; + /** + * Estimated power in the unbatched SCAN state + */ + public double basicScanPower; + /** + * Total duration of batched scans across all UIDs. + */ + public long batchedScanDuration; + /** + * Estimated power in the BATCHED SCAN state + */ + public double batchedScanPower; + /** + * Estimated total power when active; used only in the absence of WiFiManager power + * reporting. + */ + public double activePower; + /** + * Measured consumed energy from power monitoring hardware (micro-coulombs) + */ + public long consumedEnergy; + } + + @Override + void finish(PowerComponentAggregatedPowerStats stats) { + if (stats.getPowerStatsDescriptor() == null) { + return; + } + + unpackPowerStatsDescriptor(stats.getPowerStatsDescriptor()); + + if (mPlan == null) { + mPlan = new PowerEstimationPlan(stats.getConfig()); + } + + for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) { + DeviceStateEstimation estimation = mPlan.deviceStateEstimations.get(i); + Intermediates intermediates = new Intermediates(); + estimation.intermediates = intermediates; + computeDevicePowerEstimates(stats, estimation.stateValues, intermediates); + } + + double ratio = 1.0; + if (mStatsLayout.getEnergyConsumerCount() != 0) { + ratio = computeEstimateAdjustmentRatioUsingConsumedEnergy(); + if (ratio != 1) { + for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) { + DeviceStateEstimation estimation = mPlan.deviceStateEstimations.get(i); + adjustDevicePowerEstimates(stats, estimation.stateValues, + (Intermediates) estimation.intermediates, ratio); + } + } + } + + combineDeviceStateEstimates(); + + ArrayList<Integer> uids = new ArrayList<>(); + stats.collectUids(uids); + if (!uids.isEmpty()) { + for (int uid : uids) { + for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { + computeUidActivityTotals(stats, uid, mPlan.uidStateEstimates.get(i)); + } + } + + for (int uid : uids) { + for (int i = 0; i < mPlan.uidStateEstimates.size(); i++) { + computeUidPowerEstimates(stats, uid, mPlan.uidStateEstimates.get(i)); + } + } + } + mPlan.resetIntermediates(); + } + + private void unpackPowerStatsDescriptor(PowerStats.Descriptor descriptor) { + if (descriptor.equals(mLastUsedDescriptor)) { + return; + } + + mLastUsedDescriptor = descriptor; + mStatsLayout = new WifiPowerStatsLayout(descriptor); + mTmpDeviceStatsArray = new long[descriptor.statsArrayLength]; + mTmpUidStatsArray = new long[descriptor.uidStatsArrayLength]; + mHasWifiPowerController = mStatsLayout.isPowerReportingSupported(); + } + + /** + * Compute power estimates using the power profile. + */ + private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats, + int[] deviceStates, Intermediates intermediates) { + if (!stats.getDeviceStats(mTmpDeviceStatsArray, deviceStates)) { + return; + } + + for (int i = mStatsLayout.getEnergyConsumerCount() - 1; i >= 0; i--) { + intermediates.consumedEnergy += mStatsLayout.getConsumedEnergy(mTmpDeviceStatsArray, i); + } + + intermediates.basicScanDuration = + mStatsLayout.getDeviceBasicScanTime(mTmpDeviceStatsArray); + intermediates.batchedScanDuration = + mStatsLayout.getDeviceBatchedScanTime(mTmpDeviceStatsArray); + if (mHasWifiPowerController) { + intermediates.rxPower = mRxPowerEstimator.calculatePower( + mStatsLayout.getDeviceRxTime(mTmpDeviceStatsArray)); + intermediates.txPower = mTxPowerEstimator.calculatePower( + mStatsLayout.getDeviceTxTime(mTmpDeviceStatsArray)); + intermediates.scanPower = mScanPowerEstimator.calculatePower( + mStatsLayout.getDeviceScanTime(mTmpDeviceStatsArray)); + intermediates.idlePower = mIdlePowerEstimator.calculatePower( + mStatsLayout.getDeviceIdleTime(mTmpDeviceStatsArray)); + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, + intermediates.rxPower + intermediates.txPower + intermediates.scanPower + + intermediates.idlePower); + } else { + intermediates.activePower = mActivePowerEstimator.calculatePower( + mStatsLayout.getDeviceActiveTime(mTmpDeviceStatsArray)); + intermediates.basicScanPower = + mScanPowerEstimator.calculatePower(intermediates.basicScanDuration); + intermediates.batchedScanPower = + mBatchedScanPowerEstimator.calculatePower(intermediates.batchedScanDuration); + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, + intermediates.activePower + intermediates.basicScanPower + + intermediates.batchedScanPower); + } + + stats.setDeviceStats(deviceStates, mTmpDeviceStatsArray); + } + + /** + * Compute an adjustment ratio using the total power estimated using the power profile + * and the total power measured by hardware. + */ + private double computeEstimateAdjustmentRatioUsingConsumedEnergy() { + long totalConsumedEnergy = 0; + double totalPower = 0; + + for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) { + Intermediates intermediates = + (Intermediates) mPlan.deviceStateEstimations.get(i).intermediates; + if (mHasWifiPowerController) { + totalPower += intermediates.rxPower + intermediates.txPower + + intermediates.scanPower + intermediates.idlePower; + } else { + totalPower += intermediates.activePower + intermediates.basicScanPower + + intermediates.batchedScanPower; + } + totalConsumedEnergy += intermediates.consumedEnergy; + } + + if (totalPower == 0) { + return 1; + } + + return uCtoMah(totalConsumedEnergy) / totalPower; + } + + /** + * Uniformly apply the same adjustment to all power estimates in order to ensure that the total + * estimated power matches the measured consumed power. We are not claiming that all + * averages captured in the power profile have to be off by the same percentage in reality. + */ + private void adjustDevicePowerEstimates(PowerComponentAggregatedPowerStats stats, + int[] deviceStates, Intermediates intermediates, double ratio) { + double adjutedPower; + if (mHasWifiPowerController) { + intermediates.rxPower *= ratio; + intermediates.txPower *= ratio; + intermediates.scanPower *= ratio; + intermediates.idlePower *= ratio; + adjutedPower = intermediates.rxPower + intermediates.txPower + intermediates.scanPower + + intermediates.idlePower; + } else { + intermediates.activePower *= ratio; + intermediates.basicScanPower *= ratio; + intermediates.batchedScanPower *= ratio; + adjutedPower = intermediates.activePower + intermediates.basicScanPower + + intermediates.batchedScanPower; + } + + if (!stats.getDeviceStats(mTmpDeviceStatsArray, deviceStates)) { + return; + } + + mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, adjutedPower); + stats.setDeviceStats(deviceStates, mTmpDeviceStatsArray); + } + + /** + * Combine power estimates before distributing them proportionally to UIDs. + */ + private void combineDeviceStateEstimates() { + for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) { + CombinedDeviceStateEstimate cdse = mPlan.combinedDeviceStateEstimations.get(i); + Intermediates + cdseIntermediates = new Intermediates(); + cdse.intermediates = cdseIntermediates; + List<DeviceStateEstimation> deviceStateEstimations = cdse.deviceStateEstimations; + for (int j = deviceStateEstimations.size() - 1; j >= 0; j--) { + DeviceStateEstimation dse = deviceStateEstimations.get(j); + Intermediates intermediates = (Intermediates) dse.intermediates; + if (mHasWifiPowerController) { + cdseIntermediates.rxPower += intermediates.rxPower; + cdseIntermediates.txPower += intermediates.txPower; + cdseIntermediates.scanPower += intermediates.scanPower; + cdseIntermediates.idlePower += intermediates.idlePower; + } else { + cdseIntermediates.activePower += intermediates.activePower; + cdseIntermediates.basicScanPower += intermediates.basicScanPower; + cdseIntermediates.batchedScanPower += intermediates.batchedScanPower; + } + cdseIntermediates.basicScanDuration += intermediates.basicScanDuration; + cdseIntermediates.batchedScanDuration += intermediates.batchedScanDuration; + cdseIntermediates.consumedEnergy += intermediates.consumedEnergy; + } + } + } + + private void computeUidActivityTotals(PowerComponentAggregatedPowerStats stats, int uid, + UidStateEstimate uidStateEstimate) { + Intermediates intermediates = + (Intermediates) uidStateEstimate.combinedDeviceStateEstimate.intermediates; + for (UidStateProportionalEstimate proportionalEstimate : + uidStateEstimate.proportionalEstimates) { + if (!stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) { + continue; + } + + intermediates.rxPackets += mStatsLayout.getUidRxPackets(mTmpUidStatsArray); + intermediates.txPackets += mStatsLayout.getUidTxPackets(mTmpUidStatsArray); + } + } + + private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats, int uid, + UidStateEstimate uidStateEstimate) { + Intermediates intermediates = + (Intermediates) uidStateEstimate.combinedDeviceStateEstimate.intermediates; + for (UidStateProportionalEstimate proportionalEstimate : + uidStateEstimate.proportionalEstimates) { + if (!stats.getUidStats(mTmpUidStatsArray, uid, proportionalEstimate.stateValues)) { + continue; + } + + double power = 0; + if (mHasWifiPowerController) { + if (intermediates.rxPackets != 0) { + power += intermediates.rxPower * mStatsLayout.getUidRxPackets(mTmpUidStatsArray) + / intermediates.rxPackets; + } + if (intermediates.txPackets != 0) { + power += intermediates.txPower * mStatsLayout.getUidTxPackets(mTmpUidStatsArray) + / intermediates.txPackets; + } + long totalScanDuration = + intermediates.basicScanDuration + intermediates.batchedScanDuration; + if (totalScanDuration != 0) { + long scanDuration = mStatsLayout.getUidScanTime(mTmpUidStatsArray) + + mStatsLayout.getUidBatchedScanTime(mTmpUidStatsArray); + power += intermediates.scanPower * scanDuration / totalScanDuration; + } + } else { + long totalPackets = intermediates.rxPackets + intermediates.txPackets; + if (totalPackets != 0) { + long packets = mStatsLayout.getUidRxPackets(mTmpUidStatsArray) + + mStatsLayout.getUidTxPackets(mTmpUidStatsArray); + power += intermediates.activePower * packets / totalPackets; + } + + if (intermediates.basicScanDuration != 0) { + long scanDuration = mStatsLayout.getUidScanTime(mTmpUidStatsArray); + power += intermediates.basicScanPower * scanDuration + / intermediates.basicScanDuration; + } + + if (intermediates.batchedScanDuration != 0) { + long batchedScanDuration = mStatsLayout.getUidBatchedScanTime( + mTmpUidStatsArray); + power += intermediates.batchedScanPower * batchedScanDuration + / intermediates.batchedScanDuration; + } + } + mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power); + stats.setUidStats(uid, proportionalEstimate.stateValues, mTmpUidStatsArray); + + if (DEBUG) { + Slog.d(TAG, "UID: " + uid + + " states: " + Arrays.toString(proportionalEstimate.stateValues) + + " stats: " + Arrays.toString(mTmpUidStatsArray) + + " rx: " + mStatsLayout.getUidRxPackets(mTmpUidStatsArray) + + " rx-power: " + intermediates.rxPower + + " rx-packets: " + intermediates.rxPackets + + " tx: " + mStatsLayout.getUidTxPackets(mTmpUidStatsArray) + + " tx-power: " + intermediates.txPower + + " tx-packets: " + intermediates.txPackets + + " power: " + power); + } + } + } + + @Override + String deviceStatsToString(PowerStats.Descriptor descriptor, long[] stats) { + unpackPowerStatsDescriptor(descriptor); + if (mHasWifiPowerController) { + return "rx: " + mStatsLayout.getDeviceRxTime(stats) + + " tx: " + mStatsLayout.getDeviceTxTime(stats) + + " scan: " + mStatsLayout.getDeviceScanTime(stats) + + " idle: " + mStatsLayout.getDeviceIdleTime(stats) + + " power: " + mStatsLayout.getDevicePowerEstimate(stats); + } else { + return "active: " + mStatsLayout.getDeviceActiveTime(stats) + + " scan: " + mStatsLayout.getDeviceBasicScanTime(stats) + + " batched-scan: " + mStatsLayout.getDeviceBatchedScanTime(stats) + + " power: " + mStatsLayout.getDevicePowerEstimate(stats); + } + } + + @Override + String stateStatsToString(PowerStats.Descriptor descriptor, int key, long[] stats) { + // Unsupported for this power component + return null; + } + + @Override + String uidStatsToString(PowerStats.Descriptor descriptor, long[] stats) { + unpackPowerStatsDescriptor(descriptor); + return "rx: " + mStatsLayout.getUidRxPackets(stats) + + " tx: " + mStatsLayout.getUidTxPackets(stats) + + " scan: " + mStatsLayout.getUidScanTime(stats) + + " batched-scan: " + mStatsLayout.getUidBatchedScanTime(stats) + + " power: " + mStatsLayout.getUidPowerEstimate(stats); + } +} diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index f925b5fdb7e0..236a746d49a3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -77,6 +77,7 @@ import static android.content.pm.ActivityInfo.FLAG_NO_HISTORY; import static android.content.pm.ActivityInfo.FLAG_SHOW_FOR_ALL_USERS; import static android.content.pm.ActivityInfo.FLAG_STATE_NOT_NEEDED; import static android.content.pm.ActivityInfo.FLAG_TURN_SCREEN_ON; +import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION; import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED; import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; @@ -2119,9 +2120,19 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mCameraCompatControlEnabled = mWmService.mContext.getResources() .getBoolean(R.bool.config_isCameraCompatControlForStretchedIssuesEnabled); mResolveConfigHint = new TaskFragment.ConfigOverrideHint(); - mResolveConfigHint.mUseLegacyInsetsForStableBounds = - mWmService.mFlags.mInsetsDecoupledConfiguration - && !info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED); + if (mWmService.mFlags.mInsetsDecoupledConfiguration) { + // When the stable configuration is the default behavior, override for the legacy apps + // without forward override flag. + mResolveConfigHint.mUseOverrideInsetsForStableBounds = + !info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + && !info.isChangeEnabled( + OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION); + } else { + // When the stable configuration is not the default behavior, forward overriding the + // listed apps. + mResolveConfigHint.mUseOverrideInsetsForStableBounds = + info.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION); + } mTargetSdk = info.applicationInfo.targetSdkVersion; @@ -8466,7 +8477,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mCompatDisplayInsets = new CompatDisplayInsets( mDisplayContent, this, letterboxedContainerBounds, - mResolveConfigHint.mUseLegacyInsetsForStableBounds); + mResolveConfigHint.mUseOverrideInsetsForStableBounds); } private void clearSizeCompatModeAttributes() { @@ -8670,7 +8681,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) { rotation = mDisplayContent.getRotation(); } - if (!mResolveConfigHint.mUseLegacyInsetsForStableBounds + if (!mResolveConfigHint.mUseOverrideInsetsForStableBounds || getCompatDisplayInsets() != null || isFloating(parentWindowingMode) || parentAppBounds == null || parentAppBounds.isEmpty() || rotation == ROTATION_UNDEFINED) { @@ -8987,7 +8998,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mDisplayContent == null) { return true; } - if (!mResolveConfigHint.mUseLegacyInsetsForStableBounds) { + if (!mResolveConfigHint.mUseOverrideInsetsForStableBounds) { // No insets should be considered any more. return true; } @@ -9006,7 +9017,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final Task task = getTask(); task.calculateInsetFrames(outNonDecorBounds /* outNonDecorBounds */, outStableBounds /* outStableBounds */, parentBounds /* bounds */, di, - mResolveConfigHint.mUseLegacyInsetsForStableBounds); + mResolveConfigHint.mUseOverrideInsetsForStableBounds); final int orientationWithInsets = outStableBounds.height() >= outStableBounds.width() ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; // If orientation does not match the orientation with insets applied, then a @@ -9063,7 +9074,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A getResolvedOverrideConfiguration().windowConfiguration.getBounds(); final int stableBoundsOrientation = stableBounds.width() > stableBounds.height() ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; - final int parentOrientation = mResolveConfigHint.mUseLegacyInsetsForStableBounds + final int parentOrientation = mResolveConfigHint.mUseOverrideInsetsForStableBounds ? stableBoundsOrientation : newParentConfig.orientation; // If the activity requires a different orientation (either by override or activityInfo), @@ -9088,7 +9099,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } - final Rect parentAppBounds = mResolveConfigHint.mUseLegacyInsetsForStableBounds + final Rect parentAppBounds = mResolveConfigHint.mUseOverrideInsetsForStableBounds ? outNonDecorBounds : newParentConfig.windowConfiguration.getAppBounds(); // TODO(b/182268157): Explore using only one type of parentBoundsWithInsets, either app // bounds or stable bounds to unify aspect ratio logic. diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index a739e577bca3..b3c43bc33edd 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3736,6 +3736,14 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return false; } + // If the app is using auto-enter, and it explicitly requests entering PiP while pausing, + // return false immediately since auto-enter should take in place instead. + if (fromClient && r.isState(PAUSING) && params.isAutoEnterEnabled()) { + Slog.w(TAG, "Skip client enterPictureInPictureMode request while pausing," + + " auto-enter-pip is enabled"); + return false; + } + if (isPip2ExperimentEnabled()) { // If PiP2 flag is on and request to enter PiP comes in, // we request a direct transition TRANSIT_PIP from Shell to get the right entry bounds. diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index cb5ad910c651..da1581e404dc 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -905,13 +905,11 @@ final class LetterboxUiController { // Note that we check the task rather than the parent as with ActivityEmbedding the parent might // be a TaskFragment, and its windowing mode is always MULTI_WINDOW, even if the task is // actually fullscreen. - private boolean isDisplayFullScreenAndInPosture(DeviceStateController.DeviceState state, - boolean isTabletop) { + private boolean isDisplayFullScreenAndInPosture(boolean isTabletop) { Task task = mActivityRecord.getTask(); - return mActivityRecord.mDisplayContent != null - && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture(state, - isTabletop) - && task != null + return mActivityRecord.mDisplayContent != null && task != null + && mActivityRecord.mDisplayContent.getDisplayRotation().isDeviceInPosture( + DeviceStateController.DeviceState.HALF_FOLDED, isTabletop) && task.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; } @@ -939,16 +937,14 @@ final class LetterboxUiController { } private boolean isFullScreenAndBookModeEnabled() { - return isDisplayFullScreenAndInPosture( - DeviceStateController.DeviceState.HALF_FOLDED, false /* isTabletop */) + return isDisplayFullScreenAndInPosture(/* isTabletop */ false) && mLetterboxConfiguration.getIsAutomaticReachabilityInBookModeEnabled(); } float getVerticalPositionMultiplier(Configuration parentConfiguration) { // Don't check resolved configuration because it may not be updated yet during // configuration change. - boolean tabletopMode = isDisplayFullScreenAndInPosture( - DeviceStateController.DeviceState.HALF_FOLDED, true /* isTabletop */); + boolean tabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true); return isVerticalReachabilityEnabled(parentConfiguration) // Using the last global dynamic position to avoid "jumps" when moving // between apps or activities. @@ -981,16 +977,15 @@ final class LetterboxUiController { } private boolean shouldUseSplitScreenAspectRatio(@NonNull Configuration parentConfiguration) { - final boolean isBookMode = isDisplayFullScreenAndInPosture( - DeviceStateController.DeviceState.HALF_FOLDED, - /* isTabletop */ false); + final boolean isBookMode = isDisplayFullScreenAndInPosture(/* isTabletop */ false); final boolean isNotCenteredHorizontally = getHorizontalPositionMultiplier( parentConfiguration) != LETTERBOX_POSITION_MULTIPLIER_CENTER; - final boolean isTabletopMode = isDisplayFullScreenAndInPosture( - DeviceStateController.DeviceState.HALF_FOLDED, - /* isTabletop */ true); + final boolean isTabletopMode = isDisplayFullScreenAndInPosture(/* isTabletop */ true); + final boolean isLandscape = isFixedOrientationLandscape( + mActivityRecord.getOverrideOrientation()); + // Don't resize to split screen size when in book mode if letterbox position is centered - return ((isBookMode && isNotCenteredHorizontally) || isTabletopMode) + return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape) || isCameraCompatSplitScreenAspectRatioAllowed() && isCameraCompatTreatmentActive(); } @@ -1647,17 +1642,13 @@ final class LetterboxUiController { if (isHorizontalReachabilityEnabled()) { int letterboxPositionForHorizontalReachability = getLetterboxConfiguration() .getLetterboxPositionForHorizontalReachability( - isDisplayFullScreenAndInPosture( - DeviceStateController.DeviceState.HALF_FOLDED, - false /* isTabletop */)); + isDisplayFullScreenAndInPosture(/* isTabletop */ false)); positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition( letterboxPositionForHorizontalReachability); } else if (isVerticalReachabilityEnabled()) { int letterboxPositionForVerticalReachability = getLetterboxConfiguration() .getLetterboxPositionForVerticalReachability( - isDisplayFullScreenAndInPosture( - DeviceStateController.DeviceState.HALF_FOLDED, - true /* isTabletop */)); + isDisplayFullScreenAndInPosture(/* isTabletop */ true)); positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition( letterboxPositionForVerticalReachability); } diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 422cc696224b..622a80920526 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -174,6 +174,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> private static final int SET_SCREEN_BRIGHTNESS_OVERRIDE = 1; private static final int SET_USER_ACTIVITY_TIMEOUT = 2; private static final int MSG_SEND_SLEEP_TRANSITION = 3; + private static final int PINNED_TASK_ABORT_TIMEOUT = 1000; static final String TAG_TASKS = TAG + POSTFIX_TASKS; static final String TAG_STATES = TAG + POSTFIX_STATES; @@ -295,6 +296,9 @@ class RootWindowContainer extends WindowContainer<DisplayContent> }; + // TODO: b/335866033 Remove the abort PiP on timeout once PiP2 flag is on. + @Nullable private Runnable mMaybeAbortPipEnterRunnable = null; + private final FindTaskResult mTmpFindTaskResult = new FindTaskResult(); static class FindTaskResult implements Predicate<Task> { @@ -2272,8 +2276,67 @@ class RootWindowContainer extends WindowContainer<DisplayContent> resumeFocusedTasksTopActivities(); notifyActivityPipModeChanged(r.getTask(), r); + + if (!isPip2ExperimentEnabled()) { + // TODO: b/335866033 Remove the abort PiP on timeout once PiP2 flag is on. + // Set up a timeout callback to potentially abort PiP enter if in an inconsistent state. + scheduleTimeoutAbortPipEnter(rootTask); + } + } + + private void scheduleTimeoutAbortPipEnter(Task rootTask) { + if (mMaybeAbortPipEnterRunnable != null) { + // If there is an abort enter PiP check pending already remove it and abort + // immediately since we are trying to enter PiP in an inconsistent state + mHandler.removeCallbacks(mMaybeAbortPipEnterRunnable); + mMaybeAbortPipEnterRunnable.run(); + } + // Snapshot a throwable early on to display the callstack upon abort later on timeout. + final Throwable enterPipThrowable = new Throwable(); + // Set up a timeout to potentially roll back the task change to PINNED mode + // by aborting PiP. + mMaybeAbortPipEnterRunnable = new Runnable() { + @Override + public void run() { + synchronized (mService.mGlobalLock) { + if (mTransitionController.inTransition()) { + // If this task is a part an active transition aborting PiP might break + // it; so run the timeout callback directly once idle. + + final Runnable expectedMaybeAbortAtTimeout = mMaybeAbortPipEnterRunnable; + mTransitionController.mStateValidators.add(() -> { + // If a second PiP transition comes in, it runs the abort runnable for + // the first transition pre-emptively, so we need to avoid calling + // the same runnable twice when validating states. + if (expectedMaybeAbortAtTimeout != mMaybeAbortPipEnterRunnable) return; + mMaybeAbortPipEnterRunnable = null; + run(); + }); + return; + } else { + mMaybeAbortPipEnterRunnable = null; + } + mService.deferWindowLayout(); + final ActivityRecord top = rootTask.getTopMostActivity(); + final ActivityManager.RunningTaskInfo beforeTaskInfo = + rootTask.getTaskInfo(); + if (top != null && !top.inPinnedWindowingMode() + && rootTask.abortPipEnter(top)) { + Slog.wtf(TAG, "Enter PiP was aborted via a scheduled timeout" + + "task_state_before=" + beforeTaskInfo + + "task_state_after=" + rootTask.getTaskInfo(), + enterPipThrowable); + } + mService.continueWindowLayout(); + } + } + }; + mHandler.postDelayed(mMaybeAbortPipEnterRunnable, PINNED_TASK_ABORT_TIMEOUT); + Slog.d(TAG, "a delayed check for potentially aborting PiP if " + + "in a wrong state is scheduled."); } + /** * Notifies when an activity enters or leaves PIP mode. * @@ -2892,6 +2955,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent> mService.mH.post(mDestroyAllActivitiesRunnable); } + void removeAllMaybeAbortPipEnterRunnable() { + if (mMaybeAbortPipEnterRunnable == null) { + return; + } + mHandler.removeCallbacks(mMaybeAbortPipEnterRunnable); + mMaybeAbortPipEnterRunnable = null; + } + // Tries to put all activity tasks to sleep. Returns true if all tasks were // successfully put to sleep. boolean putTasksToSleep(boolean allowDelay, boolean shuttingDown) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9d3ffa95f744..f23a440eb0ed 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -4852,11 +4852,13 @@ class Task extends TaskFragment { /** * Abort an incomplete pip-entry. If left in this state, it will cover everything but remain * paused. If this is needed, there is a bug -- this should only be used for recovery. + * + * @return true if there is an inconsistency in the task and activity state. */ - void abortPipEnter(ActivityRecord top) { + boolean abortPipEnter(ActivityRecord top) { // an incomplete state has the task PINNED but the activity not. if (!inPinnedWindowingMode() || top.inPinnedWindowingMode() || !canMoveTaskToBack(this)) { - return; + return false; } final Transition transition = new Transition(TRANSIT_TO_BACK, 0 /* flags */, mTransitionController, mWmService.mSyncEngine); @@ -4878,6 +4880,7 @@ class Task extends TaskFragment { top.setWindowingMode(WINDOWING_MODE_UNDEFINED); top.mWaitForEnteringPinnedMode = false; } + return true; } void resumeNextFocusAfterReparent() { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 7a8bea0c726b..2b631f7a404e 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -2204,7 +2204,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { static class ConfigOverrideHint { @Nullable DisplayInfo mTmpOverrideDisplayInfo; @Nullable ActivityRecord.CompatDisplayInsets mTmpCompatInsets; - boolean mUseLegacyInsetsForStableBounds; + boolean mUseOverrideInsetsForStableBounds; } void computeConfigResourceOverrides(@NonNull Configuration inOutConfig, @@ -2237,11 +2237,11 @@ class TaskFragment extends WindowContainer<WindowContainer> { @NonNull Configuration parentConfig, @Nullable ConfigOverrideHint overrideHint) { DisplayInfo overrideDisplayInfo = null; ActivityRecord.CompatDisplayInsets compatInsets = null; - boolean useLegacyInsetsForStableBounds = false; + boolean useOverrideInsetsForStableBounds = false; if (overrideHint != null) { overrideDisplayInfo = overrideHint.mTmpOverrideDisplayInfo; compatInsets = overrideHint.mTmpCompatInsets; - useLegacyInsetsForStableBounds = overrideHint.mUseLegacyInsetsForStableBounds; + useOverrideInsetsForStableBounds = overrideHint.mUseOverrideInsetsForStableBounds; if (overrideDisplayInfo != null) { // Make sure the screen related configs can be computed by the provided // display info. @@ -2321,7 +2321,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { // The non decor inset are areas that could never be removed in Honeycomb. See // {@link WindowManagerPolicy#getNonDecorInsetsLw}. calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di, - useLegacyInsetsForStableBounds); + useOverrideInsetsForStableBounds); } else { // Apply the given non-decor and stable insets to calculate the corresponding bounds // for screen size of configuration. diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 407b5d517dd7..eeedec398be4 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -1044,6 +1044,13 @@ public abstract class WindowManagerInternal { int[] fromOrientations, int[] toOrientations); /** + * Set current screen capture session id that will be used during sensitive content protections. + * + * @param sessionId Session id for this screen capture protection + */ + public abstract void setBlockScreenCaptureForAppsSessionId(long sessionId); + + /** * Set whether screen capture should be disabled for all windows of a specific app windows based * on sensitive content protections. * diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 07e534cf4b4d..de7c0eb22321 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -44,6 +44,7 @@ import static android.os.Process.myPid; import static android.os.Process.myUid; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.permission.flags.Flags.sensitiveContentImprovements; +import static android.permission.flags.Flags.sensitiveContentMetricsBugfix; import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; @@ -113,6 +114,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WIND import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_MOVEMENT; import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR; import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS; +import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED; import static com.android.internal.util.LatencyTracker.ACTION_ROTATE_SCREEN; import static com.android.server.LockGuard.INDEX_WINDOW; import static com.android.server.LockGuard.installLock; @@ -340,6 +342,7 @@ import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.DumpUtils; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.LatencyTracker; import com.android.internal.view.WindowManagerPolicyThread; import com.android.server.AnimationThread; @@ -1110,6 +1113,9 @@ public class WindowManagerService extends IWindowManager.Stub SystemPerformanceHinter mSystemPerformanceHinter; @GuardedBy("mGlobalLock") + private long mSensitiveContentProtectionSessionId = 0; + + @GuardedBy("mGlobalLock") final SensitiveContentPackages mSensitiveContentPackages = new SensitiveContentPackages(); /** * UIDs for which a Toast has been shown to indicate @@ -4519,7 +4525,8 @@ public class WindowManagerService extends IWindowManager.Stub } } - int getDisplayUserRotation(int displayId) { + @Override + public int getDisplayUserRotation(int displayId) { synchronized (mGlobalLock) { final DisplayContent display = mRoot.getDisplayContent(displayId); if (display == null) { @@ -8739,6 +8746,16 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void setBlockScreenCaptureForAppsSessionId(long sessionId) { + synchronized (mGlobalLock) { + if (sensitiveContentMetricsBugfix() + && mSensitiveContentProtectionSessionId != sessionId) { + mSensitiveContentProtectionSessionId = sessionId; + } + } + } + + @Override public void addBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) { synchronized (mGlobalLock) { boolean modified = @@ -10202,6 +10219,15 @@ public class WindowManagerService extends IWindowManager.Stub Toast.LENGTH_SHORT) .show(); }); + // If blocked due to notification protection (null window token) log protection applied + if (sensitiveContentMetricsBugfix() + && mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(w.getOwningPackage(), uid, null)) { + FrameworkStatsLog.write( + SENSITIVE_NOTIFICATION_APP_PROTECTION_APPLIED, + mSensitiveContentProtectionSessionId, + uid); + } } } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index e3f06c27da77..1573d09364ea 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.Manifest.permission.START_TASKS_FROM_RECENTS; import static android.app.ActivityManager.isStartResultSuccessful; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; @@ -857,6 +858,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } final int childWindowingMode = c.getActivityWindowingMode(); + if (!ActivityTaskManagerService.isPip2ExperimentEnabled() + && tr.getWindowingMode() == WINDOWING_MODE_PINNED + && (childWindowingMode == WINDOWING_MODE_PINNED + || childWindowingMode == WINDOWING_MODE_UNDEFINED)) { + // If setActivityWindowingMode requested to match its pinned task's windowing mode, + // remove any inconsistency checking timeout callbacks for PiP. + Slog.d(TAG, "Task and activity windowing modes match, so remove any timeout " + + "abort PiP callbacks scheduled if needed; task_win_mode=" + + tr.getWindowingMode() + ", activity_win_mode=" + childWindowingMode); + mService.mRootWindowContainer.removeAllMaybeAbortPipEnterRunnable(); + } if (childWindowingMode > -1) { tr.forAllActivities(a -> { a.setWindowingMode(childWindowingMode); }); } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 9d8246d95180..d91a2115a884 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -631,6 +631,11 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio return; } + final WindowProcessController caller = mAtm.mProcessMap.getProcess(r.launchedFromPid); + if (caller != null && caller.mInstrumenting) { + return; + } + final long diff = launchTime - lastLaunchTime; if (diff < RAPID_ACTIVITY_LAUNCH_MS) { mRapidActivityLaunchCount++; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java index c4e2dc802104..b76279b095c7 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyCacheImpl.java @@ -32,7 +32,6 @@ import com.android.internal.annotations.GuardedBy; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; /** * Implementation of {@link DevicePolicyCache}, to which {@link DevicePolicyManagerService} pushes @@ -48,18 +47,11 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { private final Object mLock = new Object(); /** - * Indicates which user is screen capture disallowed on. Can be {@link UserHandle#USER_NULL}, - * {@link UserHandle#USER_ALL} or a concrete user ID. - */ - @GuardedBy("mLock") - private int mScreenCaptureDisallowedUser = UserHandle.USER_NULL; - - /** * Indicates if screen capture is disallowed on a specific user or all users if * it contains {@link UserHandle#USER_ALL}. */ @GuardedBy("mLock") - private Set<Integer> mScreenCaptureDisallowedUsers = new HashSet<>(); + private final Set<Integer> mScreenCaptureDisallowedUsers = new HashSet<>(); @GuardedBy("mLock") private final SparseIntArray mPasswordQuality = new SparseIntArray(); @@ -70,9 +62,8 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { @GuardedBy("mLock") private ArrayMap<String, String> mLauncherShortcutOverrides = new ArrayMap<>(); - /** Maps to {@code ActiveAdmin.mAdminCanGrantSensorsPermissions}. */ - private final AtomicBoolean mCanGrantSensorsPermissions = new AtomicBoolean(false); + private volatile boolean mCanGrantSensorsPermissions = false; @GuardedBy("mLock") private final SparseIntArray mContentProtectionPolicy = new SparseIntArray(); @@ -87,10 +78,6 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { @Override public boolean isScreenCaptureAllowed(int userHandle) { - return isScreenCaptureAllowedInPolicyEngine(userHandle); - } - - private boolean isScreenCaptureAllowedInPolicyEngine(int userHandle) { // This won't work if resolution mechanism is not strictest applies, but it's ok for now. synchronized (mLock) { return !mScreenCaptureDisallowedUsers.contains(userHandle) @@ -98,18 +85,6 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { } } - public int getScreenCaptureDisallowedUser() { - synchronized (mLock) { - return mScreenCaptureDisallowedUser; - } - } - - public void setScreenCaptureDisallowedUser(int userHandle) { - synchronized (mLock) { - mScreenCaptureDisallowedUser = userHandle; - } - } - public void setScreenCaptureDisallowedUser(int userHandle, boolean disallowed) { synchronized (mLock) { if (disallowed) { @@ -170,12 +145,12 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { @Override public boolean canAdminGrantSensorsPermissions() { - return mCanGrantSensorsPermissions.get(); + return mCanGrantSensorsPermissions; } /** Sets admin control over permission grants. */ public void setAdminCanGrantSensorsPermissions(boolean canGrant) { - mCanGrantSensorsPermissions.set(canGrant); + mCanGrantSensorsPermissions = canGrant; } @Override @@ -205,7 +180,7 @@ public class DevicePolicyCacheImpl extends DevicePolicyCache { pw.println("Password quality: " + mPasswordQuality); pw.println("Permission policy: " + mPermissionPolicy); pw.println("Content protection policy: " + mContentProtectionPolicy); - pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions.get()); + pw.println("Admin can grant sensors permission: " + mCanGrantSensorsPermissions); pw.print("Shortcuts overrides: "); pw.println(mLauncherShortcutOverrides); pw.decreaseIndent(); diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt index c0d988d0c46b..b0c7073d792b 100644 --- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt +++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt @@ -20,6 +20,7 @@ import android.app.AppOpsManager import android.companion.virtual.VirtualDeviceManager import android.os.Handler import android.os.UserHandle +import android.permission.PermissionManager import android.permission.flags.Flags import android.util.ArrayMap import android.util.ArraySet @@ -142,7 +143,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } } - override fun getNonDefaultUidModes(uid: Int, persistentDeviceId: String): SparseIntArray { + override fun getNonDefaultUidModes(uid: Int, deviceId: String): SparseIntArray { val appId = UserHandle.getAppId(uid) val userId = UserHandle.getUserId(uid) service.getState { @@ -150,7 +151,8 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS with(appIdPolicy) { opNameMapToOpSparseArray(getAppOpModes(appId, userId)?.map) } if (Flags.runtimePermissionAppopsMappingEnabled()) { runtimePermissionNameToAppOp.forEachIndexed { _, permissionName, appOpCode -> - val mode = getUidModeFromPermissionState(appId, userId, permissionName) + val mode = + getUidModeFromPermissionState(appId, userId, permissionName, deviceId) if (mode != AppOpsManager.opToDefaultMode(appOpCode)) { modes[appOpCode] = mode } @@ -165,7 +167,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS return opNameMapToOpSparseArray(getPackageModes(packageName, userId)) } - override fun getUidMode(uid: Int, persistentDeviceId: String, op: Int): Int { + override fun getUidMode(uid: Int, deviceId: String, op: Int): Int { val appId = UserHandle.getAppId(uid) val userId = UserHandle.getUserId(uid) val opName = AppOpsManager.opToPublicName(op) @@ -174,7 +176,9 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS return if (!Flags.runtimePermissionAppopsMappingEnabled() || permissionName == null) { service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } } } else { - service.getState { getUidModeFromPermissionState(appId, userId, permissionName) } + service.getState { + getUidModeFromPermissionState(appId, userId, permissionName, deviceId) + } } } @@ -187,15 +191,31 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS private fun GetStateScope.getUidModeFromPermissionState( appId: Int, userId: Int, - permissionName: String + permissionName: String, + deviceId: String ): Int { + val checkDevicePermissionFlags = + deviceId != VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT && + permissionName in PermissionManager.DEVICE_AWARE_PERMISSIONS val permissionFlags = - with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) } + if (checkDevicePermissionFlags) { + with(devicePermissionPolicy) { + getPermissionFlags(appId, deviceId, userId, permissionName) + } + } else { + with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) } + } val backgroundPermissionName = foregroundToBackgroundPermissionName[permissionName] val backgroundPermissionFlags = if (backgroundPermissionName != null) { - with(permissionPolicy) { - getPermissionFlags(appId, userId, backgroundPermissionName) + if (checkDevicePermissionFlags) { + with(devicePermissionPolicy) { + getPermissionFlags(appId, deviceId, userId, backgroundPermissionName) + } + } else { + with(permissionPolicy) { + getPermissionFlags(appId, userId, backgroundPermissionName) + } } } else { PermissionFlags.RUNTIME_GRANTED @@ -207,7 +227,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS val fullerPermissionName = PermissionService.getFullerPermission(permissionName) ?: return result - return getUidModeFromPermissionState(appId, userId, fullerPermissionName) + return getUidModeFromPermissionState(appId, userId, fullerPermissionName, deviceId) } private fun evaluateModeFromPermissionFlags( @@ -224,7 +244,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS MODE_IGNORED } - override fun setUidMode(uid: Int, persistentDeviceId: String, code: Int, mode: Int): Boolean { + override fun setUidMode(uid: Int, deviceId: String, code: Int, mode: Int): Boolean { if ( Flags.runtimePermissionAppopsMappingEnabled() && code in runtimeAppOpToPermissionNames ) { @@ -308,7 +328,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS // and we have our own persistence. } - override fun getForegroundOps(uid: Int, persistentDeviceId: String): SparseBooleanArray { + override fun getForegroundOps(uid: Int, deviceId: String): SparseBooleanArray { return SparseBooleanArray().apply { getUidModes(uid)?.forEachIndexed { _, op, mode -> if (mode == AppOpsManager.MODE_FOREGROUND) { @@ -317,7 +337,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } if (Flags.runtimePermissionAppopsMappingEnabled()) { foregroundableOps.forEachIndexed { _, op, _ -> - if (getUidMode(uid, persistentDeviceId, op) == AppOpsManager.MODE_FOREGROUND) { + if (getUidMode(uid, deviceId, op) == AppOpsManager.MODE_FOREGROUND) { this[op] = true } } @@ -501,7 +521,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS ) } } - ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode -> + ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode -> addPendingChangedModeIfNeeded( appId, userId, diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java index 6d89e80e5480..8db896b76f6c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java @@ -47,6 +47,7 @@ public class DisplayBrightnessStateTest { float sdrBrightness = 0.2f; boolean shouldUseAutoBrightness = true; boolean shouldUpdateScreenBrightnessSetting = true; + int brightnessAdjustmentFlag = 2; BrightnessReason brightnessReason = new BrightnessReason(); brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC); brightnessReason.setModifier(BrightnessReason.MODIFIER_DIMMED); @@ -56,6 +57,7 @@ public class DisplayBrightnessStateTest { .setBrightnessReason(brightnessReason) .setShouldUseAutoBrightness(shouldUseAutoBrightness) .setShouldUpdateScreenBrightnessSetting(shouldUpdateScreenBrightnessSetting) + .setBrightnessAdjustmentFlag(brightnessAdjustmentFlag) .build(); assertEquals(displayBrightnessState.getBrightness(), brightness, FLOAT_DELTA); @@ -105,7 +107,9 @@ public class DisplayBrightnessStateTest { .append("\n shouldUpdateScreenBrightnessSetting:") .append(displayBrightnessState.shouldUpdateScreenBrightnessSetting()) .append("\n mBrightnessEvent:") - .append(Objects.toString(displayBrightnessState.getBrightnessEvent(), "null")); + .append(Objects.toString(displayBrightnessState.getBrightnessEvent(), "null")) + .append("\n mBrightnessAdjustmentFlag:") + .append(displayBrightnessState.getBrightnessAdjustmentFlag()); return sb.toString(); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java index 1ae4099e41d4..13a1445bce76 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -113,7 +114,8 @@ public final class DisplayBrightnessControllerTest { when(mDisplayBrightnessStrategySelector.selectStrategy( any(StrategySelectionRequest.class))).thenReturn(displayBrightnessStrategy); mDisplayBrightnessController.updateBrightness(displayPowerRequest, targetDisplayState); - verify(displayBrightnessStrategy).updateBrightness(displayPowerRequest); + verify(displayBrightnessStrategy).updateBrightness( + eq(new StrategyExecutionRequest(displayPowerRequest, DEFAULT_BRIGHTNESS))); assertEquals(mDisplayBrightnessController.getCurrentDisplayBrightnessStrategy(), displayBrightnessStrategy); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java index 6e163ca69c13..54f22687bc75 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java @@ -41,8 +41,10 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.display.AutomaticBrightnessController; +import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessEvent; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.After; import org.junit.Before; @@ -295,15 +297,11 @@ public class AutomaticBrightnessStrategyTest { mContext.getContentResolver(), Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, UserHandle.USER_CURRENT), 0.0f); - assertEquals(BrightnessReason.ADJUSTMENT_AUTO, - mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags()); float invalidBrightness = -0.5f; mAutomaticBrightnessStrategy .adjustAutomaticBrightnessStateIfValid(invalidBrightness); assertEquals(autoBrightnessAdjustment, mAutomaticBrightnessStrategy.getAutoBrightnessAdjustment(), 0.0f); - assertEquals(0, - mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentReasonsFlags()); } @Test @@ -426,6 +424,80 @@ public class AutomaticBrightnessStrategyTest { assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessValid()); } + @Test + public void + updateBrightness_constructsDisplayBrightnessState_withAdjustmentAutoAdjustmentFlag() { + BrightnessEvent brightnessEvent = new BrightnessEvent(DISPLAY_ID); + mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy( + mContext, DISPLAY_ID, displayId -> brightnessEvent); + new AutomaticBrightnessStrategy(mContext, DISPLAY_ID); + mAutomaticBrightnessStrategy.setAutomaticBrightnessController( + mAutomaticBrightnessController); + float brightness = 0.4f; + BrightnessReason brightnessReason = new BrightnessReason(); + brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC); + when(mAutomaticBrightnessController.getAutomaticScreenBrightness(brightnessEvent)) + .thenReturn(brightness); + + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = + mock(DisplayManagerInternal.DisplayPowerRequest.class); + DisplayBrightnessState expectedDisplayBrightnessState = new DisplayBrightnessState.Builder() + .setBrightness(brightness) + .setSdrBrightness(brightness) + .setBrightnessReason(brightnessReason) + .setDisplayBrightnessStrategyName(mAutomaticBrightnessStrategy.getName()) + .setIsSlowChange(false) + .setBrightnessEvent(brightnessEvent) + .setBrightnessAdjustmentFlag(BrightnessReason.ADJUSTMENT_AUTO) + .setShouldUpdateScreenBrightnessSetting(true) + .build(); + DisplayBrightnessState actualDisplayBrightnessState = mAutomaticBrightnessStrategy + .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f)); + assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState); + } + + @Test + public void + updateBrightness_constructsDisplayBrightnessState_withAdjustmentTempAdjustmentFlag() { + BrightnessEvent brightnessEvent = new BrightnessEvent(DISPLAY_ID); + mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy( + mContext, DISPLAY_ID, displayId -> brightnessEvent); + new AutomaticBrightnessStrategy(mContext, DISPLAY_ID); + mAutomaticBrightnessStrategy.setAutomaticBrightnessController( + mAutomaticBrightnessController); + float brightness = 0.4f; + BrightnessReason brightnessReason = new BrightnessReason(); + brightnessReason.setReason(BrightnessReason.REASON_AUTOMATIC); + when(mAutomaticBrightnessController.getAutomaticScreenBrightness(brightnessEvent)) + .thenReturn(brightness); + + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = + mock(DisplayManagerInternal.DisplayPowerRequest.class); + float temporaryBrightness = 0.3f; + float autoBrightnessAdjustment = 0.1f; + mAutomaticBrightnessStrategy.setTemporaryAutoBrightnessAdjustment(temporaryBrightness); + mAutomaticBrightnessStrategy.accommodateUserBrightnessChanges(true, + brightness, DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT, + Display.STATE_ON, mock(BrightnessConfiguration.class), + AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED); + when(mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment()).thenReturn( + autoBrightnessAdjustment); + + DisplayBrightnessState expectedDisplayBrightnessState = new DisplayBrightnessState.Builder() + .setBrightness(brightness) + .setSdrBrightness(brightness) + .setBrightnessReason(brightnessReason) + .setDisplayBrightnessStrategyName(mAutomaticBrightnessStrategy.getName()) + .setIsSlowChange(false) + .setBrightnessEvent(brightnessEvent) + .setBrightnessAdjustmentFlag(BrightnessReason.ADJUSTMENT_AUTO_TEMP) + .setShouldUpdateScreenBrightnessSetting(true) + .build(); + DisplayBrightnessState actualDisplayBrightnessState = mAutomaticBrightnessStrategy + .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f)); + assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState); + } + private void setPendingAutoBrightnessAdjustment(float pendingAutoBrightnessAdjustment) { Settings.System.putFloat(mContext.getContentResolver(), Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingAutoBrightnessAdjustment); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java index c4346317a6ef..47f174684771 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java @@ -27,6 +27,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.Before; import org.junit.Test; @@ -58,7 +59,8 @@ public class BoostBrightnessStrategyTest { .setDisplayBrightnessStrategyName(mBoostBrightnessStrategy.getName()) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mBoostBrightnessStrategy.updateBrightness(displayPowerRequest); + mBoostBrightnessStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java index d60caf6efb7a..9246780a8516 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java @@ -25,6 +25,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.Before; import org.junit.Test; @@ -55,7 +56,8 @@ public class DozeBrightnessStrategyTest { .setDisplayBrightnessStrategyName(mDozeBrightnessModeStrategy.getName()) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mDozeBrightnessModeStrategy.updateBrightness(displayPowerRequest); + mDozeBrightnessModeStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java index d8569f75996f..682c9cc980d2 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java @@ -26,6 +26,7 @@ import androidx.test.filters.SmallTest; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.Before; import org.junit.Test; @@ -59,7 +60,8 @@ public class FollowerBrightnessStrategyTest { .setIsSlowChange(slowChange) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mFollowerBrightnessStrategy.updateBrightness(displayPowerRequest); + mFollowerBrightnessStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(expectedDisplayBrightnessState, updatedDisplayBrightnessState); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java index c2fa4eb88f52..ccf630948aac 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java @@ -28,6 +28,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import com.android.server.display.brightness.StrategySelectionNotifyRequest; import com.android.server.display.feature.DisplayManagerFlags; @@ -70,7 +71,8 @@ public class OffloadBrightnessStrategyTest { .setShouldUpdateScreenBrightnessSetting(true) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mOffloadBrightnessStrategy.updateBrightness(displayPowerRequest); + mOffloadBrightnessStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState); assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT, mOffloadBrightnessStrategy .getOffloadScreenBrightness(), 0.0f); diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java index 530245dacd8b..8e7b4636a392 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java @@ -26,6 +26,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.Before; import org.junit.Test; @@ -58,7 +59,8 @@ public class OverrideBrightnessStrategyTest { .setDisplayBrightnessStrategyName(mOverrideBrightnessStrategy.getName()) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mOverrideBrightnessStrategy.updateBrightness(displayPowerRequest); + mOverrideBrightnessStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java index 7147aa8d3701..e799d0e31165 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java @@ -26,6 +26,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.Before; import org.junit.Test; @@ -56,7 +57,8 @@ public final class ScreenOffBrightnessStrategyTest { .getName()) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mScreenOffBrightnessModeStrategy.updateBrightness(displayPowerRequest); + mScreenOffBrightnessModeStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java index 9830edbea645..aaada4115a56 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java @@ -26,6 +26,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; +import com.android.server.display.brightness.StrategyExecutionRequest; import org.junit.Before; import org.junit.Test; @@ -58,7 +59,8 @@ public class TemporaryBrightnessStrategyTest { .setDisplayBrightnessStrategyName(mTemporaryBrightnessStrategy.getName()) .build(); DisplayBrightnessState updatedDisplayBrightnessState = - mTemporaryBrightnessStrategy.updateBrightness(displayPowerRequest); + mTemporaryBrightnessStrategy.updateBrightness( + new StrategyExecutionRequest(displayPowerRequest, 0.2f)); assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState); } diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java new file mode 100644 index 000000000000..99968d5117c7 --- /dev/null +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamAccessibilityTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.dreams; + + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.service.dreams.utils.DreamAccessibility; +import android.text.TextUtils; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Collections; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DreamAccessibilityTest { + + @Mock + private View mView; + + @Mock + private Context mContext; + + @Mock + private Resources mResources; + + @Mock + private AccessibilityNodeInfo mAccessibilityNodeInfo; + + @Captor + private ArgumentCaptor<View.AccessibilityDelegate> mAccessibilityDelegateArgumentCaptor; + + private DreamAccessibility mDreamAccessibility; + private static final String CUSTOM_ACTION = "Custom Action"; + private static final String EXISTING_ACTION = "Existing Action"; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mDreamAccessibility = new DreamAccessibility(mContext, mView); + + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getString(R.string.dream_accessibility_action_click)) + .thenReturn(CUSTOM_ACTION); + } + /** + * Test to verify the configuration of accessibility actions within a view delegate. + */ + @Test + public void testConfigureAccessibilityActions() { + when(mAccessibilityNodeInfo.getActionList()).thenReturn(new ArrayList<>()); + + mDreamAccessibility.updateAccessibilityConfiguration(false); + + verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture()); + View.AccessibilityDelegate capturedDelegate = + mAccessibilityDelegateArgumentCaptor.getValue(); + + capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo); + + verify(mAccessibilityNodeInfo).addAction(argThat(action -> + action.getId() == AccessibilityNodeInfo.ACTION_CLICK + && TextUtils.equals(action.getLabel(), CUSTOM_ACTION))); + } + + /** + * Test to verify the configuration of accessibility actions within a view delegate, + * specifically checking the removal of an existing click action and addition + * of a new custom action. + */ + @Test + public void testConfigureAccessibilityActions_RemovesExistingClickAction() { + AccessibilityNodeInfo.AccessibilityAction existingAction = + new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, + EXISTING_ACTION); + when(mAccessibilityNodeInfo.getActionList()) + .thenReturn(Collections.singletonList(existingAction)); + + mDreamAccessibility.updateAccessibilityConfiguration(false); + + verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture()); + View.AccessibilityDelegate capturedDelegate = + mAccessibilityDelegateArgumentCaptor.getValue(); + + capturedDelegate.onInitializeAccessibilityNodeInfo(mView, mAccessibilityNodeInfo); + + verify(mAccessibilityNodeInfo).removeAction(existingAction); + verify(mAccessibilityNodeInfo).addAction(argThat(action -> + action.getId() == AccessibilityNodeInfo.ACTION_CLICK + && TextUtils.equals(action.getLabel(), CUSTOM_ACTION))); + + } + + /** + * Test to verify the removal of a custom accessibility action within a view delegate. + */ + @Test + public void testRemoveCustomAccessibilityAction() { + + AccessibilityNodeInfo.AccessibilityAction existingAction = + new AccessibilityNodeInfo.AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, + EXISTING_ACTION); + when(mAccessibilityNodeInfo.getActionList()) + .thenReturn(Collections.singletonList(existingAction)); + + mDreamAccessibility.updateAccessibilityConfiguration(false); + verify(mView).setAccessibilityDelegate(mAccessibilityDelegateArgumentCaptor.capture()); + View.AccessibilityDelegate capturedDelegate = + mAccessibilityDelegateArgumentCaptor.getValue(); + when(mView.getAccessibilityDelegate()).thenReturn(capturedDelegate); + clearInvocations(mView); + + mDreamAccessibility.updateAccessibilityConfiguration(true); + verify(mView).setAccessibilityDelegate(null); + } + + /** + * Test to verify the removal of custom accessibility action is not called if delegate is not + * set by the dreamService. + */ + @Test + public void testRemoveCustomAccessibility_DoesNotRemoveDelegateNotSetByDreamAccessibility() { + mDreamAccessibility.updateAccessibilityConfiguration(true); + verify(mView, never()).setAccessibilityDelegate(any()); + } +} + diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java index db704342ab61..88ab871529ee 100644 --- a/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java +++ b/services/tests/dreamservicetests/src/com/android/server/dreams/DreamControllerTest.java @@ -19,6 +19,8 @@ package com.android.server.dreams; import static android.os.PowerManager.USER_ACTIVITY_EVENT_OTHER; import static android.os.PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -270,6 +272,31 @@ public class DreamControllerTest { eq(USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS)); } + @Test + public void setDreamHasFocus_true_dreamHasFocus() { + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + + mDreamController.setDreamHasFocus(true); + assertTrue(mDreamController.dreamHasFocus()); + } + + @Test + public void setDreamHasFocus_false_dreamDoesNotHaveFocus() { + mDreamController.startDream(mToken, mDreamName, false /*isPreview*/, false /*doze*/, + 0 /*userId*/, null /*wakeLock*/, mOverlayName, "test" /*reason*/); + + mDreamController.setDreamHasFocus(false); + assertFalse(mDreamController.dreamHasFocus()); + } + + @Test + public void setDreamHasFocus_notDreaming_dreamDoesNotHaveFocus() { + mDreamController.setDreamHasFocus(true); + // Dream still doesn't have focus because it was never started. + assertFalse(mDreamController.dreamHasFocus()); + } + private ServiceConnection captureServiceConnection() { verify(mContext).bindServiceAsUser(any(), mServiceConnectionACaptor.capture(), anyInt(), any()); diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java index 124ae20aebe1..a20d935c50aa 100644 --- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java @@ -16,15 +16,18 @@ package com.android.server; +import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX; import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -34,6 +37,7 @@ import android.content.pm.PackageManagerInternal; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.os.Process; +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; @@ -309,6 +313,26 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { } @Test + @RequiresFlagsDisabled(FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX) + public void mediaProjectionOnStart_flagDisabled_neverSetBlockScreenCaptureForAppsSessionId() { + setupSensitiveNotification(); + + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); + + verify(mWindowManager, never()).setBlockScreenCaptureForAppsSessionId(anyLong()); + } + + @Test + @RequiresFlagsEnabled(FLAG_SENSITIVE_CONTENT_METRICS_BUGFIX) + public void mediaProjectionOnStart_setBlockScreenCaptureForAppsSessionId() { + setupSensitiveNotification(); + + mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); + + verify(mWindowManager).setBlockScreenCaptureForAppsSessionId(anyLong()); + } + + @Test public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() { ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); @@ -323,7 +347,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verifyZeroInteractions(mWindowManager); + verify(mWindowManager, never()).addBlockScreenCaptureForApps(any()); } @Test @@ -332,7 +356,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verifyZeroInteractions(mWindowManager); + verify(mWindowManager, never()).addBlockScreenCaptureForApps(any()); } @Test @@ -400,7 +424,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verifyZeroInteractions(mWindowManager); + verify(mWindowManager, never()).addBlockScreenCaptureForApps(any()); } @Test @@ -411,7 +435,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verifyZeroInteractions(mWindowManager); + verify(mWindowManager, never()).addBlockScreenCaptureForApps(any()); } @Test @@ -422,7 +446,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verifyZeroInteractions(mWindowManager); + verify(mWindowManager, never()).addBlockScreenCaptureForApps(any()); } @Test @@ -435,7 +459,7 @@ public class SensitiveContentProtectionManagerServiceNotificationTest { mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo()); - verifyZeroInteractions(mWindowManager); + verify(mWindowManager, never()).addBlockScreenCaptureForApps(any()); } @Test diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java new file mode 100644 index 000000000000..8b1d423abd21 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsCollectorTest.java @@ -0,0 +1,416 @@ +/* + * 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.power.stats; + +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.power.stats.EnergyConsumerType; +import android.net.NetworkStats; +import android.net.wifi.WifiManager; +import android.os.BatteryConsumer; +import android.os.BatteryStatsManager; +import android.os.Handler; +import android.os.WorkSource; +import android.os.connectivity.WifiActivityEnergyInfo; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.IndentingPrintWriter; +import android.util.SparseArray; + +import com.android.internal.os.Clock; +import com.android.internal.os.PowerStats; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class WifiPowerStatsCollectorTest { + private static final int APP_UID1 = 42; + private static final int APP_UID2 = 24; + private static final int APP_UID3 = 44; + private static final int ISOLATED_UID = 99123; + + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Rule(order = 1) + public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() + .setPowerStatsThrottlePeriodMillis(BatteryConsumer.POWER_COMPONENT_WIFI, 1000); + + private MockBatteryStatsImpl mBatteryStats; + + private final MockClock mClock = mStatsRule.getMockClock(); + + @Mock + private Context mContext; + @Mock + private PackageManager mPackageManager; + @Mock + private WifiManager mWifiManager; + @Mock + private PowerStatsCollector.ConsumedEnergyRetriever mConsumedEnergyRetriever; + @Mock + private Supplier<NetworkStats> mNetworkStatsSupplier; + @Mock + private PowerStatsUidResolver mPowerStatsUidResolver; + + private NetworkStats mNetworkStats; + private List<NetworkStats.Entry> mNetworkStatsEntries; + + private static class ScanTimes { + public long scanTimeMs; + public long batchScanTimeMs; + } + + private final SparseArray<ScanTimes> mScanTimes = new SparseArray<>(); + private long mWifiActiveDuration; + + private final WifiPowerStatsCollector.WifiStatsRetriever mWifiStatsRetriever = + new WifiPowerStatsCollector.WifiStatsRetriever() { + @Override + public void retrieveWifiScanTimes(Callback callback) { + for (int i = 0; i < mScanTimes.size(); i++) { + int uid = mScanTimes.keyAt(i); + ScanTimes scanTimes = mScanTimes.valueAt(i); + callback.onWifiScanTime(uid, scanTimes.scanTimeMs, scanTimes.batchScanTimeMs); + } + } + + @Override + public long getWifiActiveDuration() { + return mWifiActiveDuration; + } + }; + + private final List<PowerStats> mRecordedPowerStats = new ArrayList<>(); + + private WifiPowerStatsCollector.Injector mInjector = new WifiPowerStatsCollector.Injector() { + @Override + public Handler getHandler() { + return mStatsRule.getHandler(); + } + + @Override + public Clock getClock() { + return mStatsRule.getMockClock(); + } + + @Override + public PowerStatsUidResolver getUidResolver() { + return mPowerStatsUidResolver; + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + @Override + public PowerStatsCollector.ConsumedEnergyRetriever getConsumedEnergyRetriever() { + return mConsumedEnergyRetriever; + } + + @Override + public IntSupplier getVoltageSupplier() { + return () -> 3500; + } + + @Override + public Supplier<NetworkStats> getWifiNetworkStatsSupplier() { + return mNetworkStatsSupplier; + } + + @Override + public WifiPowerStatsCollector.WifiStatsRetriever getWifiStatsRetriever() { + return mWifiStatsRetriever; + } + + @Override + public WifiManager getWifiManager() { + return mWifiManager; + } + }; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true); + when(mPowerStatsUidResolver.mapUid(anyInt())).thenAnswer(invocation -> { + int uid = invocation.getArgument(0); + if (uid == ISOLATED_UID) { + return APP_UID2; + } else { + return uid; + } + }); + mBatteryStats = mStatsRule.getBatteryStats(); + } + + @SuppressWarnings("GuardedBy") + @Test + public void triggering() throws Throwable { + PowerStatsCollector collector = mBatteryStats.getPowerStatsCollector( + BatteryConsumer.POWER_COMPONENT_WIFI); + collector.addConsumer(mRecordedPowerStats::add); + + mBatteryStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_WIFI, true); + + mockWifiActivityInfo(1000, 2000, 3000, 600, 100); + + // This should trigger a sample collection to establish a baseline + mBatteryStats.onSystemReady(mContext); + + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + + mRecordedPowerStats.clear(); + mStatsRule.setTime(20000, 20000); + mBatteryStats.noteWifiOnLocked(mClock.realtime, mClock.uptime); + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + + mRecordedPowerStats.clear(); + mStatsRule.setTime(40000, 40000); + mBatteryStats.noteWifiOffLocked(mClock.realtime, mClock.uptime); + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + + mRecordedPowerStats.clear(); + mStatsRule.setTime(50000, 50000); + mBatteryStats.noteWifiRunningLocked(new WorkSource(APP_UID1), mClock.realtime, + mClock.uptime); + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + + mRecordedPowerStats.clear(); + mStatsRule.setTime(60000, 60000); + mBatteryStats.noteWifiStoppedLocked(new WorkSource(APP_UID1), mClock.realtime, + mClock.uptime); + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + + mRecordedPowerStats.clear(); + mStatsRule.setTime(70000, 70000); + mBatteryStats.noteWifiStateLocked(BatteryStatsManager.WIFI_STATE_ON_CONNECTED_STA, + "mywyfy", mClock.realtime); + mStatsRule.waitForBackgroundThread(); + assertThat(mRecordedPowerStats).hasSize(1); + } + + @Test + public void collectStats_powerReportingSupported() throws Throwable { + PowerStats powerStats = collectPowerStats(true); + assertThat(powerStats.durationMs).isEqualTo(7500); + + PowerStats.Descriptor descriptor = powerStats.descriptor; + WifiPowerStatsLayout layout = new WifiPowerStatsLayout(descriptor); + assertThat(layout.isPowerReportingSupported()).isTrue(); + assertThat(layout.getDeviceRxTime(powerStats.stats)).isEqualTo(6000); + assertThat(layout.getDeviceTxTime(powerStats.stats)).isEqualTo(1000); + assertThat(layout.getDeviceScanTime(powerStats.stats)).isEqualTo(200); + assertThat(layout.getDeviceIdleTime(powerStats.stats)).isEqualTo(300); + assertThat(layout.getConsumedEnergy(powerStats.stats, 0)) + .isEqualTo((64321 - 10000) * 1000 / 3500); + + verifyUidStats(powerStats); + } + + @Test + public void collectStats_powerReportingUnsupported() { + PowerStats powerStats = collectPowerStats(false); + assertThat(powerStats.durationMs).isEqualTo(13200); + + PowerStats.Descriptor descriptor = powerStats.descriptor; + WifiPowerStatsLayout layout = new WifiPowerStatsLayout(descriptor); + assertThat(layout.isPowerReportingSupported()).isFalse(); + assertThat(layout.getDeviceActiveTime(powerStats.stats)).isEqualTo(7500); + assertThat(layout.getDeviceBasicScanTime(powerStats.stats)).isEqualTo(234 + 100 + 300); + assertThat(layout.getDeviceBatchedScanTime(powerStats.stats)).isEqualTo(345 + 200 + 400); + assertThat(layout.getConsumedEnergy(powerStats.stats, 0)) + .isEqualTo((64321 - 10000) * 1000 / 3500); + + verifyUidStats(powerStats); + } + + private void verifyUidStats(PowerStats powerStats) { + WifiPowerStatsLayout layout = new WifiPowerStatsLayout(powerStats.descriptor); + assertThat(powerStats.uidStats.size()).isEqualTo(2); + long[] actual1 = powerStats.uidStats.get(APP_UID1); + assertThat(layout.getUidRxBytes(actual1)).isEqualTo(1000); + assertThat(layout.getUidTxBytes(actual1)).isEqualTo(2000); + assertThat(layout.getUidRxPackets(actual1)).isEqualTo(100); + assertThat(layout.getUidTxPackets(actual1)).isEqualTo(200); + assertThat(layout.getUidScanTime(actual1)).isEqualTo(234); + assertThat(layout.getUidBatchedScanTime(actual1)).isEqualTo(345); + + // Combines APP_UID2 and ISOLATED_UID + long[] actual2 = powerStats.uidStats.get(APP_UID2); + assertThat(layout.getUidRxBytes(actual2)).isEqualTo(6000); + assertThat(layout.getUidTxBytes(actual2)).isEqualTo(3000); + assertThat(layout.getUidRxPackets(actual2)).isEqualTo(60); + assertThat(layout.getUidTxPackets(actual2)).isEqualTo(30); + assertThat(layout.getUidScanTime(actual2)).isEqualTo(100 + 300); + assertThat(layout.getUidBatchedScanTime(actual2)).isEqualTo(200 + 400); + + assertThat(powerStats.uidStats.get(ISOLATED_UID)).isNull(); + assertThat(powerStats.uidStats.get(APP_UID3)).isNull(); + } + + @Test + public void dump() throws Throwable { + PowerStats powerStats = collectPowerStats(true); + StringWriter sw = new StringWriter(); + IndentingPrintWriter pw = new IndentingPrintWriter(sw); + powerStats.dump(pw); + pw.flush(); + String dump = sw.toString(); + assertThat(dump).contains("duration=7500"); + assertThat(dump).contains( + "stats=[6000, 1000, 300, 200, 634, 945, " + ((64321 - 10000) * 1000 / 3500) + + ", 0, 0]"); + assertThat(dump).contains("UID 24: [6000, 3000, 60, 30, 400, 600, 0]"); + assertThat(dump).contains("UID 42: [1000, 2000, 100, 200, 234, 345, 0]"); + } + + private PowerStats collectPowerStats(boolean hasPowerReporting) { + when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(hasPowerReporting); + + WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, 0); + collector.setEnabled(true); + + when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.WIFI)) + .thenReturn(new int[]{777}); + + if (hasPowerReporting) { + mockWifiActivityInfo(1000, 600, 100, 2000, 3000); + } else { + mWifiActiveDuration = 5700; + } + mockNetworkStats(1000); + mockNetworkStatsEntry(APP_UID1, 4321, 321, 1234, 23); + mockNetworkStatsEntry(APP_UID2, 4000, 40, 2000, 20); + mockNetworkStatsEntry(ISOLATED_UID, 2000, 20, 1000, 10); + mockNetworkStatsEntry(APP_UID3, 314, 281, 314, 281); + mockWifiScanTimes(APP_UID1, 1000, 2000); + mockWifiScanTimes(APP_UID2, 3000, 4000); + mockWifiScanTimes(ISOLATED_UID, 5000, 6000); + + when(mConsumedEnergyRetriever.getConsumedEnergyUws(eq(new int[]{777}))) + .thenReturn(new long[]{10000}); + + collector.collectStats(); + + if (hasPowerReporting) { + mockWifiActivityInfo(1100, 6600, 1100, 2200, 3300); + } else { + mWifiActiveDuration = 13200; + } + mockNetworkStats(1100); + mockNetworkStatsEntry(APP_UID1, 5321, 421, 3234, 223); + mockNetworkStatsEntry(APP_UID2, 8000, 80, 4000, 40); + mockNetworkStatsEntry(ISOLATED_UID, 4000, 40, 2000, 20); + mockNetworkStatsEntry(APP_UID3, 314, 281, 314, 281); // Unchanged + mockWifiScanTimes(APP_UID1, 1234, 2345); + mockWifiScanTimes(APP_UID2, 3100, 4200); + mockWifiScanTimes(ISOLATED_UID, 5300, 6400); + + when(mConsumedEnergyRetriever.getConsumedEnergyUws(eq(new int[]{777}))) + .thenReturn(new long[]{64321}); + + mStatsRule.setTime(20000, 20000); + return collector.collectStats(); + } + + private void mockWifiActivityInfo(long timestamp, long rxTimeMs, long txTimeMs, int scanTimeMs, + int idleTimeMs) { + int stackState = 0; + WifiActivityEnergyInfo info = new WifiActivityEnergyInfo(timestamp, stackState, txTimeMs, + rxTimeMs, scanTimeMs, idleTimeMs); + doAnswer(invocation -> { + WifiManager.OnWifiActivityEnergyInfoListener listener = invocation.getArgument(1); + listener.onWifiActivityEnergyInfo(info); + return null; + }).when(mWifiManager).getWifiActivityEnergyInfoAsync(any(), any()); + } + + private void mockNetworkStats(long elapsedRealtime) { + if (RavenwoodRule.isOnRavenwood()) { + mNetworkStats = mock(NetworkStats.class); + ArrayList<NetworkStats.Entry> networkStatsEntries = new ArrayList<>(); + when(mNetworkStats.iterator()).thenAnswer(inv -> networkStatsEntries.iterator()); + mNetworkStatsEntries = networkStatsEntries; + } else { + mNetworkStats = new NetworkStats(elapsedRealtime, 1); + } + when(mNetworkStatsSupplier.get()).thenReturn(mNetworkStats); + } + + private void mockNetworkStatsEntry(int uid, long rxBytes, long rxPackets, long txBytes, + long txPackets) { + if (RavenwoodRule.isOnRavenwood()) { + NetworkStats.Entry entry = mock(NetworkStats.Entry.class); + when(entry.getUid()).thenReturn(uid); + when(entry.getMetered()).thenReturn(METERED_NO); + when(entry.getRoaming()).thenReturn(ROAMING_NO); + when(entry.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO); + when(entry.getRxBytes()).thenReturn(rxBytes); + when(entry.getRxPackets()).thenReturn(rxPackets); + when(entry.getTxBytes()).thenReturn(txBytes); + when(entry.getTxPackets()).thenReturn(txPackets); + when(entry.getOperations()).thenReturn(100L); + mNetworkStatsEntries.add(entry); + } else { + mNetworkStats = mNetworkStats + .addEntry(new NetworkStats.Entry("wifi", uid, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets, + txBytes, txPackets, 100)); + reset(mNetworkStatsSupplier); + when(mNetworkStatsSupplier.get()).thenReturn(mNetworkStats); + } + } + + private void mockWifiScanTimes(int uid, long scanTimeMs, long batchScanTimeMs) { + ScanTimes scanTimes = new ScanTimes(); + scanTimes.scanTimeMs = scanTimeMs; + scanTimes.batchScanTimeMs = batchScanTimeMs; + mScanTimes.put(uid, scanTimes); + } +} diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java new file mode 100644 index 000000000000..257a1a67f7b0 --- /dev/null +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java @@ -0,0 +1,592 @@ +/* + * 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.power.stats; + +import static android.net.NetworkStats.DEFAULT_NETWORK_NO; +import static android.net.NetworkStats.METERED_NO; +import static android.net.NetworkStats.ROAMING_NO; +import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND; +import static android.os.BatteryConsumer.PROCESS_STATE_CACHED; +import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND; +import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE; + +import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_OTHER; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE; +import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.power.stats.EnergyConsumerType; +import android.net.NetworkStats; +import android.net.wifi.WifiManager; +import android.os.BatteryConsumer; +import android.os.Handler; +import android.os.Process; +import android.os.connectivity.WifiActivityEnergyInfo; +import android.platform.test.ravenwood.RavenwoodRule; +import android.util.SparseArray; + +import com.android.internal.os.Clock; +import com.android.internal.os.PowerProfile; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +public class WifiPowerStatsProcessorTest { + @Rule(order = 0) + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + private static final double PRECISION = 0.00001; + private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42; + private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101; + private static final int WIFI_ENERGY_CONSUMER_ID = 1; + private static final int VOLTAGE_MV = 3500; + + @Rule(order = 1) + public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule() + .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE, 360.0) + .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX, 480.0) + .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX, 720.0) + .setAveragePower(PowerProfile.POWER_WIFI_ACTIVE, 360.0) + .setAveragePower(PowerProfile.POWER_WIFI_SCAN, 480.0) + .setAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, 720.0) + .initMeasuredEnergyStatsLocked(); + + @Mock + private Context mContext; + @Mock + private PowerStatsUidResolver mPowerStatsUidResolver; + @Mock + private PackageManager mPackageManager; + @Mock + private PowerStatsCollector.ConsumedEnergyRetriever mConsumedEnergyRetriever; + @Mock + private Supplier<NetworkStats> mNetworkStatsSupplier; + @Mock + private WifiManager mWifiManager; + + private static class ScanTimes { + public long scanTimeMs; + public long batchScanTimeMs; + } + + private final SparseArray<ScanTimes> mScanTimes = new SparseArray<>(); + private long mWifiActiveDuration; + + private final WifiPowerStatsCollector.WifiStatsRetriever mWifiStatsRetriever = + new WifiPowerStatsCollector.WifiStatsRetriever() { + @Override + public void retrieveWifiScanTimes(Callback callback) { + for (int i = 0; i < mScanTimes.size(); i++) { + int uid = mScanTimes.keyAt(i); + ScanTimes scanTimes = mScanTimes.valueAt(i); + callback.onWifiScanTime(uid, scanTimes.scanTimeMs, scanTimes.batchScanTimeMs); + } + } + + @Override + public long getWifiActiveDuration() { + return mWifiActiveDuration; + } + }; + + private final WifiPowerStatsCollector.Injector mInjector = + new WifiPowerStatsCollector.Injector() { + @Override + public Handler getHandler() { + return mStatsRule.getHandler(); + } + + @Override + public Clock getClock() { + return mStatsRule.getMockClock(); + } + + @Override + public PowerStatsUidResolver getUidResolver() { + return mPowerStatsUidResolver; + } + + @Override + public PackageManager getPackageManager() { + return mPackageManager; + } + + @Override + public PowerStatsCollector.ConsumedEnergyRetriever getConsumedEnergyRetriever() { + return mConsumedEnergyRetriever; + } + + @Override + public IntSupplier getVoltageSupplier() { + return () -> VOLTAGE_MV; + } + + @Override + public Supplier<NetworkStats> getWifiNetworkStatsSupplier() { + return mNetworkStatsSupplier; + } + + @Override + public WifiManager getWifiManager() { + return mWifiManager; + } + + @Override + public WifiPowerStatsCollector.WifiStatsRetriever getWifiStatsRetriever() { + return mWifiStatsRetriever; + } + }; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI)).thenReturn(true); + when(mPowerStatsUidResolver.mapUid(anyInt())) + .thenAnswer(invocation -> invocation.getArgument(0)); + } + + @Test + public void powerProfileModel_powerController() { + when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(true); + + // No power monitoring hardware + when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.WIFI)) + .thenReturn(new int[0]); + + WifiPowerStatsProcessor processor = + new WifiPowerStatsProcessor(mStatsRule.getPowerProfile()); + + PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor); + + WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, 0); + collector.setEnabled(true); + + // Initial empty WifiActivityEnergyInfo. + mockWifiActivityEnergyInfo(new WifiActivityEnergyInfo(0L, + WifiActivityEnergyInfo.STACK_STATE_INVALID, 0L, 0L, 0L, 0L)); + + // Establish a baseline + aggregatedStats.addPowerStats(collector.collectStats(), 0); + + // Turn the screen off after 2.5 seconds + aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, + 5000); + + // Note application network activity + NetworkStats networkStats = mockNetworkStats(10000, 1, + mockNetworkStatsEntry("wifi", APP_UID1, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 10000, 1500, 20000, 300, 100), + mockNetworkStatsEntry("wifi", APP_UID2, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 5000, 500, 3000, 100, 111)); + when(mNetworkStatsSupplier.get()).thenReturn(networkStats); + + mockWifiScanTimes(APP_UID1, 300, 400); + mockWifiScanTimes(APP_UID2, 100, 200); + + mockWifiActivityEnergyInfo(new WifiActivityEnergyInfo(10000, + WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 2000, 3000, 100, 600)); + + mStatsRule.setTime(10_000, 10_000); + + aggregatedStats.addPowerStats(collector.collectStats(), 10_000); + + processor.finish(aggregatedStats); + + WifiPowerStatsLayout statsLayout = + new WifiPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor()); + + // RX power = 'rx-duration * PowerProfile[wifi.controller.rx]` + // RX power = 3000 * 480 = 1440000 mA-ms = 0.4 mAh + // TX power = 'tx-duration * PowerProfile[wifi.controller.tx]` + // TX power = 2000 * 720 = 1440000 mA-ms = 0.4 mAh + // Scan power = 'scan-duration * PowerProfile[wifi.scan]` + // Scan power = 100 * 480 = 48000 mA-ms = 0.013333 mAh + // Idle power = 'idle-duration * PowerProfile[wifi.idle]` + // Idle power = 600 * 360 = 216000 mA-ms = 0.06 mAh + // Total power = RX + TX + Scan + Idle = 0.873333 + // Screen-on - 25% + // Screen-off - 75% + double expectedPower = 0.873333; + long[] deviceStats = new long[aggregatedStats.getPowerStatsDescriptor().statsArrayLength]; + aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(expectedPower * 0.25); + + aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(expectedPower * 0.75); + + // UID1 = + // (1500 / 2000) * 0.4 // rx + // + (300 / 400) * 0.4 // tx + // + (700 / 1000) * 0.013333 // scan (basic + batched) + // = 0.609333 mAh + double expectedPower1 = 0.609333; + long[] uidStats = new long[aggregatedStats.getPowerStatsDescriptor().uidStatsArrayLength]; + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.5); + + // UID2 = + // (500 / 2000) * 0.4 // rx + // + (100 / 400) * 0.4 // tx + // + (300 / 1000) * 0.013333 // scan (basic + batched) + // = 0.204 mAh + double expectedPower2 = 0.204; + aggregatedStats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 0.75); + } + + @Test + public void consumedEnergyModel_powerController() { + when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(true); + + // PowerStats hardware is available + when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.WIFI)) + .thenReturn(new int[] {WIFI_ENERGY_CONSUMER_ID}); + + WifiPowerStatsProcessor processor = + new WifiPowerStatsProcessor(mStatsRule.getPowerProfile()); + + PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor); + + WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, 0); + collector.setEnabled(true); + + // Initial empty WifiActivityEnergyInfo. + mockWifiActivityEnergyInfo(new WifiActivityEnergyInfo(0L, + WifiActivityEnergyInfo.STACK_STATE_INVALID, 0L, 0L, 0L, 0L)); + + when(mConsumedEnergyRetriever.getConsumedEnergyUws( + new int[]{WIFI_ENERGY_CONSUMER_ID})) + .thenReturn(new long[]{0}); + + // Establish a baseline + aggregatedStats.addPowerStats(collector.collectStats(), 0); + + // Turn the screen off after 2.5 seconds + aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, + 5000); + + // Note application network activity + NetworkStats networkStats = mockNetworkStats(10000, 1, + mockNetworkStatsEntry("wifi", APP_UID1, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 10000, 1500, 20000, 300, 100), + mockNetworkStatsEntry("wifi", APP_UID2, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 5000, 500, 3000, 100, 111)); + when(mNetworkStatsSupplier.get()).thenReturn(networkStats); + + mockWifiScanTimes(APP_UID1, 300, 400); + mockWifiScanTimes(APP_UID2, 100, 200); + + mockWifiActivityEnergyInfo(new WifiActivityEnergyInfo(10000, + WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 2000, 3000, 100, 600)); + + mStatsRule.setTime(10_000, 10_000); + + // 10 mAh represented as microWattSeconds + long energyUws = 10 * 3600 * VOLTAGE_MV; + when(mConsumedEnergyRetriever.getConsumedEnergyUws( + new int[]{WIFI_ENERGY_CONSUMER_ID})).thenReturn(new long[]{energyUws}); + + aggregatedStats.addPowerStats(collector.collectStats(), 10_000); + + processor.finish(aggregatedStats); + + WifiPowerStatsLayout statsLayout = + new WifiPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor()); + + // All estimates are computed as in the #powerProfileModel_powerController test, + // except they are all scaled by the same ratio to ensure that the total estimated + // energy is equal to the measured energy + double expectedPower = 10; + long[] deviceStats = new long[aggregatedStats.getPowerStatsDescriptor().statsArrayLength]; + aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(expectedPower * 0.25); + + aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(expectedPower * 0.75); + + // UID1 + // 0.609333 // power profile model estimate + // 0.873333 // power profile model estimate for total power + // 10 // total consumed energy + // = 0.609333 * (10 / 0.873333) = 6.9771 + double expectedPower1 = 6.9771; + long[] uidStats = new long[aggregatedStats.getPowerStatsDescriptor().uidStatsArrayLength]; + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.5); + + // UID2 + // 0.204 // power profile model estimate + // 0.873333 // power profile model estimate for total power + // 10 // total consumed energy + // = 0.204 * (10 / 0.873333) = 2.33588 + double expectedPower2 = 2.33588; + aggregatedStats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 0.75); + } + + @Test + public void powerProfileModel_noPowerController() { + when(mWifiManager.isEnhancedPowerReportingSupported()).thenReturn(false); + + // No power monitoring hardware + when(mConsumedEnergyRetriever.getEnergyConsumerIds(EnergyConsumerType.WIFI)) + .thenReturn(new int[0]); + + WifiPowerStatsProcessor processor = + new WifiPowerStatsProcessor(mStatsRule.getPowerProfile()); + + PowerComponentAggregatedPowerStats aggregatedStats = createAggregatedPowerStats(processor); + + WifiPowerStatsCollector collector = new WifiPowerStatsCollector(mInjector, 0); + collector.setEnabled(true); + + // Establish a baseline + aggregatedStats.addPowerStats(collector.collectStats(), 0); + + // Turn the screen off after 2.5 seconds + aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, + 5000); + + // Note application network activity + NetworkStats networkStats = mockNetworkStats(10000, 1, + mockNetworkStatsEntry("wifi", APP_UID1, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 10000, 1500, 20000, 300, 100), + mockNetworkStatsEntry("wifi", APP_UID2, 0, 0, + METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 5000, 500, 3000, 100, 111)); + when(mNetworkStatsSupplier.get()).thenReturn(networkStats); + + mScanTimes.clear(); + mWifiActiveDuration = 8000; + mockWifiScanTimes(APP_UID1, 300, 400); + mockWifiScanTimes(APP_UID2, 100, 200); + + mStatsRule.setTime(10_000, 10_000); + + aggregatedStats.addPowerStats(collector.collectStats(), 10_000); + + processor.finish(aggregatedStats); + + WifiPowerStatsLayout statsLayout = + new WifiPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor()); + + // Total active power = 'active-duration * PowerProfile[wifi.on]` + // active = 8000 * 360 = 2880000 mA-ms = 0.8 mAh + // UID1 rxPackets + txPackets = 1800 + // UID2 rxPackets + txPackets = 600 + // Total rx+tx packets = 2400 + // Total scan power = `scan-duration * PowerProfile[wifi.scan]` + // scan = (100 + 300) * 480 = 192000 mA-ms = 0.05333 mAh + // Total batch scan power = `(200 + 400) * PowerProfile[wifi.batchedscan]` + // bscan = (200 + 400) * 720 = 432000 mA-ms = 0.12 mAh + // + // Expected power = active + scan + bscan = 0.97333 + double expectedPower = 0.97333; + long[] deviceStats = new long[aggregatedStats.getPowerStatsDescriptor().statsArrayLength]; + aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(expectedPower * 0.25); + + aggregatedStats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER)); + assertThat(statsLayout.getDevicePowerEstimate(deviceStats)) + .isWithin(PRECISION).of(expectedPower * 0.75); + + // UID1 = + // (1800 / 2400) * 0.8 // active + // + (300 / 400) * 0.05333 // scan + // + (400 / 600) * 0.12 // batched scan + // = 0.72 mAh + double expectedPower1 = 0.72; + long[] uidStats = new long[aggregatedStats.getPowerStatsDescriptor().uidStatsArrayLength]; + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID1, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower1 * 0.5); + + // UID2 = + // (600 / 2400) * 0.8 // active + // + (100 / 400) * 0.05333 // scan + // + (200 / 600) * 0.12 // batched scan + // = 0.253333 mAh + double expectedPower2 = 0.25333; + aggregatedStats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 0.25); + + aggregatedStats.getUidStats(uidStats, APP_UID2, + states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED)); + assertThat(statsLayout.getUidPowerEstimate(uidStats)) + .isWithin(PRECISION).of(expectedPower2 * 0.75); + } + + private static PowerComponentAggregatedPowerStats createAggregatedPowerStats( + WifiPowerStatsProcessor processor) { + AggregatedPowerStatsConfig.PowerComponent config = + new AggregatedPowerStatsConfig.PowerComponent(BatteryConsumer.POWER_COMPONENT_WIFI) + .trackDeviceStates(STATE_POWER, STATE_SCREEN) + .trackUidStates(STATE_POWER, STATE_SCREEN, STATE_PROCESS_STATE) + .setProcessor(processor); + + PowerComponentAggregatedPowerStats aggregatedStats = + new PowerComponentAggregatedPowerStats( + new AggregatedPowerStats(mock(AggregatedPowerStatsConfig.class)), config); + + aggregatedStats.setState(STATE_POWER, POWER_STATE_OTHER, 0); + aggregatedStats.setState(STATE_SCREEN, SCREEN_STATE_ON, 0); + aggregatedStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0); + aggregatedStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0); + + return aggregatedStats; + } + + private int[] states(int... states) { + return states; + } + + private void mockWifiActivityEnergyInfo(WifiActivityEnergyInfo waei) { + doAnswer(invocation -> { + WifiManager.OnWifiActivityEnergyInfoListener + listener = invocation.getArgument(1); + listener.onWifiActivityEnergyInfo(waei); + return null; + }).when(mWifiManager).getWifiActivityEnergyInfoAsync(any(), any()); + } + + private NetworkStats mockNetworkStats(int elapsedTime, int initialSize, + NetworkStats.Entry... entries) { + NetworkStats stats; + if (RavenwoodRule.isOnRavenwood()) { + stats = mock(NetworkStats.class); + when(stats.iterator()).thenAnswer(inv -> List.of(entries).iterator()); + } else { + stats = new NetworkStats(elapsedTime, initialSize); + for (NetworkStats.Entry entry : entries) { + stats = stats.addEntry(entry); + } + } + return stats; + } + + private static NetworkStats.Entry mockNetworkStatsEntry(@Nullable String iface, int uid, + int set, int tag, int metered, int roaming, int defaultNetwork, long rxBytes, + long rxPackets, long txBytes, long txPackets, long operations) { + if (RavenwoodRule.isOnRavenwood()) { + NetworkStats.Entry entry = mock(NetworkStats.Entry.class); + when(entry.getUid()).thenReturn(uid); + when(entry.getMetered()).thenReturn(metered); + when(entry.getRoaming()).thenReturn(roaming); + when(entry.getDefaultNetwork()).thenReturn(defaultNetwork); + when(entry.getRxBytes()).thenReturn(rxBytes); + when(entry.getRxPackets()).thenReturn(rxPackets); + when(entry.getTxBytes()).thenReturn(txBytes); + when(entry.getTxPackets()).thenReturn(txPackets); + when(entry.getOperations()).thenReturn(operations); + return entry; + } else { + return new NetworkStats.Entry(iface, uid, set, tag, metered, + roaming, defaultNetwork, rxBytes, rxPackets, txBytes, txPackets, operations); + } + } + + private void mockWifiScanTimes(int uid, long scanTimeMs, long batchScanTimeMs) { + ScanTimes scanTimes = new ScanTimes(); + scanTimes.scanTimeMs = scanTimeMs; + scanTimes.batchScanTimeMs = batchScanTimeMs; + mScanTimes.put(uid, scanTimes); + } +} diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java index b487dc69ec6e..c970a3e34d12 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java @@ -16,7 +16,8 @@ package com.android.server.appop; -import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static com.google.common.truth.Truth.assertThat; @@ -31,6 +32,7 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.Manifest; import android.app.AppOpsManager; import android.app.AppOpsManager.OnOpActiveChangedListener; import android.companion.virtual.VirtualDeviceManager; @@ -38,7 +40,8 @@ import android.companion.virtual.VirtualDeviceParams; import android.content.AttributionSource; import android.content.Context; import android.os.Process; -import android.virtualdevice.cts.common.FakeAssociationRule; +import android.permission.PermissionManager; +import android.virtualdevice.cts.common.VirtualDeviceRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -49,7 +52,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; /** * Tests app ops version upgrades @@ -59,7 +61,13 @@ import java.util.concurrent.atomic.AtomicInteger; public class AppOpsActiveWatcherTest { @Rule - public FakeAssociationRule mFakeAssociationRule = new FakeAssociationRule(); + public VirtualDeviceRule virtualDeviceRule = + VirtualDeviceRule.withAdditionalPermissions( + Manifest.permission.GRANT_RUNTIME_PERMISSIONS, + Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, + Manifest.permission.CREATE_VIRTUAL_DEVICE, + Manifest.permission.GET_APP_OPS_STATS + ); private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000; @Test @@ -145,20 +153,28 @@ public class AppOpsActiveWatcherTest { @Test public void testWatchActiveOpsForExternalDevice() { - final VirtualDeviceManager virtualDeviceManager = getContext().getSystemService( - VirtualDeviceManager.class); - AtomicInteger virtualDeviceId = new AtomicInteger(); - runWithShellPermissionIdentity(() -> { - final VirtualDeviceManager.VirtualDevice virtualDevice = - virtualDeviceManager.createVirtualDevice( - mFakeAssociationRule.getAssociationInfo().getId(), - new VirtualDeviceParams.Builder().setName("virtual_device").build()); - virtualDeviceId.set(virtualDevice.getDeviceId()); - }); + VirtualDeviceManager.VirtualDevice virtualDevice = + virtualDeviceRule.createManagedVirtualDevice( + new VirtualDeviceParams.Builder() + .setDevicePolicy(POLICY_TYPE_CAMERA, DEVICE_POLICY_CUSTOM) + .build() + ); + + PermissionManager permissionManager = + getContext().getSystemService(PermissionManager.class); + + // Unlike runtime permission being automatically granted to the default device, we need to + // grant camera permission to the external device first before we can start op. + permissionManager.grantRuntimePermission( + getContext().getOpPackageName(), + Manifest.permission.CAMERA, + virtualDevice.getPersistentDeviceId() + ); + final OnOpActiveChangedListener listener = mock(OnOpActiveChangedListener.class); AttributionSource attributionSource = new AttributionSource(Process.myUid(), getContext().getOpPackageName(), getContext().getAttributionTag(), - virtualDeviceId.get()); + virtualDevice.getDeviceId()); final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); appOpsManager.startWatchingActive(new String[]{AppOpsManager.OPSTR_CAMERA, @@ -171,7 +187,7 @@ public class AppOpsActiveWatcherTest { verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA), eq(Process.myUid()), eq(getContext().getOpPackageName()), - eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(true), + eq(getContext().getAttributionTag()), eq(virtualDevice.getDeviceId()), eq(true), eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE), eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE)); verifyNoMoreInteractions(listener); @@ -182,7 +198,7 @@ public class AppOpsActiveWatcherTest { verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA), eq(Process.myUid()), eq(getContext().getOpPackageName()), - eq(getContext().getAttributionTag()), eq(virtualDeviceId.get()), eq(false), + eq(getContext().getAttributionTag()), eq(virtualDevice.getDeviceId()), eq(false), eq(AppOpsManager.ATTRIBUTION_FLAGS_NONE), eq(AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE)); verifyNoMoreInteractions(listener); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index a8e0c268011b..bd1681358291 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -49,6 +49,7 @@ import android.hardware.biometrics.BiometricRequestConstants; import android.hardware.biometrics.BiometricSourceType; import android.hardware.biometrics.common.ICancellationSignal; import android.hardware.biometrics.common.OperationContext; +import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.events.AuthenticationAcquiredInfo; import android.hardware.biometrics.events.AuthenticationErrorInfo; import android.hardware.biometrics.events.AuthenticationFailedInfo; @@ -391,8 +392,8 @@ public class FingerprintAuthenticationClientTest { verify(mBiometricContext).subscribe(mOperationContextCaptor.capture(), mStartHalConsumerCaptor.capture(), mContextInjector.capture(), any()); - mStartHalConsumerCaptor.getValue().accept(mOperationContextCaptor - .getValue().toAidlContext()); + final OperationContextExt operationContext = mOperationContextCaptor.getValue(); + mStartHalConsumerCaptor.getValue().accept(operationContext.toAidlContext()); final ArgumentCaptor<OperationContext> captor = ArgumentCaptor.forClass(OperationContext.class); @@ -400,16 +401,20 @@ public class FingerprintAuthenticationClientTest { OperationContext opContext = captor.getValue(); - assertThat(opContext).isSameInstanceAs( - mOperationContextCaptor.getValue().toAidlContext()); + assertThat(opContext).isSameInstanceAs(operationContext.toAidlContext()); + opContext.operationState = new OperationState(); + opContext.operationState.setFingerprintOperationState( + new OperationState.FingerprintOperationState()); mContextInjector.getValue().accept(opContext); verify(mHal).onContextChanged(same(opContext)); + verify(mHal, times(2)).setIgnoreDisplayTouches( + opContext.operationState.getFingerprintOperationState().isHardwareIgnoringTouches); client.stopHalOperation(); - verify(mBiometricContext).unsubscribe(same(mOperationContextCaptor.getValue())); + verify(mBiometricContext).unsubscribe(same(operationContext)); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java index 400f9bbec83e..4f94704ee39f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java @@ -317,6 +317,70 @@ public class ActivityTaskManagerServiceTests extends WindowTestsBase { } @Test + public void testEnterPipModeWhenResumed_autoEnterEnabled_returnTrue() { + final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity(); + PictureInPictureParams params = mock(PictureInPictureParams.class); + activity.pictureInPictureArgs = params; + + doReturn(true).when(activity).isState(RESUMED); + doReturn(false).when(activity).inPinnedWindowingMode(); + doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean()); + doReturn(true).when(params).isAutoEnterEnabled(); + + assertTrue(mAtm.enterPictureInPictureMode(activity, params, + true /* fromClient */, true /* isAutoEnter */)); + } + + @Test + public void testEnterPipModeWhenResumed_autoEnterDisabled_returnTrue() { + final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity(); + PictureInPictureParams params = mock(PictureInPictureParams.class); + activity.pictureInPictureArgs = params; + + doReturn(true).when(activity).isState(RESUMED); + doReturn(false).when(activity).inPinnedWindowingMode(); + doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean()); + doReturn(false).when(params).isAutoEnterEnabled(); + + assertTrue(mAtm.enterPictureInPictureMode(activity, params, + true /* fromClient */, false /* isAutoEnter */)); + } + + @Test + public void testEnterPipModeWhenPausing_autoEnterEnabled_returnFalse() { + final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity(); + PictureInPictureParams params = mock(PictureInPictureParams.class); + activity.pictureInPictureArgs = params; + + doReturn(true).when(activity).isState(PAUSING); + doReturn(false).when(activity).inPinnedWindowingMode(); + doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean()); + doReturn(true).when(params).isAutoEnterEnabled(); + + assertFalse(mAtm.enterPictureInPictureMode(activity, params, + true /* fromClient */, true /* isAutoEnter */)); + } + + @Test + public void testEnterPipModeWhenPausing_autoEnterDisabled_returnTrue() { + final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity(); + PictureInPictureParams params = mock(PictureInPictureParams.class); + activity.pictureInPictureArgs = params; + + doReturn(true).when(activity).isState(PAUSING); + doReturn(false).when(activity).inPinnedWindowingMode(); + doReturn(true).when(activity).checkEnterPictureInPictureState(anyString(), anyBoolean()); + doReturn(false).when(params).isAutoEnterEnabled(); + + assertTrue(mAtm.enterPictureInPictureMode(activity, params, + true /* fromClient */, false /* isAutoEnter */)); + } + + @Test public void testResumeNextActivityOnCrashedAppDied() { mSupervisor.beginDeferResume(); final ActivityRecord homeActivity = new ActivityBuilder(mAtm) diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 000162a9e705..ac1dc087fedb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4484,6 +4484,21 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testPortraitAppInTabletop_notSplitScreen() { + final int dw = 2400; + setUpDisplaySizeWithApp(dw, 2000); + prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); + + final int initialWidth = mActivity.getBounds().width(); + + setFoldablePosture(true /* isHalfFolded */, true /* isTabletop */); + + final int finalWidth = mActivity.getBounds().width(); + assertEquals(initialWidth, finalWidth); + assertNotEquals(finalWidth, getExpectedSplitSize(dw)); + } + + @Test public void testUpdateResolvedBoundsHorizontalPosition_bookModeEnabled() { // Set up a display in landscape with a fixed-orientation PORTRAIT app setUpDisplaySizeWithApp(2800, 1400); diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 87bb0f05c742..b00571585d00 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -954,6 +954,11 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_MODEM_STATE_CONNECTED = 7; /** + * The satellite modem is being powered on. + * @hide + */ + public static final int SATELLITE_MODEM_STATE_ENABLING_SATELLITE = 8; + /** * Satellite modem state is unknown. This generic modem state should be used only when the * modem state cannot be mapped to other specific modem states. */ @@ -970,6 +975,7 @@ public final class SatelliteManager { SATELLITE_MODEM_STATE_UNAVAILABLE, SATELLITE_MODEM_STATE_NOT_CONNECTED, SATELLITE_MODEM_STATE_CONNECTED, + SATELLITE_MODEM_STATE_ENABLING_SATELLITE, SATELLITE_MODEM_STATE_UNKNOWN }) @Retention(RetentionPolicy.SOURCE) diff --git a/tests/FlickerTests/test-apps/app-helpers/OWNERS b/tests/FlickerTests/test-apps/app-helpers/OWNERS new file mode 100644 index 000000000000..ab6253200f73 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/OWNERS @@ -0,0 +1,2 @@ +uysalorhan@google.com +pragyabajoria@google.com
\ No newline at end of file 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 new file mode 100644 index 000000000000..461dfeca09e3 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -0,0 +1,124 @@ +/* + * 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.wm.flicker.helpers + +import android.tools.device.apphelpers.IStandardAppHelper +import android.tools.helpers.SYSTEMUI_PACKAGE +import android.tools.traces.parsers.WindowManagerStateHelper +import android.tools.traces.wm.WindowingMode +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until + +/** + * Wrapper class around App helper classes. This class adds functionality to the apps that the + * desktop apps would have. + */ +open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : + IStandardAppHelper by innerHelper { + private val TIMEOUT_MS = 3_000L + private val CAPTION = "desktop_mode_caption" + private val CAPTION_HANDLE = "caption_handle" + private val MAXIMIZE_BUTTON = "maximize_window" + private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view" + private val CLOSE_BUTTON = "close_window" + + private val caption: BySelector + get() = By.res(SYSTEMUI_PACKAGE, CAPTION) + + /** Wait for an app moved to desktop to finish its transition. */ + private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .withWindowSurfaceAppeared(innerHelper) + .withFreeformApp(innerHelper) + .withAppTransitionIdle() + .waitForAndVerify() + } + + /** Move an app to Desktop by dragging the app handle at the top. */ + fun enterDesktopWithDrag( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + ) { + innerHelper.launchViaIntent(wmHelper) + dragToDesktop(wmHelper, device) + waitForAppToMoveToDesktop(wmHelper) + } + + private fun dragToDesktop(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val startX = windowRect.centerX() + + // Start dragging a little under the top to prevent dragging the notification shade. + val startY = 10 + + val displayRect = + wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect + ?: throw IllegalStateException("Default display is null") + + // The position we want to drag to + val endY = displayRect.centerY() / 2 + + // drag the window to move to desktop + device.drag(startX, startY, startX, endY, 100) + } + + /** Click maximise button on the app header for the given app. */ + fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val caption = getCaptionForTheApp(wmHelper, device) + val maximizeButton = + caption + ?.children + ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) } + ?.children + ?.get(0) + maximizeButton?.click() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + /** Click close button on the app header for the given app. */ + fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val caption = getCaptionForTheApp(wmHelper, device) + val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) } + closeButton?.click() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceDisappeared(innerHelper) + .waitForAndVerify() + } + + private fun getCaptionForTheApp( + wmHelper: WindowManagerStateHelper, + device: UiDevice + ): UiObject2? { + if ( + wmHelper.getWindow(innerHelper)?.windowingMode != + WindowingMode.WINDOWING_MODE_FREEFORM.value + ) + error("expected a freeform window with caption but window is not in freeform mode") + val captions = + device.wait(Until.findObjects(caption), TIMEOUT_MS) + ?: error("Unable to find view $caption\n") + + return captions.find { + wmHelper.getWindowRegion(innerHelper).bounds.contains(it.visibleBounds) + } + } +} |