Merge "Add support for rules with filter=ALL in ZenModesBackend" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 3e5f1cb..fa11278 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -379,6 +379,7 @@
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 @@
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 443a6c0e..5a3ff83 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -263,6 +263,7 @@
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 f4d1304..b3312a8 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -8788,6 +8788,18 @@
* 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 @@
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 f7e0e9f..714454b 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -93,7 +93,10 @@
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 @@
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 @@
+ " 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 @@
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 @@
}
/**
+ * {@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 BigTextStyle bigText(CharSequence cs) {
mBigText = safeCharSequence(cs);
- if (Flags.cleanUpSpansAndNewLines()) {
- mBigText = cleanUpNewLines(mBigText);
- }
return this;
}
@@ -8227,6 +8355,9 @@
// 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 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 c080a6b..cfbe741 100644
--- a/core/java/android/app/contextualsearch/ContextualSearchManager.java
+++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java
@@ -27,6 +27,7 @@
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 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 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 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 @@
"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 06a0f5c..769b658 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -237,19 +237,15 @@
@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 00a814e..6ab66b3 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -39,11 +39,6 @@
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 535cebb..7ac9547 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1548,6 +1548,26 @@
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 6d3a56d..c57a3a6 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -113,6 +113,26 @@
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 7aa0349..3c4307c 100644
--- a/core/java/android/database/DatabaseUtils.java
+++ b/core/java/android/database/DatabaseUtils.java
@@ -1671,7 +1671,7 @@
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 ad74a9f..2ceb1c7 100644
--- a/core/java/android/os/connectivity/WifiActivityEnergyInfo.java
+++ b/core/java/android/os/connectivity/WifiActivityEnergyInfo.java
@@ -37,8 +37,10 @@
* 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 @@
@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 @@
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 @@
* voltage);
}
+ private static long calculateEnergyMicroJoules$ravenwood(long txDurationMillis,
+ long rxDurationMillis, long idleDurationMillis) {
+ return 0;
+ }
+
/** @hide */
public WifiActivityEnergyInfo(
@ElapsedRealtimeLong long timeSinceBootMillis,
@@ -152,7 +160,7 @@
+ " mControllerRxDurationMillis=" + mControllerRxDurationMillis
+ " mControllerScanDurationMillis=" + mControllerScanDurationMillis
+ " mControllerIdleDurationMillis=" + mControllerIdleDurationMillis
- + " mControllerEnergyUsedMicroJoules=" + mControllerEnergyUsedMicroJoules
+ + " mControllerEnergyUsedMicroJoules=" + getControllerEnergyUsedMicroJoules()
+ " }";
}
@@ -231,6 +239,11 @@
/** 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 3b59901..55011e5 100644
--- a/core/java/android/permission/IPermissionManager.aidl
+++ b/core/java/android/permission/IPermissionManager.aidl
@@ -98,7 +98,7 @@
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 2daf4ac..55bb430 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1680,9 +1680,9 @@
* @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 9a02b74b..5da0cb4 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -79,6 +79,11 @@
mService.endDream(this);
}
+ @Override
+ public void comeToFront() {
+ mService.comeToFront(this);
+ }
+
private void onExitRequested() {
try {
mDreamOverlayCallback.onExitRequested();
@@ -130,6 +135,16 @@
});
}
+ 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 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 353828c..c26d83c 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -18,6 +18,7 @@
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.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 @@
private boolean mDebug = false;
private ComponentName mDreamComponent;
+ private DreamAccessibility mDreamAccessibility;
private boolean mShouldShowComplications;
private DreamServiceWrapper mDreamServiceWrapper;
@@ -455,6 +458,15 @@
/** {@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 void setInteractive(boolean interactive) {
mInteractive = interactive;
+ updateAccessibilityMessage();
}
/**
@@ -1149,6 +1162,19 @@
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 @@
// 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 @@
});
}
+ 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 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 e45384f..85f0368 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -48,4 +48,5 @@
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 78b7280..5054d4d 100644
--- a/core/java/android/service/dreams/IDreamOverlayClient.aidl
+++ b/core/java/android/service/dreams/IDreamOverlayClient.aidl
@@ -42,4 +42,7 @@
/** 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 8b5d875..2e2651b 100644
--- a/core/java/android/service/dreams/IDreamService.aidl
+++ b/core/java/android/service/dreams/IDreamService.aidl
@@ -25,4 +25,5 @@
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 88f1090..0a458bc 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -19,3 +19,10 @@
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 0000000..c38f41b
--- /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 fb57921..4281da1 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -1685,6 +1685,10 @@
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 dc6a035..b8b84d9 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -644,6 +644,15 @@
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 @@
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 @@
}
/**
+ * 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 @@
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 d39c4ce..6160fdb 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -1073,6 +1073,24 @@
}
/**
+ * 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 @@
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 38aeb37..161a79b 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -30,6 +30,8 @@
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 @@
// 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 bde9c77..3015791 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -477,7 +477,12 @@
}
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 51d7caa..65d9b3a 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -291,6 +291,15 @@
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 6c852c3..86e5bea 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -76,6 +76,21 @@
"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 fbb5116..fb1c331 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -11254,8 +11254,10 @@
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 4c2bbd4..c8d6810 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 @@
# 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 969f95d..792c223 100644
--- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
+++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl
@@ -81,4 +81,5 @@
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 0203ea4..04332cd 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -120,4 +120,5 @@
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 ae47899..8d97362 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 60289c1..e96240d 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 fbc8811..0bf6347 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 c9cb2cc..e25fdf9 100644
--- a/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseUtilsTest.java
@@ -90,6 +90,12 @@
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 a67821b..0297901 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 @@
/** 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 74f087b..0b3c2ba3 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 @@
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 b87c2f6..7ceaaea 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 @@
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 @@
/** 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 @@
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 57cf992..e885262 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 @@
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 10b7619..fdde3ee 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_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 @@
@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 32442f7..4f71a02 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 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 c1adfff..d8ac8e9 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 @@
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 cc8e3e0..472003c 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 @@
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 0000000..5563bb9
--- /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 0000000..3d16d2219
--- /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 0000000..75dfeba
--- /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 4c781d3..9dfafe9 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 d99d875..1c7d623 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 0000000..0c2b501
--- /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 0403b4f..9e9998e 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 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 @@
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 345bc5e..0000000
--- 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 b870023..8e07a2f 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 6993d52..7a155c5 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -45,7 +45,7 @@
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 @@
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 14e7a53..859cc57 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 @@
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 c16602c..a8db0f4 100644
--- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
+++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
@@ -52,8 +52,8 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
// 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, 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 8a13c03..4492c85 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -2088,28 +2088,24 @@
}
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 (i == 0) {
+ *initialOffset = offset;
+ }
+ if (CC_UNLIKELY((offset < 0)
+ || (size < 0)
+ || ((INT32_MAX - offset) < size)
+ || ((offset - (*initialOffset)) != *totalSize))) {
if (errorDetailMsg) {
- *errorDetailMsg = "Error: Queuing an empty BufferInfo";
+ *errorDetailMsg = "Error: offset/size in 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 (flags == 0 && size == 0) {
if (errorDetailMsg) {
- *errorDetailMsg = "Error: offset/size in BufferInfo";
+ *errorDetailMsg = "Error: Queuing an empty BufferInfo";
}
return BAD_VALUE;
}
diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt
index 1c203e3..346c87d 100644
--- a/native/android/libandroid.map.txt
+++ b/native/android/libandroid.map.txt
@@ -366,6 +366,7 @@
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 fbb35e2..83056b2 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -463,6 +463,15 @@
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 @@
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 @@
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 974e6e6..58f56b8 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
usleep(2); // Sleep for longer than preferredUpdateRateNanos.
struct TestPair {
- WorkDuration duration;
+ hal::WorkDuration duration;
int expectedResult;
};
std::vector<TestPair> testPairs{
@@ -282,7 +294,7 @@
{{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 cddbb6b..8cdef38 100644
--- a/packages/CrashRecovery/aconfig/flags.aconfig
+++ b/packages/CrashRecovery/aconfig/flags.aconfig
@@ -16,3 +16,11 @@
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 bf69d3b..4437475 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 77979f0..8755247 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 8f67354..7ea4b18 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.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 a67839a..73c96d9 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 4af9e3c..2f6d839 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 @@
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 36a9ecf..a7b7da5 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 @@
}
}
- // 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 @@
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 892d907..6174ab9 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -169,7 +169,6 @@
"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 @@
kotlincflags: ["-Xjvm-default=all"],
optimize: {
shrink_resources: false,
+ optimized_shrink_resources: false,
proguard_flags_files: ["proguard.flags"],
},
@@ -515,6 +515,7 @@
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 561c85e..5cc3caf 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 16bc7f2..e38f347 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -776,6 +776,13 @@
}
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 @@
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 f1f1b57..f539a23 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 @@
@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 @@
}
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 @@
weight = lockScreenWeight,
textSize = -1f,
color = lockScreenColor,
- animate = isAnimationEnabled,
+ animate = true,
duration = APPEAR_ANIM_DURATION,
interpolator = Interpolators.EMPHASIZED_DECELERATE,
delay = 0,
@@ -327,7 +322,7 @@
}
fun animateFoldAppear(animate: Boolean = true) {
- if (isAnimationEnabled && textAnimator == null) {
+ if (textAnimator == null) {
return
}
logger.d("animateFoldAppear")
@@ -344,7 +339,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
onAnimationEnd = onAnimationEnd
)
textAnimator?.glyphFilter = glyphFilter
- if (color != null && !isAnimationEnabled) {
- setTextColor(color)
- }
}
}
}
@@ -482,7 +471,7 @@
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 04c4efb..fefe5a0 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 @@
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 @@
2)).isTrue();
}
- private enum Direction {
- DOWN,
- UP,
- }
-
@Test
public void testSwipeUp_whenBouncerInitiallyShowing_reduceHeightWithExclusionRects() {
mTouchHandler.getTouchInitiationRegion(SCREEN_BOUNDS, mRegion,
@@ -210,7 +204,7 @@
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 @@
}
/**
- * 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 @@
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 @@
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 @@
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 @@
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 @@
}
/**
- * 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 @@
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 @@
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 @@
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 @@
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 27bffd0..11a4241 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 @@
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.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.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 @@
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);
+ }
+
+ // Verifies that a swipe down in the gesture region is captured by the shade touch handler.
+ @Test
+ public void testSwipeDown_captured() {
+ final boolean captured = swipe(Direction.DOWN);
+
+ assertThat(captured).isTrue();
+ }
+
+ // Verifies that a swipe in the upward direction is not catpured.
+ @Test
+ public void testSwipeUp_notCaptured() {
+ final boolean captured = swipe(Direction.UP);
+
+ // Motion events not captured as the swipe is going in the wrong direction.
+ assertThat(captured).isFalse();
+ }
+
+ // Verifies that a swipe down forwards captured touches to the shade window for handling.
+ @Test
+ public void testSwipeDown_sentToShadeWindow() {
+ swipe(Direction.DOWN);
+
+ // Both motion events are sent for the shade window to process.
+ verify(mCentralSurfaces, times(2)).handleExternalShadeWindowTouch(any());
+ }
+
+ // Verifies that a swipe down is not forwarded to the shade window.
+ @Test
+ public void testSwipeUp_touchesNotSent() {
+ swipe(Direction.UP);
+
+ // 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());
}
/**
- * Verify that touches aren't handled when the bouncer is showing.
+ * 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.
*/
- @Test
- public void testInactiveOnBouncer() {
- when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
+ private boolean swipe(Direction direction) {
+ Mockito.clearInvocations(mTouchSession);
mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).pop();
+
+ 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;
}
- /**
- * Make sure {@link ShadeTouchHandler}
- */
- @Test
- public void testTouchPilferingOnScroll() {
- final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class);
- final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class);
-
- final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor =
- ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
-
- mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture());
-
- assertThat(gestureListenerArgumentCaptor.getValue()
- .onScroll(motionEvent1, motionEvent2, 1, 1))
- .isTrue();
+ private enum Direction {
+ DOWN, UP,
}
-
- /**
- * Ensure touches are propagated to the {@link ShadeViewController}.
- */
- @Test
- public void testEventPropagation() {
- final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
-
- final ArgumentCaptor<InputChannelCompat.InputEventListener>
- inputEventListenerArgumentCaptor =
- ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
-
- mTouchHandler.onSessionStart(mTouchSession);
- verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
- inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
- verify(mShadeViewController).handleExternalTouch(motionEvent);
- }
-
}
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 0000000..722eb2b
--- /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 76f15d2..b4b812d 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.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 @@
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 a4c7abd..fe4d32d 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.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 @@
@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 @@
MockitoAnnotations.initMocks(this)
fakeWidgets = MutableStateFlow(emptyMap())
logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
+ backupUtils = CommunalBackupUtils(kosmos.applicationContext)
setAppWidgetIds(emptyList())
@@ -106,6 +115,8 @@
communalWidgetHost,
communalWidgetDao,
logBuffer,
+ backupManager,
+ backupUtils,
)
}
@@ -129,6 +140,9 @@
priority = communalItemRankEntry.rank,
)
)
+
+ // Verify backup not requested
+ verify(backupManager, never()).dataChanged()
}
@Test
@@ -152,6 +166,9 @@
verify(communalWidgetHost).allocateIdAndBindWidget(provider, user)
verify(communalWidgetDao).addWidget(id, provider, priority)
+
+ // Verify backup requested
+ verify(backupManager).dataChanged()
}
@Test
@@ -176,6 +193,9 @@
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 @@
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 @@
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 @@
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 @@
verify(communalWidgetDao).deleteWidgetById(id)
verify(appWidgetHost, never()).deleteAppWidgetId(id)
+
+ // Verify backup not requested
+ verify(backupManager, never()).dataChanged()
}
@Test
@@ -259,6 +291,147 @@
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 @@
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 41bc1dc..e2e5169 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 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.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.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 @@
Handler mHandler;
@Mock
+ CoroutineDispatcher mDispatcher;
+
+ @Mock
BlurUtils mBlurUtils;
@Mock
@@ -103,6 +113,8 @@
@Mock
DreamOverlayStateController mStateController;
+ @Mock
+ KeyguardTransitionInteractor mKeyguardTransitionInteractor;
DreamOverlayContainerViewController mController;
@@ -115,6 +127,7 @@
when(mDreamOverlayContainerView.getViewRootImpl()).thenReturn(mViewRoot);
when(mDreamOverlayContainerView.getRootSurfaceControl())
.thenReturn(mAttachedSurfaceControl);
+ when(mKeyguardTransitionInteractor.isFinishedInStateWhere(any())).thenReturn(emptyFlow());
mController = new DreamOverlayContainerViewController(
mDreamOverlayContainerView,
@@ -124,6 +137,7 @@
mLowLightTransitionCoordinator,
mBlurUtils,
mHandler,
+ mDispatcher,
mResources,
MAX_BURN_IN_OFFSET,
BURN_IN_PROTECTION_UPDATE_INTERVAL,
@@ -131,7 +145,8 @@
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 b18a8ec..bdb0c9a 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 @@
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.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 @@
@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 @@
whenever(mAmbientTouchComponent.getTouchMonitor()).thenReturn(mTouchMonitor)
whenever(mDreamOverlayContainerViewController.containerView)
.thenReturn(mDreamOverlayContainerView)
+ whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
mWindowParams = WindowManager.LayoutParams()
mService =
DreamOverlayService(
@@ -154,24 +191,30 @@
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 @@
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 @@
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 @@
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 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mDreamOverlayContainerViewController).init()
+ verify(mDreamOverlayContainerViewController).init()
}
@Test
@@ -264,7 +306,7 @@
false /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Mockito.verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView)
+ verify(mDreamOverlayContainerViewParent).removeView(mDreamOverlayContainerView)
}
@Test
@@ -279,7 +321,7 @@
true /*shouldShowComplication*/
)
mMainExecutor.runAllReady()
- Truth.assertThat(mService!!.shouldShowComplications()).isTrue()
+ assertThat(mService.shouldShowComplications()).isTrue()
}
@Test
@@ -294,8 +336,8 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
val client = client
// Destroy the service.
- mService!!.onDestroy()
+ mService.onDestroy()
mMainExecutor.runAllReady()
// Inform the overlay service of dream starting.
@@ -458,15 +500,15 @@
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 @@
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 @@
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 @@
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 @@
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 @@
.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 29fbee0..e3c6dee 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 @@
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
similarity index 96%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekbarHapticPluginTest.kt
index 805b4a8..855b6d0 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 @@
@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 @@
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 0000000..88189db
--- /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 db31ad5..4d69f0d 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_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 @@
}
@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
similarity index 77%
rename from packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index b89ccef..a8da116 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.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.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 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 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 @@
@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 @@
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 @@
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 96b2b7a..aa0ca18 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 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 @@
@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 2ab934c..07c4b00 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.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.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 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 val underTest = kosmos.shadeStartable
+ 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 2397de6..5312ad8 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.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 @@
@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
similarity index 69%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index be5af88..fc9535c 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.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 @@
uiEventLogger,
kosmos.interactionJankMonitor,
JavaAdapter(testScope.backgroundScope),
- { shadeInteractor },
+ { kosmos.shadeInteractor },
{ kosmos.deviceUnlockedInteractor },
{ kosmos.sceneInteractor },
{ kosmos.keyguardClockInteractor },
@@ -127,59 +107,6 @@
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 0000000..47fd78a
--- /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 0000000..77edf58
--- /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 0000000..292e496
--- /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 291f8a2..546bf1c 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 0000000..5e4304e
--- /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 4a73d85..19273ec 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 9aff9fd..27aa15f 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 2c9006e..3d57111 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 39e1c41..55ccaa6 100644
--- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java
+++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java
@@ -33,12 +33,23 @@
* 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 ba869bd..4b07402 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.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 @@
int serviceIndex = 0;
do {
+ startedAny = false;
queue = nextQueue;
nextQueue = new ArrayDeque<>(startables.size());
@@ -307,9 +307,9 @@
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 @@
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 d0f08f5..85aeb27 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.MotionEvent;
import android.view.VelocityTracker;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.UiEvent;
@@ -94,13 +95,11 @@
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 @@
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 @@
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 @@
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 @@
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 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 @@
: 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 @@
}
}
- 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 @@
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 9ef9938..9c7fc9d 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.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 @@
*/
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 @@
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 a667de2..970699f 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.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 @@
"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 @@
userId = userHandle.identifier,
)
)
+ addHelper(
+ COMMUNAL_STATE_BACKUP_KEY,
+ CommunalBackupHelper(userHandle, CommunalBackupUtils(context = this)),
+ )
}
}
@@ -116,7 +122,7 @@
}
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 0000000..cdeeb6f
--- /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 5a174b9..f437032 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.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 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 @@
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 @@
}
}
}
+
+ 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 27af99e..7fa091a 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 @@
)
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 0000000..b95c966
--- /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 0000000..a8e5174
--- /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 595d320..3ce8109 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 e766290..b8161fd 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 @@
@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 9cd77c4..d174fd1 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.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 @@
@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 @@
@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 @@
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 e395ca9..1f54e70 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 @@
* @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 @@
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 @@
provider = provider,
priority = priority,
)
+ backupManager.dataChanged()
} else {
appWidgetHost.deleteAppWidgetId(id)
}
@@ -155,6 +171,7 @@
if (communalWidgetDao.deleteWidgetById(widgetId)) {
appWidgetHost.deleteAppWidgetId(widgetId)
logger.i("Deleted widget with id $widgetId.")
+ backupManager.dataChanged()
}
}
}
@@ -165,6 +182,76 @@
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 619e052..7448e14 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 @@
}
/**
+ * 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 0000000..2d661cd
--- /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 0000000..0816259
--- /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 60fb8d4..aa6516d 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 @@
@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 804c8c4..1dd3722 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.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 java.util.Map;
import java.util.Optional;
+import java.util.Set;
import javax.inject.Provider;
@@ -160,6 +162,11 @@
@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 23fc8ac..593196c 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.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 @@
@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 6b85d30..3462164 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 @@
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.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.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.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 @@
})
public abstract class SystemUIModule {
+ @Multibinds
+ @Dependencies
+ abstract Map<Class<?>, Set<Class<? extends CoreStartable>>> startableDependencyMap();
+
@Binds
abstract BootCompleteCache bindBootCompleteCache(BootCompleteCacheImpl bootCompleteCache);
@@ -309,6 +319,13 @@
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 5a036b1..0c2709e 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.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.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 javax.inject.Inject;
import javax.inject.Named;
+import kotlinx.coroutines.CoroutineDispatcher;
+
/**
* View controller for {@link DreamOverlayContainerView}.
*/
@@ -62,6 +67,7 @@
private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
private final DreamOverlayStateController mStateController;
private final LowLightTransitionCoordinator mLowLightTransitionCoordinator;
+ private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final ComplicationHostViewController mComplicationHostViewController;
@@ -81,6 +87,7 @@
// 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 @@
private boolean mBouncerAnimating;
private boolean mWakingUpFromSwipe;
+ private boolean mAnyBouncerShowing;
private final BouncerlessScrimController mBouncerlessScrimController;
@@ -170,6 +178,7 @@
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 @@
PrimaryBouncerCallbackInteractor primaryBouncerCallbackInteractor,
DreamOverlayAnimationsController animationsController,
DreamOverlayStateController stateController,
- BouncerlessScrimController bouncerlessScrimController) {
+ BouncerlessScrimController bouncerlessScrimController,
+ KeyguardTransitionInteractor keyguardTransitionInteractor) {
super(containerView);
mDreamOverlayContentView = contentView;
mStatusBarViewController = statusBarViewController;
@@ -190,6 +200,8 @@
mBouncerlessScrimController = bouncerlessScrimController;
mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+
mComplicationHostViewController = complicationHostViewController;
mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
R.dimen.dream_overlay_y_offset);
@@ -200,6 +212,7 @@
ViewGroup.LayoutParams.MATCH_PARENT));
mHandler = handler;
+ mMainDispatcher = mainDispatcher;
mMaxBurnInOffset = maxBurnInOffset;
mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
mMillisUntilFullJitter = millisUntilFullJitter;
@@ -225,6 +238,12 @@
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 @@
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 1135afe..a9ef531 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 @@
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.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.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 @@
* 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 @@
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 @@
// 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 @@
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 @@
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 @@
mUiEventLogger = uiEventLogger;
mDreamOverlayCallbackController = dreamOverlayCallbackController;
mWindowTitle = windowTitle;
+ mCommunalInteractor = communalInteractor;
+ mSystemDialogsCloser = systemDialogsCloser;
final ViewModelStore viewModelStore = new ViewModelStore();
final Complication.Host host =
@@ -203,10 +283,32 @@
new HashSet<>(Arrays.asList(
mDreamComplicationComponent.getHideComplicationTouchHandler(),
mDreamOverlayComponent.getCommunalTouchHandler())));
- mLifecycleOwner = lifecycleOwner;
- mLifecycleRegistry = mLifecycleOwner.getRegistry();
+ mLifecycleRegistry = lifecycleOwner.getRegistry();
- mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED));
+ mExecutor.execute(() -> setLifecycleStateLocked(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 @@
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 @@
return;
}
- setCurrentStateLocked(Lifecycle.State.RESUMED);
+ setLifecycleStateLocked(Lifecycle.State.RESUMED);
mStateController.setOverlayActive(true);
final ComponentName dreamComponent = getDreamComponent();
mStateController.setLowLightActive(
@@ -276,14 +379,27 @@
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 @@
}
}
+ @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/volume/panel/component/mediaoutput/shared/model/SessionWithPlayback.kt b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlayback.kt
copy to packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
index c4476fc..6e7239a4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlayback.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/SystemDialogsCloser.java
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel.component.mediaoutput.shared.model
+package com.android.systemui.dreams;
-import android.media.session.PlaybackState
-
-data class SessionWithPlayback(
- val session: MediaDeviceSession,
- val playback: PlaybackState,
-)
+/** 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 31710ac..516b8c5 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.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 @@
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 1c047dd..fff0c58 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 @@
// 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 304fdd6..ca6c8da 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 @@
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 cfa5294..0000000
--- 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
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
rename to packages/SystemUI/src/com/android/systemui/haptics/slider/SeekbarHapticPlugin.kt
index ed82278..2007db34 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 @@
/**
* 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 @@
private val velocityTracker = VelocityTracker.obtain()
- private val sliderEventProducer = SeekableSliderEventProducer()
+ private val sliderEventProducer = SliderStateProducer()
private val sliderHapticFeedbackProvider =
SliderHapticFeedbackProvider(
@@ -56,7 +56,7 @@
systemClock,
)
- private var sliderTracker: SeekableSliderTracker? = null
+ private var sliderTracker: SliderStateTracker? = null
private var pluginScope: CoroutineScope? = null
@@ -86,7 +86,7 @@
fun startInScope(scope: CoroutineScope) {
if (sliderTracker != null) stop()
sliderTracker =
- SeekableSliderTracker(
+ SliderStateTracker(
sliderHapticFeedbackProvider,
sliderEventProducer,
scope,
@@ -116,28 +116,52 @@
/** 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 @@
*
* 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 @@
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 4a63941..0edef99 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 @@
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 @@
/* 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 0000000..1124ab1
--- /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
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderTracker.kt
rename to packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateTracker.kt
index 0af3038..14cf411 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.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 @@
* 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 @@
// 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 @@
}
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 @@
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 0000000..692fbb0
--- /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 7224536..d191768 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 @@
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 92612b8..7d05539 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 @@
}
/** 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 5dafd94..c997617 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 @@
Dispatchers.Main + createCoroutineTracingContext() + coroutineContext
val traceName =
if (Compile.IS_DEBUG && coroutineTracing()) {
- traceSectionName()
+ inferTraceSectionName()
} else {
DEFAULT_TRACE_NAME
}
@@ -197,16 +198,21 @@
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 bd3893b..e6c785e 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.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 @@
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 @@
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 9790a52..d5b05ef 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 @@
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 24089a7..1a881b6 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 @@
});
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 8397d9f..37f2a21 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 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 @@
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 b425fb9..083cee7 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.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 @@
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 @@
BrightnessSliderView brightnessSliderView,
FalsingManager falsingManager,
UiEventLogger uiEventLogger,
- SeekableSliderHapticPlugin brightnessSliderHapticPlugin,
+ SeekbarHapticPlugin brightnessSliderHapticPlugin,
ActivityStarter activityStarter) {
super(brightnessSliderView);
mFalsingManager = falsingManager;
@@ -314,7 +314,7 @@
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 a8481cd..a5a5474 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.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.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,12 +73,27 @@
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
/**
+ * 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.
+ */
+ private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
+
+ /**
+ * 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 touchMonitor: TouchMonitor? = 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.
*/
@@ -80,20 +101,6 @@
private var rightEdgeSwipeRegionWidth: Int = 0
/**
- * 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.
- */
- private var topEdgeSwipeRegionWidth: Int = 0
-
- /**
- * 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
- * resources when [initView] is called.
- */
- private var bottomEdgeSwipeRegionWidth: Int = 0
-
- /**
* True if we are currently tracking a gesture for opening the hub that started in the edge
* swipe region.
*/
@@ -102,9 +109,6 @@
/** 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 @@
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 @@
* 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 @@
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 })
- collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it })
+ collectFlow(
+ containerView,
+ communalInteractor.isCommunalShowing,
+ {
+ hubShowing = it
+ updateLifecycleState()
+ }
+ )
+ collectFlow(
+ containerView,
+ and(shadeInteractor.isAnyFullyExpanded, not(shadeInteractor.isUserInteracting)),
+ {
+ shadeShowing = it
+ updateLifecycleState()
+ }
+ )
collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
communalContainerView = containerView
@@ -221,10 +246,24 @@
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 @@
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 @@
// 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 @@
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 adcb14a..8b7e11c 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 @@
|| 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 @@
state.panelVisible,
state.shadeOrQsExpanded,
state.notificationShadeFocusable,
+ state.glanceableHubShowing,
state.bouncerShowing,
state.keyguardFadingAway,
state.keyguardGoingAway,
@@ -740,6 +744,12 @@
}
@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 e0a98b3..6a4b52a 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 @@
@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 @@
shadeOrQsExpanded.toString(),
notificationShadeFocusable.toString(),
bouncerShowing.toString(),
+ glanceableHubShowing.toString(),
keyguardFadingAway.toString(),
keyguardGoingAway.toString(),
qsExpanded.toString(),
@@ -119,6 +121,7 @@
panelVisible: Boolean,
panelExpanded: Boolean,
notificationShadeFocusable: Boolean,
+ glanceableHubShowing: Boolean,
bouncerShowing: Boolean,
keyguardFadingAway: Boolean,
keyguardGoingAway: Boolean,
@@ -149,6 +152,7 @@
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 @@
"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 907cf5e..44f86da 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 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.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 @@
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 @@
}
/**
- * 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 8eed097..396c5f2 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 @@
* 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 e669556..707d59aa 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 @@
/** 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 d2fe20d..8104755 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 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 @@
/**
* 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 2651d2e..7d97428 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 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 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 @@
return contextForUser.getPackageManager();
}
+ /** Default impl for CoreStartable. */
+ default void start() {}
+
boolean updateIsKeyguard();
boolean updateIsKeyguard(boolean forceStateChange);
@@ -279,11 +283,12 @@
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 8af7ee8..d5e66ff 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 @@
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 e9aa7aa..b2b2cea 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 @@
};
@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 2245541..cd82e1d 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.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 @@
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 @@
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 eebb6fb..22c0530 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.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 @@
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 41ad035..83b8029 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 @@
/** 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
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlayback.kt
rename to packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/shared/model/SessionWithPlaybackState.kt
index c4476fc..bac969d 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 f19fa20..d60d981 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.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.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 @@
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 @@
)
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 @@
)
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 @@
)
} 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 @@
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 e157fc5..4f7610a 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.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 @@
@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 @@
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 @@
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 0000000..7094848
--- /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 0000000..c3849f9
--- /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 20dd913..f77c7a6 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.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 @@
.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 @@
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 c22d35c..0000000
--- 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
similarity index 79%
rename from packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SeekableSliderTrackerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderStateTrackerTest.kt
index 796d6d9..a09d345 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 @@
@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 @@
initTracker(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
// THEN the tracker job is active
- assertThat(mSeekableSliderTracker.isTracking).isTrue()
+ assertThat(mSliderStateTracker.isTracking).isTrue()
}
@Test
@@ -65,14 +65,14 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
)
// 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 @@
)
// 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 @@
)
// 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
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 @@
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 @@
)
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
)
// 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 @@
// 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 @@
// 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 @@
// 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 @@
// 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 @@
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 6a22d86..fb91c78 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.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 @@
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 fd9daf8..03f5ecf 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.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.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 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 @@
@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 @@
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 @@
shadeInteractor,
powerManager,
communalColors,
+ ambientTouchComponentFactory,
kosmos.sceneDataSourceDelegator,
)
@@ -215,62 +218,6 @@
}
@Test
- fun onTouchEvent_topSwipeWhenCommunalOpen_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()
- }
- }
-
- @Test
- fun onTouchEvent_bottomSwipeWhenCommunalOpen_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()
- }
- }
-
- @Test
- fun onTouchEvent_topSwipeWhenDreaming_doesNotIntercept() =
- with(kosmos) {
- testScope.runTest {
- // Communal is open.
- goToScene(CommunalScenes.Communal)
-
- // Device is dreaming.
- fakeKeyguardRepository.setDreaming(true)
- runCurrent()
-
- // Touch event in the top swipe region is not intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
- }
- }
-
- @Test
- fun onTouchEvent_bottomSwipeWhenDreaming_doesNotIntercept() =
- with(kosmos) {
- testScope.runTest {
- // Communal is open.
- goToScene(CommunalScenes.Communal)
-
- // Device is dreaming.
- fakeKeyguardRepository.setDreaming(true)
- runCurrent()
-
- // Touch event in the bottom swipe region is not intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
- }
- }
-
- @Test
fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() =
with(kosmos) {
testScope.runTest {
@@ -327,6 +274,141 @@
}
@Test
+ 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)
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+ // Communal closes.
+ goToScene(CommunalScenes.Blank)
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
+ fun lifecycle_startedAfterPrimaryBouncerShows() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Bouncer is visible.
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.PRIMARY_BOUNCER,
+ testScope
+ )
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
+ fun lifecycle_startedAfterAlternateBouncerShows() =
+ with(kosmos) {
+ testScope.runTest {
+ // Communal is open.
+ goToScene(CommunalScenes.Communal)
+
+ // Bouncer is visible.
+ fakeKeyguardTransitionRepository.sendTransitionSteps(
+ KeyguardState.GLANCEABLE_HUB,
+ KeyguardState.ALTERNATE_BOUNCER,
+ testScope
+ )
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
+ 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)
+
+ // Shade shows up.
+ fakeShadeRepository.setQsExpansion(1.0f)
+ testableLooper.processAllMessages()
+
+ assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+ }
+ }
+
+ @Test
fun editMode_communalAvailable() =
with(kosmos) {
testScope.runTest {
@@ -371,8 +453,6 @@
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 @@
)
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 da09579..d95cc2e 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 @@
}
@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 4ed6fe2..329c0f1 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 @@
_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 4e24233..e2a1fe4 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 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 162f278..d4b7937 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.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.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.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.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 @@
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 @@
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 @@
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 @@
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
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/scene/FakeWindowRootViewComponent.kt
index 63a05d7..6ac3a62 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 9b4d378..243e224 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -266,6 +266,8 @@
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 5e566aa..522aa67 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 @@
// 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 @@
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 @@
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 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
private void sendUpdateIntentLocked(Provider provider, int[] appWidgetIds,
boolean interactive) {
- Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
- intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
- intent.setComponent(provider.id.componentName);
+ 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);
+ 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 @@
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 @@
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 @@
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 @@
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 @@
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 7f64786..9a73a2d 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_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.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 @@
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 6c7546e..64bca33 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 @@
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 @@
}
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 bd67cf420..e171064 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -425,6 +425,8 @@
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 @@
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 @@
mECBMStarted[i] = false;
mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN;
mSCBMStarted[i] = false;
+ mCarrierRoamingNtnMode[i] = false;
}
}
}
@@ -854,6 +858,7 @@
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 @@
mECBMStarted[i] = false;
mSCBMReason[i] = TelephonyManager.STOP_REASON_UNKNOWN;
mSCBMStarted[i] = false;
+ mCarrierRoamingNtnMode[i] = false;
}
mAppOps = mContext.getSystemService(AppOpsManager.class);
@@ -1523,6 +1529,14 @@
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 @@
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 c3a3bf7..eec22c9 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 @@
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 @@
}
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 @@
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 @@
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 f98799d..4f84149 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.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 @@
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 @@
.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 @@
AggregatedPowerStatsConfig.STATE_PROCESS_STATE)
.setProcessor(
new CpuPowerStatsProcessor(mPowerProfile, mCpuScalingPolicies));
+
config.trackPowerComponent(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO)
.trackDeviceStates(
AggregatedPowerStatsConfig.STATE_POWER,
@@ -490,9 +497,21 @@
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 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 fa6b54b..211f952 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 @@
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 045d137..065f3bd 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.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 @@
* 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 @@
/**
* 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 b8bfeda..e59de6a 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 @@
}
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 @@
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 @@
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 @@
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 @@
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 acc086b..00a5911 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.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 @@
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 e3e3be2..baa154d 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 @@
private final float mCustomAnimationRate;
private final BrightnessEvent mBrightnessEvent;
+ private final int mBrightnessAdjustmentFlag;
private DisplayBrightnessState(Builder builder) {
mBrightness = builder.getBrightness();
@@ -58,6 +59,7 @@
mCustomAnimationRate = builder.getCustomAnimationRate();
mShouldUpdateScreenBrightnessSetting = builder.shouldUpdateScreenBrightnessSetting();
mBrightnessEvent = builder.getBrightnessEvent();
+ mBrightnessAdjustmentFlag = builder.getBrightnessAdjustmentFlag();
}
/**
@@ -138,6 +140,14 @@
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 @@
.append(mShouldUpdateScreenBrightnessSetting);
stringBuilder.append("\n mBrightnessEvent:")
.append(Objects.toString(mBrightnessEvent, "null"));
+ stringBuilder.append("\n mBrightnessAdjustmentFlag:").append(mBrightnessAdjustmentFlag);
return stringBuilder.toString();
}
@@ -187,7 +198,8 @@
&& mCustomAnimationRate == otherState.getCustomAnimationRate()
&& mShouldUpdateScreenBrightnessSetting
== otherState.shouldUpdateScreenBrightnessSetting()
- && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent());
+ && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent())
+ && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag();
}
@Override
@@ -195,7 +207,7 @@
return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason,
mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness,
mCustomAnimationRate,
- mShouldUpdateScreenBrightnessSetting, mBrightnessEvent);
+ mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag);
}
/**
@@ -222,6 +234,8 @@
private BrightnessEvent mBrightnessEvent;
+ public int mBrightnessAdjustmentFlag = 0;
+
/**
* Create a builder starting with the values from the specified {@link
* DisplayBrightnessState}.
@@ -242,6 +256,7 @@
builder.setShouldUpdateScreenBrightnessSetting(
state.shouldUpdateScreenBrightnessSetting());
builder.setBrightnessEvent(state.getBrightnessEvent());
+ builder.setBrightnessAdjustmentFlag(state.getBrightnessAdjustmentFlag());
return builder;
}
@@ -418,7 +433,6 @@
return new DisplayBrightnessState(this);
}
-
/**
* This is used to get the BrightnessEvent object from its builder
*/
@@ -434,5 +448,21 @@
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 e88ace1..c5d8686 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 @@
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 @@
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 8b3e4a4..6a88a76 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 @@
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 @@
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 0000000..82c0bbf
--- /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 4be7332..5c4fa842 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 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.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 @@
// 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 @@
@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 @@
.setIsSlowChange(hasAppliedAutoBrightness()
&& !getAutoBrightnessAdjustmentChanged())
.setBrightnessEvent(brightnessEvent)
+ .setBrightnessAdjustmentFlag(mAutoBrightnessAdjustmentReasonsFlags)
+ .setShouldUpdateScreenBrightnessSetting(
+ brightness != strategyExecutionRequest.getCurrentScreenBrightness())
.build();
}
@@ -360,13 +371,6 @@
}
/**
- * 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 @@
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 9c1acea..009a47a 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 @@
// 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 61dd6d5..e96b83a 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 @@
/**
* 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 1f7efd1..2b493f3 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 @@
@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 baac276..5a07ce2 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 @@
@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 dfd47a3..9dc6cff 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 @@
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 60e5eb0..b46873a 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 @@
@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 9605a88..a2982b1 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 @@
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 c9dc298..6a3162c 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 @@
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 6a691d1..6b8817a 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 @@
// 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 0e8a5fb..a818eab 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 @@
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 @@
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 @@
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 42c9e08..fc63494 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.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 @@
/** 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 @@
}
}
+ 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 @@
});
}
+ @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 b18871c..a0dbfa0 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.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 @@
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 e75d0a3..38c95f7 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 @@
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 @@
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 8f6aa95..99401a1 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 @@
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 @@
return;
}
ensureRemoteIntelligenceServiceInitialized();
+ int callerUid = Binder.getCallingUid();
mRemoteOnDeviceIntelligenceService.run(
- service -> service.listFeatures(Binder.getCallingUid(),
+ service -> service.listFeatures(callerUid,
listFeaturesCallback));
}
@@ -250,8 +252,9 @@
return;
}
ensureRemoteIntelligenceServiceInitialized();
+ int callerUid = Binder.getCallingUid();
mRemoteOnDeviceIntelligenceService.run(
- service -> service.getFeatureDetails(Binder.getCallingUid(), feature,
+ service -> service.getFeatureDetails(callerUid, feature,
featureDetailsCallback));
}
@@ -272,8 +275,9 @@
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 @@
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 @@
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 @@
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 20c5b5f..28254d0 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 @@
}
@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 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 @@
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 d7dd902..6dadf8f 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 @@
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 54cb9c9..49c4000 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 @@
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 @@
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 @@
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 @@
}
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 @@
LocalServices.getService(PowerStatsInternal.class));
mNetworkStatsManager = context.getSystemService(NetworkStatsManager.class);
mTelephonyManager = context.getSystemService(TelephonyManager.class);
+ mWifiManager = context.getSystemService(WifiManager.class);
}
@Override
@@ -1950,11 +1973,26 @@
}
@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 @@
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 @@
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 @@
.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 @@
}
}
- 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 @@
}
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 @@
.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 @@
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 @@
}
}
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 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 @@
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 @@
return mCpuPowerStatsCollector;
case BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO:
return mMobileRadioPowerStatsCollector;
+ case BatteryConsumer.POWER_COMPONENT_WIFI:
+ return mWifiPowerStatsCollector;
}
return null;
}
@@ -16056,6 +16147,7 @@
public void schedulePowerStatsSampleCollection() {
mCpuPowerStatsCollector.forceSchedule();
mMobileRadioPowerStatsCollector.forceSchedule();
+ mWifiPowerStatsCollector.forceSchedule();
}
/**
@@ -16074,6 +16166,7 @@
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 97f0986..0d5eabc 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 @@
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 0000000..6321053
--- /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 0000000..0fa6ec6
--- /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 0000000..5e9cc40
--- /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 f925b5f..236a746 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_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 @@
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 @@
mCompatDisplayInsets =
new CompatDisplayInsets(
mDisplayContent, this, letterboxedContainerBounds,
- mResolveConfigHint.mUseLegacyInsetsForStableBounds);
+ mResolveConfigHint.mUseOverrideInsetsForStableBounds);
}
private void clearSizeCompatModeAttributes() {
@@ -8670,7 +8681,7 @@
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 @@
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 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 @@
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 @@
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 a739e57..b3c43bc 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 @@
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 cb5ad91..da1581e 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 @@
// 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 @@
}
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 @@
}
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 @@
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 422cc69..622a809 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 @@
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 @@
};
+ // 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 @@
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 @@
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 9d3ffa9..f23a440 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 @@
/**
* 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 @@
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 7a8bea0..2b631f7 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 @@
static class ConfigOverrideHint {
@Nullable DisplayInfo mTmpOverrideDisplayInfo;
@Nullable ActivityRecord.CompatDisplayInsets mTmpCompatInsets;
- boolean mUseLegacyInsetsForStableBounds;
+ boolean mUseOverrideInsetsForStableBounds;
}
void computeConfigResourceOverrides(@NonNull Configuration inOutConfig,
@@ -2237,11 +2237,11 @@
@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 @@
// 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 407b5d5..eeedec3 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 @@
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 07e534c..de7c0eb 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.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_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.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 @@
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 @@
}
}
- 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 @@
}
@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 @@
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 e3f06c2..1573d09 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 @@
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 @@
}
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 9d8246d..d91a211 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 @@
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 c4e2dc8..b76279b 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 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 @@
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 @@
@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 @@
@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 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 @@
@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 @@
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 c0d988d..b0c7073 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.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 @@
}
}
- 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
}
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 @@
)
}
}
- ?: 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 6d89e80..8db896b 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 @@
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 @@
.setBrightnessReason(brightnessReason)
.setShouldUseAutoBrightness(shouldUseAutoBrightness)
.setShouldUpdateScreenBrightnessSetting(shouldUpdateScreenBrightnessSetting)
+ .setBrightnessAdjustmentFlag(brightnessAdjustmentFlag)
.build();
assertEquals(displayBrightnessState.getBrightness(), brightness, FLOAT_DELTA);
@@ -105,7 +107,9 @@
.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 1ae4099..13a1445 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.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 @@
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 6e163ca..54f2268 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 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 @@
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 @@
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 c434631..47f1746 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 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 @@
.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 d60caf6..9246780 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 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 @@
.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 d8569f7..682c9cc 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 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 @@
.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 c2fa4eb..ccf6309 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 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 @@
.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 530245d..8e7b463 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 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 @@
.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 7147aa8..e799d0e 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 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 @@
.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 9830edb..aaada41 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 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 @@
.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 0000000..99968d5
--- /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 db70434..88ab871 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 @@
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 @@
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 124ae20..a20d935 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.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 @@
}
@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 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -332,7 +356,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -400,7 +424,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -411,7 +435,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -422,7 +446,7 @@
mMediaProjectionCallbackCaptor.getValue().onStart(createMediaProjectionInfo());
- verifyZeroInteractions(mWindowManager);
+ verify(mWindowManager, never()).addBlockScreenCaptureForApps(any());
}
@Test
@@ -435,7 +459,7 @@
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 0000000..8b1d423
--- /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 0000000..257a1a6
--- /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 b487dc6..c970a3e 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.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.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.runner.RunWith;
import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
/**
* Tests app ops version upgrades
@@ -59,7 +61,13 @@
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 @@
@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 @@
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 @@
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 a8e0c26..bd16813 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.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 @@
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 @@
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 400f9bb..4f94704 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 @@
}
@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 000162a..ac1dc08 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 @@
}
@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 87bb0f0..b005715 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -954,6 +954,11 @@
@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 @@
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 0000000..ab62532
--- /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 0000000..461dfec
--- /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)
+ }
+ }
+}