Merge "Update SurfaceEffect color tokens to allow use via Compose." into main
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index 29df80f..876274e 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -116,3 +116,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "additional_quota_for_system_installer"
+ namespace: "backstage_power"
+ description: "Offer additional quota for system installer"
+ bug: "398264531"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0298c1e6..251776e 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2995,6 +2995,8 @@
pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
Flags.startUserBeforeScheduledAlarms());
pw.println();
+ pw.print(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND, Flags.acquireWakelockBeforeSend());
+ pw.println();
pw.decreaseIndent();
pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 637c726..54d337e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -494,6 +494,9 @@
private long mEjLimitAdditionSpecialMs = QcConstants.DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS;
+ private long mAllowedTimePeriodAdditionaInstallerMs =
+ QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS;
+
/**
* The period of time used to calculate expedited job sessions. Apps can only have expedited job
* sessions totalling {@link #mEJLimitsMs}[bucket within this period of time (without factoring
@@ -1095,6 +1098,18 @@
return baseLimitMs;
}
+ private long getAllowedTimePerPeriodMsLocked(final int userId, @NonNull final String pkgName,
+ final int standbyBucket) {
+ final long baseLimitMs = mAllowedTimePerPeriodMs[standbyBucket];
+ if (Flags.adjustQuotaDefaultConstants()
+ && Flags.additionalQuotaForSystemInstaller()
+ && standbyBucket == EXEMPTED_INDEX
+ && mSystemInstallers.contains(userId, pkgName)) {
+ return baseLimitMs + mAllowedTimePeriodAdditionaInstallerMs;
+ }
+ return baseLimitMs;
+ }
+
/**
* Returns the amount of time, in milliseconds, until the package would have reached its
* duration quota, assuming it has a job counting towards its quota the entire time. This takes
@@ -1112,25 +1127,26 @@
List<TimedEvent> events = mTimingEvents.get(userId, packageName);
final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ final long allowedTimePerPeriodMs =
+ getAllowedTimePerPeriodMsLocked(userId, packageName, standbyBucket);
if (events == null || events.size() == 0) {
// Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
// essentially run until they reach the maximum limit.
- if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
+ if (stats.windowSizeMs == allowedTimePerPeriodMs) {
return mMaxExecutionTimeMs;
}
- return mAllowedTimePerPeriodMs[standbyBucket];
+ return allowedTimePerPeriodMs;
}
final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
- final long allowedTimePerPeriodMs = mAllowedTimePerPeriodMs[standbyBucket];
final long allowedTimeRemainingMs = allowedTimePerPeriodMs - stats.executionTimeInWindowMs;
final long maxExecutionTimeRemainingMs =
mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
// Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
// essentially run until they reach the maximum limit.
- if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
+ if (stats.windowSizeMs == allowedTimePerPeriodMs) {
return calculateTimeUntilQuotaConsumedLocked(
events, startMaxElapsed, maxExecutionTimeRemainingMs);
}
@@ -1270,7 +1286,8 @@
appStats[standbyBucket] = stats;
}
if (refreshStatsIfOld) {
- final long bucketAllowedTimeMs = mAllowedTimePerPeriodMs[standbyBucket];
+ final long bucketAllowedTimeMs =
+ getAllowedTimePerPeriodMsLocked(userId, packageName, standbyBucket);
final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
@@ -1845,9 +1862,10 @@
final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats);
final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats);
final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName);
-
+ final long allowedTimePerPeriosMs =
+ getAllowedTimePerPeriodMsLocked(userId, packageName, standbyBucket);
final boolean inRegularQuota =
- stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs[standbyBucket]
+ stats.executionTimeInWindowMs < allowedTimePerPeriosMs
&& stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
&& isUnderJobCountQuota
&& isUnderTimingSessionCountQuota;
@@ -3037,6 +3055,9 @@
static final String KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
QC_CONSTANT_PREFIX + "allowed_time_per_period_restricted_ms";
@VisibleForTesting
+ static final String KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+ QC_CONSTANT_PREFIX + "allowed_time_per_period_addition_installer_ms";
+ @VisibleForTesting
static final String KEY_IN_QUOTA_BUFFER_MS =
QC_CONSTANT_PREFIX + "in_quota_buffer_ms";
@VisibleForTesting
@@ -3169,6 +3190,8 @@
10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
10 * 60 * 1000L; // 10 minutes
+ private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+ 10 * 60 * 1000L; // 10 minutes
private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
30 * 1000L; // 30 seconds
// Legacy default window size for EXEMPTED bucket
@@ -3509,6 +3532,9 @@
*/
public long EJ_LIMIT_ADDITION_INSTALLER_MS = DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS;
+ public long ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+ DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS;
+
/**
* The period of time used to calculate expedited job sessions. Apps can only have expedited
* job sessions totalling EJ_LIMIT_<bucket>_MS within this period of time (without factoring
@@ -3603,6 +3629,7 @@
case KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS:
case KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS:
+ case KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS:
case KEY_IN_QUOTA_BUFFER_MS:
case KEY_MAX_EXECUTION_TIME_MS:
case KEY_WINDOW_SIZE_ACTIVE_MS:
@@ -3847,7 +3874,7 @@
KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, KEY_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS,
KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS, KEY_ALLOWED_TIME_PER_PERIOD_FREQUENT_MS,
KEY_ALLOWED_TIME_PER_PERIOD_RARE_MS, KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
- KEY_IN_QUOTA_BUFFER_MS,
+ KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS, KEY_IN_QUOTA_BUFFER_MS,
KEY_MAX_EXECUTION_TIME_MS,
KEY_WINDOW_SIZE_EXEMPTED_MS, KEY_WINDOW_SIZE_ACTIVE_MS,
KEY_WINDOW_SIZE_WORKING_MS,
@@ -3871,6 +3898,9 @@
ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS =
properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
DEFAULT_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS);
+ ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS =
+ properties.getLong(KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+ DEFAULT_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS);
IN_QUOTA_BUFFER_MS = properties.getLong(KEY_IN_QUOTA_BUFFER_MS,
DEFAULT_IN_QUOTA_BUFFER_MS);
MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS,
@@ -3995,6 +4025,18 @@
mBucketPeriodsMs[RESTRICTED_INDEX] = newRestrictedPeriodMs;
mShouldReevaluateConstraints = true;
}
+
+ if (Flags.additionalQuotaForSystemInstaller()) {
+ // The additions must be in the range
+ // [0 minutes, exempted window size - active limit].
+ long newAdditionInstallerMs = Math.max(0,
+ Math.min(mBucketPeriodsMs[EXEMPTED_INDEX] - newAllowedTimeExemptedMs,
+ ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS));
+ if (mAllowedTimePeriodAdditionaInstallerMs != newAdditionInstallerMs) {
+ mAllowedTimePeriodAdditionaInstallerMs = newAdditionInstallerMs;
+ mShouldReevaluateConstraints = true;
+ }
+ }
}
private void updateRateLimitingConstantsLocked() {
@@ -4159,6 +4201,8 @@
.println();
pw.print(KEY_ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS,
ALLOWED_TIME_PER_PERIOD_RESTRICTED_MS).println();
+ pw.print(KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+ ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS).println();
pw.print(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println();
pw.print(KEY_WINDOW_SIZE_EXEMPTED_MS, WINDOW_SIZE_EXEMPTED_MS).println();
pw.print(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println();
@@ -4335,6 +4379,11 @@
}
@VisibleForTesting
+ long getAllowedTimePeriodAdditionInstallerMs() {
+ return mAllowedTimePeriodAdditionaInstallerMs;
+ }
+
+ @VisibleForTesting
long getEjLimitAdditionSpecialMs() {
return mEjLimitAdditionSpecialMs;
}
@@ -4435,6 +4484,8 @@
+ ": " + Flags.enforceQuotaPolicyToFgsJobs());
pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS
+ ": " + Flags.enforceQuotaPolicyToTopStartedJobs());
+ pw.println(" " + Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER
+ + ": " + Flags.additionalQuotaForSystemInstaller());
pw.println();
pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis());
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 458c171..248f191 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1651,9 +1651,65 @@
/** @hide Similar to {@link OP_CONTROL_AUDIO}, but doesn't require capabilities. */
public static final int OP_CONTROL_AUDIO_PARTIAL = AppOpEnums.APP_OP_CONTROL_AUDIO_PARTIAL;
+ /**
+ * Access coarse eye tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_EYE_TRACKING_COARSE =
+ AppOpEnums.APP_OP_EYE_TRACKING_COARSE;
+
+ /**
+ * Access fine eye tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_EYE_TRACKING_FINE =
+ AppOpEnums.APP_OP_EYE_TRACKING_FINE;
+
+ /**
+ * Access face tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_FACE_TRACKING =
+ AppOpEnums.APP_OP_FACE_TRACKING;
+
+ /**
+ * Access hand tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_HAND_TRACKING =
+ AppOpEnums.APP_OP_HAND_TRACKING;
+
+ /**
+ * Access head tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_HEAD_TRACKING =
+ AppOpEnums.APP_OP_HEAD_TRACKING;
+
+ /**
+ * Access coarse scene tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_SCENE_UNDERSTANDING_COARSE =
+ AppOpEnums.APP_OP_SCENE_UNDERSTANDING_COARSE;
+
+ /**
+ * Access fine scene tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_SCENE_UNDERSTANDING_FINE =
+ AppOpEnums.APP_OP_SCENE_UNDERSTANDING_FINE;
+
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public static final int _NUM_OP = 156;
+ public static final int _NUM_OP = 163;
/**
* All app ops represented as strings.
@@ -1813,6 +1869,13 @@
OPSTR_WRITE_SYSTEM_PREFERENCES,
OPSTR_CONTROL_AUDIO,
OPSTR_CONTROL_AUDIO_PARTIAL,
+ OPSTR_EYE_TRACKING_COARSE,
+ OPSTR_EYE_TRACKING_FINE,
+ OPSTR_FACE_TRACKING,
+ OPSTR_HAND_TRACKING,
+ OPSTR_HEAD_TRACKING,
+ OPSTR_SCENE_UNDERSTANDING_COARSE,
+ OPSTR_SCENE_UNDERSTANDING_FINE,
})
public @interface AppOpString {}
@@ -2579,6 +2642,36 @@
/** @hide Access to a audio playback and control APIs without capability requirements */
public static final String OPSTR_CONTROL_AUDIO_PARTIAL = "android:control_audio_partial";
+ /** @hide Access coarse eye tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_EYE_TRACKING_COARSE = "android:eye_tracking_coarse";
+
+ /** @hide Access fine eye tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_EYE_TRACKING_FINE = "android:eye_tracking_fine";
+
+ /** @hide Access face tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_FACE_TRACKING = "android:face_tracking";
+
+ /** @hide Access hand tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_HAND_TRACKING = "android:hand_tracking";
+
+ /** @hide Access head tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_HEAD_TRACKING = "android:head_tracking";
+
+ /** @hide Access coarse scene tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_SCENE_UNDERSTANDING_COARSE =
+ "android:scene_understanding_coarse";
+
+ /** @hide Access fine scene tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_SCENE_UNDERSTANDING_FINE =
+ "android:scene_understanding_fine";
+
/** {@link #sAppOpsToNote} not initialized yet for this op */
private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
/** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2657,6 +2750,14 @@
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE,
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
+ // Android XR
+ android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_COARSE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_FINE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_FACE_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_HAND_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_HEAD_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_COARSE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_FINE : OP_NONE,
};
/**
@@ -3192,6 +3293,41 @@
"CONTROL_AUDIO").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
new AppOpInfo.Builder(OP_CONTROL_AUDIO_PARTIAL, OPSTR_CONTROL_AUDIO_PARTIAL,
"CONTROL_AUDIO_PARTIAL").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
+ new AppOpInfo.Builder(OP_EYE_TRACKING_COARSE, OPSTR_EYE_TRACKING_COARSE,
+ "EYE_TRACKING_COARSE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.EYE_TRACKING_COARSE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_EYE_TRACKING_FINE, OPSTR_EYE_TRACKING_FINE,
+ "EYE_TRACKING_FINE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.EYE_TRACKING_FINE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_FACE_TRACKING, OPSTR_FACE_TRACKING,
+ "FACE_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.FACE_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_HAND_TRACKING, OPSTR_HAND_TRACKING,
+ "HAND_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.HAND_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_HEAD_TRACKING, OPSTR_HEAD_TRACKING,
+ "HEAD_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.HEAD_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_COARSE, OPSTR_SCENE_UNDERSTANDING_COARSE,
+ "SCENE_UNDERSTANDING_COARSE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.SCENE_UNDERSTANDING_COARSE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_FINE, OPSTR_SCENE_UNDERSTANDING_FINE,
+ "SCENE_UNDERSTANDING_FINE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.SCENE_UNDERSTANDING_FINE : null)
+ .build(),
};
// The number of longs needed to form a full bitmask of app ops
@@ -3301,6 +3437,15 @@
}
/**
+ * Returns whether the provided {@code op} is a valid op code or not.
+ *
+ * @hide
+ */
+ public static boolean isValidOp(int op) {
+ return op >= 0 && op < sAppOpInfos.length;
+ }
+
+ /**
* @hide
*/
public static int strDebugOpToOp(String op) {
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index fa977c9..2daa52b 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -228,7 +228,7 @@
public AutomaticZenRule(Parcel source) {
enabled = source.readInt() == ENABLED;
if (source.readInt() == ENABLED) {
- name = getTrimmedString(source.readString());
+ name = getTrimmedString(source.readString8());
}
interruptionFilter = source.readInt();
conditionId = getTrimmedUri(source.readParcelable(null, android.net.Uri.class));
@@ -238,11 +238,11 @@
source.readParcelable(null, android.content.ComponentName.class));
creationTime = source.readLong();
mZenPolicy = source.readParcelable(null, ZenPolicy.class);
- mPkg = source.readString();
+ mPkg = source.readString8();
mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
mAllowManualInvocation = source.readBoolean();
mIconResId = source.readInt();
- mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+ mTriggerDescription = getTrimmedString(source.readString8(), MAX_DESC_LENGTH);
mType = source.readInt();
}
@@ -514,7 +514,7 @@
dest.writeInt(enabled ? ENABLED : DISABLED);
if (name != null) {
dest.writeInt(1);
- dest.writeString(name);
+ dest.writeString8(name);
} else {
dest.writeInt(0);
}
@@ -524,11 +524,11 @@
dest.writeParcelable(configurationActivity, 0);
dest.writeLong(creationTime);
dest.writeParcelable(mZenPolicy, 0);
- dest.writeString(mPkg);
+ dest.writeString8(mPkg);
dest.writeParcelable(mDeviceEffects, 0);
dest.writeBoolean(mAllowManualInvocation);
dest.writeInt(mIconResId);
- dest.writeString(mTriggerDescription);
+ dest.writeString8(mTriggerDescription);
dest.writeInt(mType);
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index eb9feb9..8af5b1b 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -189,6 +189,7 @@
* @param arguments Any additional arguments that were supplied when the
* instrumentation was started.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public void onCreate(Bundle arguments) {
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 719e438..1b71e73 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -25,6 +25,9 @@
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static android.graphics.drawable.Icon.TYPE_URI;
import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static java.util.Objects.requireNonNull;
@@ -6001,6 +6004,8 @@
contentView.setViewVisibility(p.mTextViewId, View.GONE);
contentView.setTextViewText(p.mTextViewId, null);
}
+
+ updateExpanderAlignment(contentView, p, hasSecondLine);
setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
// Update margins to leave space for the top line (but not for headerless views like
@@ -6010,12 +6015,29 @@
int margin = getContentMarginTop(mContext,
R.dimen.notification_2025_content_margin_top);
contentView.setViewLayoutMargin(R.id.notification_main_column,
- RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX);
+ RemoteViews.MARGIN_TOP, margin, COMPLEX_UNIT_PX);
}
return contentView;
}
+ private static void updateExpanderAlignment(RemoteViews contentView,
+ StandardTemplateParams p, boolean hasSecondLine) {
+ if (notificationsRedesignTemplates() && p.mHeaderless) {
+ if (!hasSecondLine) {
+ // If there's no text, let's center the expand button vertically to align things
+ // more nicely. This is handled separately for notifications that use a
+ // NotificationHeaderView, see NotificationHeaderView#centerTopLine.
+ contentView.setViewLayoutHeight(R.id.expand_button, MATCH_PARENT,
+ COMPLEX_UNIT_PX);
+ } else {
+ // Otherwise, just use the default height for the button to keep it top-aligned.
+ contentView.setViewLayoutHeight(R.id.expand_button, WRAP_CONTENT,
+ COMPLEX_UNIT_PX);
+ }
+ }
+ }
+
private static void setHeaderlessVerticalMargins(RemoteViews contentView,
StandardTemplateParams p, boolean hasSecondLine) {
if (Flags.notificationsRedesignTemplates() || !p.mHeaderless) {
@@ -9560,7 +9582,7 @@
int marginStart = res.getDimensionPixelSize(
R.dimen.notification_2025_content_margin_start);
contentView.setViewLayoutMargin(R.id.title,
- RemoteViews.MARGIN_START, marginStart, TypedValue.COMPLEX_UNIT_PX);
+ RemoteViews.MARGIN_START, marginStart, COMPLEX_UNIT_PX);
}
if (isLegacyHeaderless) {
// Collapsed legacy messaging style has a 1-line limit.
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 726999a..050ef23 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1282,6 +1282,10 @@
* delegate for (see {@link #canNotifyAsPackage(String)}), or it will not be returned. To query
* a channel as a notification delegate, call this method from a context created for that
* package (see {@link Context#createPackageContext(String, int)}).</p>
+ *
+ * <p>If a conversation channel with the given conversation id is not found, this method will
+ * instead return the parent channel with the given channel ID, or {@code null} if neither
+ * exists.</p>
*/
public @Nullable NotificationChannel getNotificationChannel(@NonNull String channelId,
@NonNull String conversationId) {
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 7a811a1..5b0cf115 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -132,7 +132,7 @@
per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS
-
+per-file Presentation.java = file:/services/core/java/com/android/server/wm/OWNERS
# Multitasking
per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/app/Presentation.java b/core/java/android/app/Presentation.java
index bdab39d..f39e2dd 100644
--- a/core/java/android/app/Presentation.java
+++ b/core/java/android/app/Presentation.java
@@ -20,6 +20,8 @@
import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
+
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -34,6 +36,8 @@
import android.view.Display;
import android.view.Gravity;
import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
@@ -277,6 +281,11 @@
@Override
public void show() {
super.show();
+
+ WindowInsetsController controller = getWindow().getInsetsController();
+ if (controller != null && enablePresentationForConnectedDisplays()) {
+ controller.hide(WindowInsets.Type.systemBars());
+ }
}
/**
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 67ade79..0085e4f 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -143,3 +143,10 @@
is_fixed_read_only: true
bug: "370928384"
}
+
+flag {
+ name: "device_aware_settings_override"
+ namespace: "virtual_devices"
+ description: "Settings override for virtual devices"
+ bug: "371801645"
+}
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index c4af871..bebca57 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1499,7 +1499,7 @@
}
@VisibleForTesting
- static final class DisplayListenerDelegate {
+ public static final class DisplayListenerDelegate {
public final DisplayListener mListener;
public volatile long mInternalEventFlagsMask;
@@ -1536,7 +1536,7 @@
}
@VisibleForTesting
- boolean isEventFilterExplicit() {
+ public boolean isEventFilterExplicit() {
return mIsEventFilterExplicit;
}
@@ -1892,7 +1892,7 @@
}
@VisibleForTesting
- CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
+ public CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
return mDisplayListeners;
}
}
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 953ee08..5b5360e 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -485,6 +485,9 @@
/**
* Returns the list of ContextHubInfo objects describing the available Context Hubs.
*
+ * To find the list of hubs that include all Hubs (including both Context Hubs and Vendor Hubs),
+ * use the {@link #getHubs()} method instead.
+ *
* @return the list of ContextHubInfo objects
*
* @see ContextHubInfo
@@ -499,8 +502,8 @@
}
/**
- * Returns the list of HubInfo objects describing the available hubs (including ContextHub and
- * VendorHub). This method is primarily used for debugging purposes as most clients care about
+ * Returns the list of HubInfo objects describing the available hubs (including Context Hubs and
+ * Vendor Hubs). This method is primarily used for debugging purposes as most clients care about
* endpoints and services more than hubs.
*
* @return the list of HubInfo objects
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 4cbd5be..1cf43d4 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2636,7 +2636,7 @@
enabled = source.readInt() == 1;
snoozing = source.readInt() == 1;
if (source.readInt() == 1) {
- name = source.readString();
+ name = source.readString8();
}
zenMode = source.readInt();
conditionId = source.readParcelable(null, android.net.Uri.class);
@@ -2644,18 +2644,18 @@
component = source.readParcelable(null, android.content.ComponentName.class);
configurationActivity = source.readParcelable(null, android.content.ComponentName.class);
if (source.readInt() == 1) {
- id = source.readString();
+ id = source.readString8();
}
creationTime = source.readLong();
if (source.readInt() == 1) {
- enabler = source.readString();
+ enabler = source.readString8();
}
zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
- pkg = source.readString();
+ pkg = source.readString8();
allowManualInvocation = source.readBoolean();
- iconResName = source.readString();
- triggerDescription = source.readString();
+ iconResName = source.readString8();
+ triggerDescription = source.readString8();
type = source.readInt();
userModifiedFields = source.readInt();
zenPolicyUserModifiedFields = source.readInt();
@@ -2703,7 +2703,7 @@
dest.writeInt(snoozing ? 1 : 0);
if (name != null) {
dest.writeInt(1);
- dest.writeString(name);
+ dest.writeString8(name);
} else {
dest.writeInt(0);
}
@@ -2714,23 +2714,23 @@
dest.writeParcelable(configurationActivity, 0);
if (id != null) {
dest.writeInt(1);
- dest.writeString(id);
+ dest.writeString8(id);
} else {
dest.writeInt(0);
}
dest.writeLong(creationTime);
if (enabler != null) {
dest.writeInt(1);
- dest.writeString(enabler);
+ dest.writeString8(enabler);
} else {
dest.writeInt(0);
}
dest.writeParcelable(zenPolicy, 0);
dest.writeParcelable(zenDeviceEffects, 0);
- dest.writeString(pkg);
+ dest.writeString8(pkg);
dest.writeBoolean(allowManualInvocation);
- dest.writeString(iconResName);
- dest.writeString(triggerDescription);
+ dest.writeString8(iconResName);
+ dest.writeString8(triggerDescription);
dest.writeInt(type);
dest.writeInt(userModifiedFields);
dest.writeInt(zenPolicyUserModifiedFields);
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index f58baff..4fc894c 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -789,6 +789,12 @@
in @nullable ImeTracker.Token statsToken);
/**
+ * Updates the currently animating insets types of a remote process.
+ */
+ @EnforcePermission("MANAGE_APP_TOKENS")
+ void updateDisplayWindowAnimatingTypes(int displayId, int animatingTypes);
+
+ /**
* Called to get the expected window insets.
*
* @return {@code true} if system bars are always consumed.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 1f8f0820..7d6d5a2 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -272,6 +272,15 @@
in @nullable ImeTracker.Token imeStatsToken);
/**
+ * Notifies WindowState what insets types are currently running within the Window.
+ * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
+ *
+ * @param window The window that is insets animaiton is running.
+ * @param animatingTypes Indicates the currently animating insets types.
+ */
+ oneway void updateAnimatingTypes(IWindow window, int animatingTypes);
+
+ /**
* Called when the system gesture exclusion has changed.
*/
oneway void reportSystemGestureExclusionChanged(IWindow window, in List<Rect> exclusionRects);
@@ -372,14 +381,4 @@
*/
oneway void notifyImeWindowVisibilityChangedFromClient(IWindow window, boolean visible,
in ImeTracker.Token statsToken);
-
- /**
- * Notifies WindowState whether inset animations are currently running within the Window.
- * This value is used by the server to vote for refresh rate.
- * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
- *
- * @param window The window that is insets animaiton is running.
- * @param running Indicates the insets animation state.
- */
- oneway void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running);
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 0d82acd..462c5c6 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -211,12 +211,12 @@
}
/**
- * Notifies when the state of running animation is changed. The state is either "running" or
- * "idle".
+ * Notifies when the insets types of running animation have changed. The animatingTypes
+ * contain all types, which have an ongoing animation.
*
- * @param running {@code true} if there is any animation running; {@code false} otherwise.
+ * @param animatingTypes the {@link InsetsType}s that are currently animating
*/
- default void notifyAnimationRunningStateChanged(boolean running) {}
+ default void updateAnimatingTypes(@InsetsType int animatingTypes) {}
/** @see ViewRootImpl#isHandlingPointerEvent */
default boolean isHandlingPointerEvent() {
@@ -665,6 +665,9 @@
/** Set of inset types which are requested visible which are reported to the host */
private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ /** Set of insets types which are currently animating */
+ private @InsetsType int mAnimatingTypes = 0;
+
/** Set of inset types that we have controls of */
private @InsetsType int mControllableTypes;
@@ -745,9 +748,10 @@
mFrame, mFromState, mToState, RESIZE_INTERPOLATOR,
ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(true);
+ mHost.updateAnimatingTypes(runner.getTypes());
}
mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
+ mAnimatingTypes |= runner.getTypes();
}
};
@@ -1564,9 +1568,8 @@
}
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
- if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(true);
- }
+ mAnimatingTypes |= runner.getTypes();
+ mHost.updateAnimatingTypes(mAnimatingTypes);
mRunningAnimations.add(new RunningAnimation(runner, animationType));
if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ useInsetsAnimationThread);
@@ -1827,7 +1830,7 @@
dispatchAnimationEnd(runningAnimation.runner.getAnimation());
} else {
if (Flags.refactorInsetsController()) {
- if (removedTypes == ime()
+ if ((removedTypes & ime()) != 0
&& control.getAnimationType() == ANIMATION_TYPE_HIDE) {
if (mHost != null) {
// if the (hide) animation is cancelled, the
@@ -1842,9 +1845,11 @@
break;
}
}
- if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(false);
+ if (removedTypes > 0) {
+ mAnimatingTypes &= ~removedTypes;
+ mHost.updateAnimatingTypes(mAnimatingTypes);
}
+
onAnimationStateChanged(removedTypes, false /* running */);
}
@@ -1969,14 +1974,6 @@
return animatingTypes;
}
- private @InsetsType int computeAnimatingTypes() {
- int animatingTypes = 0;
- for (int i = 0; i < mRunningAnimations.size(); i++) {
- animatingTypes |= mRunningAnimations.get(i).runner.getTypes();
- }
- return animatingTypes;
- }
-
/**
* Called when finishing setting requested visible types or finishing setting controls.
*
@@ -1989,7 +1986,7 @@
// report its requested visibility at the end of the animation, otherwise we would
// lose the leash, and it would disappear during the animation
// TODO(b/326377046) revisit this part and see if we can make it more general
- typesToReport = mRequestedVisibleTypes | (computeAnimatingTypes() & ime());
+ typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
} else {
typesToReport = mRequestedVisibleTypes;
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9498407..7fd7be8 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2540,11 +2540,12 @@
}
/**
- * Notify the when the running state of a insets animation changed.
+ * Notify the when the animating insets types have changed.
*/
@VisibleForTesting
- public void notifyInsetsAnimationRunningStateChanged(boolean running) {
+ public void updateAnimatingTypes(@InsetsType int animatingTypes) {
if (sToolkitSetFrameRateReadOnlyFlagValue) {
+ boolean running = animatingTypes != 0;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.instant(Trace.TRACE_TAG_VIEW,
TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)",
@@ -2552,7 +2553,7 @@
}
mInsetsAnimationRunning = running;
try {
- mWindowSession.notifyInsetsAnimationRunningStateChanged(mWindow, running);
+ mWindowSession.updateAnimatingTypes(mWindow, animatingTypes);
} catch (RemoteException e) {
}
}
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index 889acca4..8954df6 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -171,6 +171,13 @@
}
@Override
+ public void updateAnimatingTypes(@WindowInsets.Type.InsetsType int animatingTypes) {
+ if (mViewRoot != null) {
+ mViewRoot.updateAnimatingTypes(animatingTypes);
+ }
+ }
+
+ @Override
public boolean hasAnimationCallbacks() {
if (mViewRoot.mView == null) {
return false;
@@ -275,13 +282,6 @@
}
@Override
- public void notifyAnimationRunningStateChanged(boolean running) {
- if (mViewRoot != null) {
- mViewRoot.notifyInsetsAnimationRunningStateChanged(running);
- }
- }
-
- @Override
public boolean isHandlingPointerEvent() {
return mViewRoot != null && mViewRoot.isHandlingPointerEvent();
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 24647f4..196ae5e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -625,6 +625,12 @@
int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000
/**
+ * Transition flag: Indicates that aod is showing hidden by entering doze
+ * @hide
+ */
+ int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000
+
+ /**
* @hide
*/
@IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -643,6 +649,7 @@
TRANSIT_FLAG_KEYGUARD_OCCLUDING,
TRANSIT_FLAG_KEYGUARD_UNOCCLUDING,
TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH,
+ TRANSIT_FLAG_AOD_APPEARING,
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionFlags {}
@@ -659,7 +666,8 @@
(TRANSIT_FLAG_KEYGUARD_GOING_AWAY
| TRANSIT_FLAG_KEYGUARD_APPEARING
| TRANSIT_FLAG_KEYGUARD_OCCLUDING
- | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING);
+ | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING
+ | TRANSIT_FLAG_AOD_APPEARING);
/**
* Remove content mode: Indicates remove content mode is currently not defined.
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 72a595d..0a86ff8 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -597,6 +597,11 @@
}
@Override
+ public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+ // NO-OP
+ }
+
+ @Override
public void reportSystemGestureExclusionChanged(android.view.IWindow window,
List<Rect> exclusionRects) {
}
@@ -679,11 +684,6 @@
@NonNull ImeTracker.Token statsToken) {
}
- @Override
- public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
- // NO-OP
- }
-
void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder();
IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 49a11ca..80a9cbc 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -235,6 +235,14 @@
}
flag {
+ name: "request_rectangle_with_source"
+ namespace: "accessibility"
+ description: "Request rectangle on screen with source parameter"
+ bug: "391877896"
+ is_exported: true
+}
+
+flag {
name: "restore_a11y_secure_settings_on_hsum_device"
namespace: "accessibility"
description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user"
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index aa0111a..60178cd 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -225,6 +225,7 @@
PHASE_SERVER_UPDATE_CLIENT_VISIBILITY,
PHASE_WM_DISPLAY_IME_CONTROLLER_SET_IME_REQUESTED_VISIBLE,
PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES,
+ PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED,
})
@Retention(RetentionPolicy.SOURCE)
@interface Phase {}
@@ -445,6 +446,9 @@
/** The control target reported its requestedVisibleTypes back to WindowManagerService. */
int PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES =
ImeProtoEnums.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES;
+ /** The requestedVisibleTypes have not been changed, so this request is not continued. */
+ int PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED =
+ ImeProtoEnums.PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED;
/**
* Called when an IME request is started.
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 16f4114..c81c2bb 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -196,3 +196,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "report_animating_insets_types"
+ namespace: "input_method"
+ description: "Adding animating insets types and report IME visibility at the beginning of hiding"
+ bug: "393049691"
+}
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 118edc2..fa7b74f 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -242,7 +242,7 @@
@Override
public void onNullBinding(ComponentName name) {
- enqueueDeferredUnbindServiceMessage();
+ unbindNow();
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 99fe0cb..5e828ba 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5211,7 +5211,11 @@
*/
@Nullable
public String getFontVariationSettings() {
- return mTextPaint.getFontVariationSettings();
+ if (Flags.typefaceRedesignReadonly()) {
+ return mTextPaint.getFontVariationOverride();
+ } else {
+ return mTextPaint.getFontVariationSettings();
+ }
}
/**
@@ -5567,10 +5571,10 @@
Math.clamp(400 + mFontWeightAdjustment,
FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
}
- mTextPaint.setFontVariationSettings(
+ mTextPaint.setFontVariationOverride(
FontVariationAxis.toFontVariationSettings(axes));
} else {
- mTextPaint.setFontVariationSettings(fontVariationSettings);
+ mTextPaint.setFontVariationOverride(fontVariationSettings);
}
effective = true;
} else {
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 4aeedbb..42bf6d1 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -97,6 +97,8 @@
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
true),
ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
+ ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
+ Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 2e36e9a..684f320 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -811,4 +811,13 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
+flag {
+ name: "enable_drag_to_desktop_incoming_transitions_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables bugfix handling incoming transitions during the DragToDesktop transition."
+ bug: "397135730"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java
index ad73294..4663d62 100644
--- a/core/java/com/android/internal/content/PackageMonitor.java
+++ b/core/java/com/android/internal/content/PackageMonitor.java
@@ -36,6 +36,7 @@
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
import com.android.internal.os.BackgroundThread;
import java.lang.ref.WeakReference;
@@ -46,6 +47,7 @@
* Helper class for monitoring the state of packages: adding, removing,
* updating, and disappearing and reappearing on the SD card.
*/
+@WeaklyReferencedCallback
public abstract class PackageMonitor extends android.content.BroadcastReceiver {
static final String TAG = "PackageMonitor";
diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
index b19967a..0e6eb18 100644
--- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
+++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java
@@ -80,7 +80,11 @@
(Process.myUid() == Process.SYSTEM_UID) ? DeviceProtos.parsedFlagsProtoPaths()
: Arrays.asList(DeviceProtos.PATHS);
for (String fileName : defaultFlagProtoFiles) {
- try (var inputStream = new FileInputStream(fileName)) {
+ final File protoFile = new File(fileName);
+ if (!protoFile.isFile() || !protoFile.canRead()) {
+ continue;
+ }
+ try (var inputStream = new FileInputStream(protoFile)) {
loadAconfigDefaultValues(inputStream.readAllBytes());
} catch (IOException e) {
Slog.w(LOG_TAG, "Failed to read Aconfig values from " + fileName, e);
@@ -120,6 +124,9 @@
final var settingsFile = new File(Environment.getUserSystemDirectory(0),
"settings_config.xml");
+ if (!settingsFile.isFile() || !settingsFile.canRead()) {
+ return;
+ }
try (var inputStream = new FileInputStream(settingsFile)) {
TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
if (parser.next() != XmlPullParser.END_TAG && "settings".equals(parser.getName())) {
@@ -186,7 +193,7 @@
}
}
} catch (IOException | XmlPullParserException e) {
- Slog.e(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e);
+ Slog.w(LOG_TAG, "Failed to read Aconfig values from settings_config.xml", e);
}
}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
index 69c0480..7ee22f3 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
@@ -157,7 +157,7 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- sForInternedString.parcel(this.name, dest, flags);
+ dest.writeString(this.name);
dest.writeInt(this.getIcon());
dest.writeInt(this.getLabelRes());
dest.writeCharSequence(this.getNonLocalizedLabel());
@@ -175,7 +175,7 @@
// We use the boot classloader for all classes that we load.
final ClassLoader boot = Object.class.getClassLoader();
//noinspection ConstantConditions
- this.name = sForInternedString.unparcel(in);
+ this.name = in.readString();
this.icon = in.readInt();
this.labelRes = in.readInt();
this.nonLocalizedLabel = in.readCharSequence();
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 7018ebc..5a180d7 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -82,7 +82,7 @@
* Notify system UI the immersive mode changed. This shall be removed when client immersive is
* enabled.
*/
- void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, int windowType);
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 9acb242..a1961ae 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -268,6 +268,9 @@
72dp (content margin) - 12dp (action padding) - 4dp (button inset) -->
<dimen name="notification_2025_actions_margin_start">56dp</dimen>
+ <!-- Notification action button text size -->
+ <dimen name="notification_2025_action_text_size">16sp</dimen>
+
<!-- The margin on the end of most content views (ignores the expander) -->
<dimen name="notification_content_margin_end">16dp</dimen>
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 8ef105f..de5f0ff 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -177,8 +177,10 @@
@RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED)
public void test_refreshRateRegistration_implicitRRCallbacksEnabled()
throws RemoteException {
+ DisplayManager.DisplayListener displayListener1 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
// Subscription without supplied events doesn't subscribe to RR events
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ mDisplayManagerGlobal.registerDisplayListener(displayListener1, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ false);
Mockito.verify(mDisplayManager)
@@ -187,7 +189,9 @@
// After registering to refresh rate changes, subscription without supplied events subscribe
// to RR events
mDisplayManagerGlobal.registerForRefreshRateChanges();
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ DisplayManager.DisplayListener displayListener2 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
+ mDisplayManagerGlobal.registerDisplayListener(displayListener2, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ false);
Mockito.verify(mDisplayManager)
@@ -203,7 +207,9 @@
}
// Subscription to RR when events are supplied doesn't happen
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ DisplayManager.DisplayListener displayListener3 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
+ mDisplayManagerGlobal.registerDisplayListener(displayListener3, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ true);
Mockito.verify(mDisplayManager)
@@ -214,7 +220,6 @@
int subscribedListenersCount = 0;
int nonSubscribedListenersCount = 0;
for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) {
-
if (delegate.isEventFilterExplicit()) {
assertEquals(ALL_DISPLAY_EVENTS, delegate.mInternalEventFlagsMask);
nonSubscribedListenersCount++;
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 4516e9c..af87af0 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -1195,6 +1195,23 @@
});
}
+ @Test
+ public void testAnimatingTypes() throws Exception {
+ prepareControls();
+
+ final int types = navigationBars() | statusBars();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ clearInvocations(mTestHost);
+ mController.hide(types);
+ // quickly jump to final state by cancelling it.
+ mController.cancelExistingAnimations();
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mTestHost, times(1)).updateAnimatingTypes(eq(types));
+ verify(mTestHost, times(1)).updateAnimatingTypes(eq(0) /* animatingTypes */);
+ }
+
private void waitUntilNextFrame() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index c40137f..f5d1e7a 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1054,7 +1054,7 @@
ViewRootImpl viewRootImpl = mView.getViewRootImpl();
sInstrumentation.runOnMainSync(() -> {
mView.invalidate();
- viewRootImpl.notifyInsetsAnimationRunningStateChanged(true);
+ viewRootImpl.updateAnimatingTypes(Type.systemBars());
mView.invalidate();
});
sInstrumentation.waitForIdleSync();
diff --git a/graphics/java/android/graphics/pdf/OWNERS b/graphics/java/android/graphics/pdf/OWNERS
index 057dc0d..24557b4 100644
--- a/graphics/java/android/graphics/pdf/OWNERS
+++ b/graphics/java/android/graphics/pdf/OWNERS
@@ -4,4 +4,3 @@
djsollen@google.com
sumir@google.com
svetoslavganov@android.com
-svetoslavganov@google.com
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 3aefcd5..9087da3 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -552,7 +552,9 @@
private fun createAppBubble(usePendingIntent: Boolean = false): Bubble {
val target = Intent(context, TestActivity::class.java)
+ val component = ComponentName(context, TestActivity::class.java)
target.setPackage(context.packageName)
+ target.setComponent(component)
if (usePendingIntent) {
// Robolectric doesn't seem to play nice with PendingIntents, have to mock it.
val pendingIntent = mock<PendingIntent>()
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index 7b583137..14c152102 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -19,7 +19,9 @@
import android.animation.AnimatorTestRule
import android.content.Context
import android.content.pm.LauncherApps
+import android.graphics.Insets
import android.graphics.PointF
+import android.graphics.Rect
import android.os.Handler
import android.os.UserManager
import android.view.IWindowManager
@@ -61,6 +63,7 @@
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
@@ -80,6 +83,10 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleBarLayerViewTest {
+ companion object {
+ const val SCREEN_WIDTH = 2000
+ const val SCREEN_HEIGHT = 1000
+ }
@get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
@@ -111,6 +118,16 @@
bubblePositioner = BubblePositioner(context, windowManager)
bubblePositioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ DeviceConfig(
+ windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
+ isLargeScreen = true,
+ isSmallTablet = false,
+ isLandscape = true,
+ isRtl = false,
+ insets = Insets.of(10, 20, 30, 40)
+ )
+ bubblePositioner.update(deviceConfig)
testBubblesList = mutableListOf()
val bubbleData = mock<BubbleData>()
@@ -313,6 +330,48 @@
assertThat(uiEventLoggerFake.logs[0]).hasBubbleInfo(bubble)
}
+ @Test
+ fun testUpdateExpandedView_updateLocation() {
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ val bubble = createBubble("first")
+
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.showExpandedView(bubble)
+ }
+ waitForExpandedViewAnimation()
+
+ val previousX = bubble.bubbleBarExpandedView!!.x
+
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.updateExpandedView()
+ }
+
+ assertThat(bubble.bubbleBarExpandedView!!.x).isNotEqualTo(previousX)
+ }
+
+ @Test
+ fun testUpdatedExpandedView_updateLocation_skipWhileAnimating() {
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ val bubble = createBubble("first")
+
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.showExpandedView(bubble)
+ }
+ waitForExpandedViewAnimation()
+
+ val previousX = bubble.bubbleBarExpandedView!!.x
+ bubble.bubbleBarExpandedView!!.isAnimating = true
+
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.updateExpandedView()
+ }
+
+ // Expanded view is not updated while animating
+ assertThat(bubble.bubbleBarExpandedView!!.x).isEqualTo(previousX)
+ }
+
private fun createBubble(key: String): Bubble {
val bubbleTaskView = FakeBubbleTaskViewFactory(context, mainExecutor).create()
val bubbleBarExpandedView =
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index d50a14c..c2aa146 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -79,7 +79,7 @@
android:layout_marginEnd="4dp">
<Button
- android:layout_width="94dp"
+ android:layout_width="108dp"
android:layout_height="60dp"
android:id="@+id/maximize_menu_size_toggle_button"
style="?android:attr/buttonBarButtonStyle"
@@ -126,7 +126,7 @@
<Button
android:id="@+id/maximize_menu_snap_left_button"
style="?android:attr/buttonBarButtonStyle"
- android:layout_width="41dp"
+ android:layout_width="48dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:layout_marginEnd="4dp"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
@@ -137,7 +137,7 @@
<Button
android:id="@+id/maximize_menu_snap_right_button"
style="?android:attr/buttonBarButtonStyle"
- android:layout_width="41dp"
+ android:layout_width="48dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
android:importantForAccessibility="yes"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index a0c68ad..32660e8 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -618,6 +618,15 @@
<!-- The vertical inset to apply to the app chip's ripple drawable -->
<dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen>
+ <!-- The corner radius of the windowing actions pill buttons's ripple drawable -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_radius">24dp</dimen>
+ <!-- The horizontal/vertical inset to apply to the ripple drawable effect of windowing
+ actions pill central buttons -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_base">2dp</dimen>
+ <!-- The horizontal/vertical vertical inset to apply to the ripple drawable effect of windowing
+ actions pill edge buttons -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_shift">4dp</dimen>
+
<!-- The corner radius of the minimize button's ripple drawable -->
<dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen>
<!-- The vertical inset to apply to the minimize button's ripple drawable -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 00c446c..7acad50 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -374,7 +374,7 @@
* of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM.
*/
public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) {
- if (!Flags.enterDesktopByDefaultOnFreeformDisplays()) {
+ if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
return false;
}
return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
@@ -387,7 +387,7 @@
* screen.
*/
public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
- if (!Flags.enableDragToMaximize()) {
+ if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) {
return false;
}
return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 5355138..26c3626 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -147,10 +147,9 @@
/** To be overridden by subclasses to adjust the animation surface change. */
void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
// Update the surface position and alpha.
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && mAnimation.getExtensionEdges() != 0x0
+ if (mAnimation.getExtensionEdges() != 0x0
&& !(mChange.hasFlags(FLAG_TRANSLUCENT)
- && mChange.getActivityComponent() != null)) {
+ && mChange.getActivityComponent() != null)) {
// Extend non-translucent activities
t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges());
}
@@ -189,8 +188,7 @@
@CallSuper
void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
onAnimationUpdate(t, mAnimation.getDuration());
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && mAnimation.getExtensionEdges() != 0x0) {
+ if (mAnimation.getExtensionEdges() != 0x0) {
t.setEdgeExtensionEffect(mLeash, /* edge */ 0);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index c3e783d..85b7ac2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -20,11 +20,9 @@
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
-import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
@@ -143,10 +141,6 @@
// ending states.
prepareForJumpCut(info, startTransaction);
} else {
- if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
- addEdgeExtensionIfNeeded(startTransaction, finishTransaction,
- postStartTransactionCallbacks, adapters);
- }
addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
duration = Math.max(duration, adapter.getDurationHint());
@@ -329,34 +323,6 @@
}
}
- /** Adds edge extension to the surfaces that have such an animation property. */
- private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
- @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
- for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
- final Animation animation = adapter.mAnimation;
- if (animation.getExtensionEdges() == 0) {
- continue;
- }
- if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)
- && adapter.mChange.getActivityComponent() != null) {
- // Skip edge extension for translucent activity.
- continue;
- }
- final TransitionInfo.Change change = adapter.mChange;
- if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
- // Need to screenshot after startTransaction is applied otherwise activity
- // may not be visible or ready yet.
- postStartTransactionCallbacks.add(
- t -> edgeExtendWindow(change, animation, t, finishTransaction));
- } else {
- // Can screenshot now (before startTransaction is applied)
- edgeExtendWindow(change, animation, startTransaction, finishTransaction);
- }
- }
- }
-
/** Adds background color to the transition if any animation has such a property. */
private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 313d151..d948928 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -364,7 +364,7 @@
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
return new Bubble(intent,
user,
- /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user),
+ /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
mainExecutor, bgExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 29837dc..677c21c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -473,7 +473,7 @@
/** Updates the expanded view size and position. */
public void updateExpandedView() {
- if (mExpandedView == null || mExpandedBubble == null) return;
+ if (mExpandedView == null || mExpandedBubble == null || mExpandedView.isAnimating()) return;
boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(),
isOverflowExpanded, mTempRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index df82091..dd2050a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -461,6 +461,14 @@
}
}
+ private void setAnimating(boolean imeAnimationOngoing) {
+ int animatingTypes = imeAnimationOngoing ? WindowInsets.Type.ime() : 0;
+ try {
+ mWmService.updateDisplayWindowAnimatingTypes(mDisplayId, animatingTypes);
+ } catch (RemoteException e) {
+ }
+ }
+
private int imeTop(float surfaceOffset, float surfacePositionY) {
// surfaceOffset is already offset by the surface's top inset, so we need to subtract
// the top inset so that the return value is in screen coordinates.
@@ -619,6 +627,9 @@
+ imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY)
+ " showing:" + (mAnimationDirection == DIRECTION_SHOW));
}
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ setAnimating(true);
+ }
int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY),
imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW,
isFloating, t);
@@ -666,6 +677,8 @@
}
if (!android.view.inputmethod.Flags.refactorInsetsController()) {
dispatchEndPositioning(mDisplayId, mCancelled, t);
+ } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ setAnimating(false);
}
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
ImeTracker.forLogging().onProgress(mStatsToken,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 59acdc5..48fadc0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -100,6 +100,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
+import com.android.wm.shell.desktopmode.DragToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver;
@@ -770,7 +771,8 @@
DesksOrganizer desksOrganizer,
DesksTransitionObserver desksTransitionObserver,
UserProfileContexts userProfileContexts,
- DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ DesktopModeCompatPolicy desktopModeCompatPolicy,
+ DragToDisplayTransitionHandler dragToDisplayTransitionHandler) {
return new DesktopTasksController(
context,
shellInit,
@@ -808,7 +810,8 @@
desksOrganizer,
desksTransitionObserver,
userProfileContexts,
- desktopModeCompatPolicy);
+ desktopModeCompatPolicy,
+ dragToDisplayTransitionHandler);
}
@WMSingleton
@@ -934,6 +937,12 @@
@WMSingleton
@Provides
+ static DragToDisplayTransitionHandler provideDragToDisplayTransitionHandler() {
+ return new DragToDisplayTransitionHandler();
+ }
+
+ @WMSingleton
+ @Provides
static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
Context context,
Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index c9a63ff..e89aafe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -27,9 +27,9 @@
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
@@ -47,31 +47,9 @@
) {
fun refreshDisplayWindowingMode() {
- if (!Flags.enableDisplayWindowingModeSwitching()) return
- // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
- val isExtendedDisplayEnabled =
- 0 !=
- Settings.Global.getInt(
- context.contentResolver,
- DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
- 0,
- )
- if (!isExtendedDisplayEnabled) {
- // No action needed in mirror or projected mode.
- return
- }
+ if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
- val hasNonDefaultDisplay =
- rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId ->
- displayId != DEFAULT_DISPLAY
- }
- val targetDisplayWindowingMode =
- if (hasNonDefaultDisplay) {
- WINDOWING_MODE_FREEFORM
- } else {
- // Use the default display windowing mode when no non-default display.
- windowManager.getWindowingMode(DEFAULT_DISPLAY)
- }
+ val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay()
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
@@ -111,6 +89,25 @@
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
+ private fun getTargetWindowingModeForDefaultDisplay(): Int {
+ if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
+ return WINDOWING_MODE_FREEFORM
+ }
+ return windowManager.getWindowingMode(DEFAULT_DISPLAY)
+ }
+
+ // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
+ private fun isExtendedDisplayEnabled() =
+ 0 !=
+ Settings.Global.getInt(
+ context.contentResolver,
+ DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+ 0,
+ )
+
+ private fun hasExternalDisplay() =
+ rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 04e609e..03423ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -1007,6 +1007,21 @@
fun saveBoundsBeforeFullImmersive(taskId: Int, bounds: Rect) =
boundsBeforeFullImmersiveByTaskId.set(taskId, Rect(bounds))
+ /** Returns the current state of the desktop, formatted for usage by remote clients. */
+ fun getDeskDisplayStateForRemote(): Array<DisplayDeskState> =
+ desktopData
+ .desksSequence()
+ .groupBy { it.displayId }
+ .map { (displayId, desks) ->
+ val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId
+ DisplayDeskState().apply {
+ this.displayId = displayId
+ this.activeDeskId = activeDeskId ?: INVALID_DESK_ID
+ this.deskIds = desks.map { it.deskId }.toIntArray()
+ }
+ }
+ .toTypedArray()
+
/** TODO: b/389960283 - consider updating only the changing desks. */
private fun updatePersistentRepository(displayId: Int) {
val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 2d9aea0..fca5084 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -205,6 +205,7 @@
private val desksTransitionObserver: DesksTransitionObserver,
private val userProfileContexts: UserProfileContexts,
private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
+ private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -811,7 +812,7 @@
willExitDesktop(
triggerTaskId = taskInfo.taskId,
displayId = displayId,
- forceToFullscreen = false,
+ forceExitDesktop = false,
)
taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
val desktopExitRunnable =
@@ -884,7 +885,7 @@
snapEventHandler.removeTaskIfTiled(displayId, taskId)
taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
- val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false)
+ val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
val desktopExitRunnable =
performDesktopExitCleanUp(
wct = wct,
@@ -977,7 +978,7 @@
) {
logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
val wct = WindowContainerTransaction()
- val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true)
+ val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceExitDesktop = true)
val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop)
// We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
@@ -996,7 +997,14 @@
deactivationRunnable?.invoke(transition)
// handles case where we are moving to full screen without closing all DW tasks.
- if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) {
+ if (
+ !taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
+ // This callback is already invoked by |addMoveToFullscreenChanges| when one of these
+ // flags is enabled.
+ &&
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
+ !Flags.enableDesktopWindowingPip()
+ ) {
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION
)
@@ -1893,16 +1901,24 @@
private fun willExitDesktop(
triggerTaskId: Int,
displayId: Int,
- forceToFullscreen: Boolean,
+ forceExitDesktop: Boolean,
): Boolean {
+ if (
+ forceExitDesktop &&
+ (Flags.enableDesktopWindowingPip() ||
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue)
+ ) {
+ // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
+ // explicitly going fullscreen, so there's no point in checking the desktop state.
+ return true
+ }
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
return false
}
} else if (
Flags.enableDesktopWindowingPip() &&
- taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
- !forceToFullscreen
+ taskRepository.isMinimizedPipPresentInDisplay(displayId)
) {
return false
} else {
@@ -2295,7 +2311,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
wct.reorder(task.token, true)
@@ -2328,7 +2344,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
return wct
@@ -2433,7 +2449,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
}
@@ -2471,7 +2487,7 @@
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
}
@@ -2701,10 +2717,12 @@
/**
* Adds split screen changes to a transaction. Note that bounds are not reset here due to
* animation; see {@link onDesktopSplitSelectAnimComplete}
- *
- * TODO: b/394268248 - desk needs to be deactivated.
*/
- private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
+ private fun addMoveToSplitChanges(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo,
+ deskId: Int?,
+ ): RunOnTransitStart? {
// This windowing mode is to get the transition animation started; once we complete
// split select, we will change windowing mode to undefined and inherit from split stage.
// Going to undefined here causes task to flicker to the top left.
@@ -2714,11 +2732,12 @@
// want it overridden in multi-window.
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
- performDesktopExitCleanupIfNeeded(
+ return performDesktopExitCleanupIfNeeded(
taskId = taskInfo.taskId,
displayId = taskInfo.displayId,
+ deskId = deskId,
wct = wct,
- forceToFullscreen = false,
+ forceToFullscreen = true,
shouldEndUpAtHome = false,
)
}
@@ -2950,14 +2969,21 @@
}
dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState)
} else {
+ val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+ logV("Split requested for task=%d in desk=%d", taskInfo.taskId, deskId)
val wct = WindowContainerTransaction()
- addMoveToSplitChanges(wct, taskInfo)
- splitScreenController.requestEnterSplitSelect(
- taskInfo,
- wct,
- if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
- taskInfo.configuration.windowConfiguration.bounds,
- )
+ val runOnTransitStart = addMoveToSplitChanges(wct, taskInfo, deskId)
+ val transition =
+ splitScreenController.requestEnterSplitSelect(
+ taskInfo,
+ wct,
+ if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT
+ else SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ taskInfo.configuration.windowConfiguration.bounds,
+ )
+ if (transition != null) {
+ runOnTransitStart?.invoke(transition)
+ }
}
}
}
@@ -3163,25 +3189,24 @@
val wct = WindowContainerTransaction()
wct.setBounds(taskInfo.token, destinationBounds)
- // TODO: b/362720497 - reparent to a specific desk within the target display.
- // Reparent task if it has been moved to a new display.
- if (Flags.enableConnectedDisplaysWindowDrag()) {
- val newDisplayId = motionEvent.getDisplayId()
- if (newDisplayId != taskInfo.getDisplayId()) {
- val displayAreaInfo =
- rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
- if (displayAreaInfo == null) {
- logW(
- "Task reparent cannot find DisplayAreaInfo for displayId=%d",
- newDisplayId,
- )
- } else {
- wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
- }
+ val newDisplayId = motionEvent.getDisplayId()
+ val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
+ val isCrossDisplayDrag =
+ Flags.enableConnectedDisplaysWindowDrag() &&
+ newDisplayId != taskInfo.getDisplayId() &&
+ displayAreaInfo != null
+ val handler =
+ if (isCrossDisplayDrag) {
+ dragToDisplayTransitionHandler
+ } else {
+ null
}
+ if (isCrossDisplayDrag) {
+ // TODO: b/362720497 - reparent to a specific desk within the target display.
+ wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
}
- transitions.startTransition(TRANSIT_CHANGE, wct, null)
+ transitions.startTransition(TRANSIT_CHANGE, wct, handler)
releaseVisualIndicator()
}
@@ -3603,27 +3628,11 @@
controller,
{ c ->
run {
- c.taskRepository.addDeskChangeListener(
- deskChangeListener,
- c.mainExecutor,
- )
- c.taskRepository.addVisibleTasksListener(
- visibleTasksListener,
- c.mainExecutor,
- )
- c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
- c.desktopModeEnterExitTransitionListener =
- desktopModeEntryExitTransitionListener
+ syncInitialState(c)
+ registerListeners(c)
}
},
- { c ->
- run {
- c.taskRepository.removeDeskChangeListener(deskChangeListener)
- c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
- c.taskbarDesktopTaskListener = null
- c.desktopModeEnterExitTransitionListener = null
- }
- },
+ { c -> run { unregisterListeners(c) } },
)
}
@@ -3719,6 +3728,31 @@
c.startLaunchIntentTransition(intent, options, displayId)
}
}
+
+ private fun syncInitialState(c: DesktopTasksController) {
+ remoteListener.call { l ->
+ // TODO: b/393962589 - implement desks limit.
+ val canCreateDesks = true
+ l.onListenerConnected(
+ c.taskRepository.getDeskDisplayStateForRemote(),
+ canCreateDesks,
+ )
+ }
+ }
+
+ private fun registerListeners(c: DesktopTasksController) {
+ c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor)
+ c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor)
+ c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+ c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener
+ }
+
+ private fun unregisterListeners(c: DesktopTasksController) {
+ c.taskRepository.removeDeskChangeListener(deskChangeListener)
+ c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
+ c.taskbarDesktopTaskListener = null
+ c.desktopModeEnterExitTransitionListener = null
+ }
}
private fun logV(msg: String, vararg arguments: Any?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
new file mode 100644
index 0000000..d51576a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.transition.Transitions
+
+/** Handles the transition to drag a window to another display by dragging the caption. */
+class DragToDisplayTransitionHandler : Transitions.TransitionHandler {
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? {
+ return null
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ for (change in info.changes) {
+ val sc = change.leash
+ val endBounds = change.endAbsBounds
+ val endPosition = change.endRelOffset
+ startTransaction
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+ finishTransaction
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+ }
+
+ startTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ return true
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index ba30d92..10b23fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -54,6 +54,7 @@
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
@@ -580,10 +581,13 @@
* @param wct transaction to apply if this is a valid request
* @param splitPosition the split position this task should move to
* @param taskBounds current freeform bounds of the task entering split
+ *
+ * @return the token of the transition that started as a result of entering split select.
*/
- public void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+ @Nullable
+ public IBinder requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
WindowContainerTransaction wct, int splitPosition, Rect taskBounds) {
- mStageCoordinator.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds);
+ return mStageCoordinator.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds);
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 77a7c54..0438d16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -122,6 +122,7 @@
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.widget.Toast;
+import android.window.DesktopExperienceFlags;
import android.window.DisplayAreaInfo;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
@@ -219,6 +220,7 @@
private final Context mContext;
private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
private final Set<SplitScreen.SplitSelectListener> mSelectListeners = new HashSet<>();
+ private final Transitions mTransitions;
private final DisplayController mDisplayController;
private final DisplayImeController mDisplayImeController;
private final DisplayInsetsController mDisplayInsetsController;
@@ -419,6 +421,7 @@
iconProvider,
mWindowDecorViewModel, STAGE_TYPE_SIDE);
}
+ mTransitions = transitions;
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
@@ -455,6 +458,7 @@
mTaskOrganizer = taskOrganizer;
mMainStage = mainStage;
mSideStage = sideStage;
+ mTransitions = transitions;
mDisplayController = displayController;
mDisplayImeController = displayImeController;
mDisplayInsetsController = displayInsetsController;
@@ -660,16 +664,22 @@
return mLogger;
}
- void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+ @Nullable
+ IBinder requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
WindowContainerTransaction wct, int splitPosition, Rect taskBounds) {
boolean enteredSplitSelect = false;
for (SplitScreen.SplitSelectListener listener : mSelectListeners) {
enteredSplitSelect |= listener.onRequestEnterSplitSelect(taskInfo, splitPosition,
taskBounds);
}
- if (enteredSplitSelect) {
- mTaskOrganizer.applyTransaction(wct);
+ if (!enteredSplitSelect) {
+ return null;
}
+ if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) {
+ mTaskOrganizer.applyTransaction(wct);
+ return null;
+ }
+ return mTransitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null);
}
void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index e9c6ade..3652a16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -67,7 +67,6 @@
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
import static com.android.wm.shell.transition.DefaultSurfaceAnimator.buildSurfaceAnimation;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange;
@@ -543,21 +542,9 @@
backgroundColorForTransition);
if (!isTask && a.getExtensionEdges() != 0x0) {
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
- startTransaction.setEdgeExtensionEffect(
- change.getLeash(), a.getExtensionEdges());
- finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
- } else {
- if (!TransitionUtil.isOpeningType(mode)) {
- // Can screenshot now (before startTransaction is applied)
- edgeExtendWindow(change, a, startTransaction, finishTransaction);
- } else {
- // Need to screenshot after startTransaction is applied otherwise
- // activity may not be visible or ready yet.
- postStartTransactionCallbacks
- .add(t -> edgeExtendWindow(change, a, t, finishTransaction));
- }
- }
+ startTransaction.setEdgeExtensionEffect(
+ change.getLeash(), a.getExtensionEdges());
+ finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
}
final Rect clipRect = TransitionUtil.isClosingType(mode)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index 7984bce..edfb560 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -26,7 +26,6 @@
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
-import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -39,20 +38,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.view.animation.Animation;
-import android.view.animation.Transformation;
-import android.window.ScreenCapture;
import android.window.TransitionInfo;
import com.android.internal.R;
@@ -317,129 +306,6 @@
}
/**
- * Adds edge extension surface to the given {@code change} for edge extension animation.
- */
- public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
- @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- // Do not create edge extension surface for transfer starting window change.
- // The app surface could be empty thus nothing can draw on the hardware renderer, which will
- // block this thread when calling Surface#unlockCanvasAndPost.
- if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- return;
- }
- final Transformation transformationAtStart = new Transformation();
- a.getTransformationAt(0, transformationAtStart);
- final Transformation transformationAtEnd = new Transformation();
- a.getTransformationAt(1, transformationAtEnd);
-
- // We want to create an extension surface that is the maximal size and the animation will
- // take care of cropping any part that overflows.
- final Insets maxExtensionInsets = Insets.min(
- transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
- final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
- change.getEndAbsBounds().height());
- final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
- change.getEndAbsBounds().width());
- if (maxExtensionInsets.left < 0) {
- final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.left, targetSurfaceHeight);
- final int xPos = maxExtensionInsets.left;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Left Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.top < 0) {
- final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.top);
- final int xPos = 0;
- final int yPos = maxExtensionInsets.top;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Top Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.right < 0) {
- final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.right, targetSurfaceHeight);
- final int xPos = targetSurfaceWidth;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Right Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.bottom < 0) {
- final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.bottom);
- final int xPos = maxExtensionInsets.left;
- final int yPos = targetSurfaceHeight;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Bottom Edge Extension", startTransaction, finishTransaction);
- }
- }
-
- /**
- * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
- * animation.
- */
- private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
- @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
- @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
- .setName(layerName)
- .setParent(surfaceToExtend)
- .setHidden(true)
- .setCallsite("TransitionAnimationHelper#createExtensionSurface")
- .setOpaque(true)
- .setBufferSize(extensionRect.width(), extensionRect.height())
- .build();
-
- final ScreenCapture.LayerCaptureArgs captureArgs =
- new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
- .setSourceCrop(edgeBounds)
- .setFrameScale(1)
- .setPixelFormat(PixelFormat.RGBA_8888)
- .setChildrenOnly(true)
- .setAllowProtected(false)
- .setCaptureSecureLayers(true)
- .build();
- final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
- ScreenCapture.captureLayers(captureArgs);
-
- if (edgeBuffer == null) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Failed to capture edge of window.");
- return null;
- }
-
- final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
- Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
- final Paint paint = new Paint();
- paint.setShader(shader);
-
- final Surface surface = new Surface(edgeExtensionLayer);
- final Canvas c = surface.lockHardwareCanvas();
- c.drawRect(extensionRect, paint);
- surface.unlockCanvasAndPost(c);
- surface.release();
-
- startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
- startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
- startTransaction.setVisibility(edgeExtensionLayer, true);
- finishTransaction.remove(edgeExtensionLayer);
-
- return edgeExtensionLayer;
- }
-
- /**
* Returns whether there is an opaque fullscreen Change positioned in front of the given Change
* in the given TransitionInfo.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index ff50672..ad2e23c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -50,6 +50,7 @@
import androidx.core.view.isGone
import com.android.window.flags.Flags
import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.ContextUtils.isRtl
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
@@ -60,6 +61,8 @@
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.calculateMenuPosition
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.isPinned
@@ -71,6 +74,7 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+
/**
* Handle menu opened when the appropriate button is clicked on.
*
@@ -467,6 +471,33 @@
val rootView = LayoutInflater.from(context)
.inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
+ private val windowingButtonRippleRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_handle_menu_windowing_action_ripple_radius)
+ private val windowingButtonDrawableInsets = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontal = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base)
+ )
+ private val windowingButtonDrawableInsetsLeft = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontalLeft = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift),
+ )
+ private val windowingButtonDrawableInsetsRight = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontalRight = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift)
+ )
+
// App Info Pill.
private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>(
@@ -708,6 +739,49 @@
desktopBtn.isSelected = taskInfo.isFreeform
desktopBtn.isEnabled = !taskInfo.isFreeform
desktopBtn.imageTintList = style.windowingButtonColor
+
+ val startInsets = if (context.isRtl) {
+ windowingButtonDrawableInsetsRight
+ } else {
+ windowingButtonDrawableInsetsLeft
+ }
+ val endInsets = if (context.isRtl) {
+ windowingButtonDrawableInsetsLeft
+ } else {
+ windowingButtonDrawableInsetsRight
+ }
+
+ fullscreenBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = startInsets
+ )
+ }
+
+ splitscreenBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = windowingButtonDrawableInsets
+ )
+ }
+
+ floatingBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = windowingButtonDrawableInsets
+ )
+ }
+
+ desktopBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = endInsets
+ )
+ }
}
private fun bindMoreActionsPill(style: MenuStyle) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
index c6cb62d..1b0e0f70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
@@ -363,10 +363,11 @@
dragEventListeners.remove(dragEventListener)
}
- override fun onTopologyChanged(topology: DisplayTopology) {
+ override fun onTopologyChanged(topology: DisplayTopology?) {
// TODO: b/383069173 - Cancel window drag when topology changes happen during drag.
displayIds.clear()
+ if (topology == null) return
val displayBounds = topology.getAbsoluteBounds()
displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) })
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index 7af6b8e..5bd4228 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -225,7 +225,7 @@
val veilAnimT = surfaceControlTransactionSupplier.get()
val iconAnimT = surfaceControlTransactionSupplier.get()
veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = VEIL_ENTRY_ALPHA_ANIMATION_DURATION
addUpdateListener {
veilAnimT.setAlpha(background, animatedValue as Float)
.apply()
@@ -243,7 +243,8 @@
})
}
iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = ICON_ALPHA_ANIMATION_DURATION
+ startDelay = ICON_ENTRY_DELAY
addUpdateListener {
iconAnimT.setAlpha(icon, animatedValue as Float)
.apply()
@@ -387,23 +388,38 @@
if (background == null || icon == null) return
veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = VEIL_EXIT_ALPHA_ANIMATION_DURATION
+ startDelay = VEIL_EXIT_DELAY
addUpdateListener {
surfaceControlTransactionSupplier.get()
.setAlpha(background, animatedValue as Float)
- .setAlpha(icon, animatedValue as Float)
.apply()
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
surfaceControlTransactionSupplier.get()
.hide(background)
- .hide(icon)
.apply()
}
})
}
+ iconAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = ICON_ALPHA_ANIMATION_DURATION
+ addUpdateListener {
+ surfaceControlTransactionSupplier.get()
+ .setAlpha(icon, animatedValue as Float)
+ .apply()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ surfaceControlTransactionSupplier.get()
+ .hide(icon)
+ .apply()
+ }
+ })
+ }
veilAnimator?.start()
+ iconAnimator?.start()
isVisible = false
}
@@ -451,7 +467,11 @@
companion object {
private const val TAG = "ResizeVeil"
- private const val RESIZE_ALPHA_DURATION = 100L
+ private const val ICON_ALPHA_ANIMATION_DURATION = 50L
+ private const val VEIL_ENTRY_ALPHA_ANIMATION_DURATION = 50L
+ private const val VEIL_EXIT_ALPHA_ANIMATION_DURATION = 200L
+ private const val ICON_ENTRY_DELAY = 33L
+ private const val VEIL_EXIT_DELAY = 33L
private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL
/** The background is a child of the veil container layer and goes at the bottom. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
new file mode 100644
index 0000000..e18239d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.common
+
+import android.annotation.ColorInt
+import android.graphics.Color
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import com.android.wm.shell.windowdecor.common.OPACITY_11
+import com.android.wm.shell.windowdecor.common.OPACITY_15
+import android.content.res.ColorStateList
+
+/**
+ * Represents drawable insets, specifying the number of pixels to inset a drawable from its bounds.
+ */
+data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
+ constructor(vertical: Int = 0, horizontal: Int = 0) :
+ this(horizontal, vertical, horizontal, vertical)
+ constructor(vertical: Int = 0, horizontalLeft: Int = 0, horizontalRight: Int = 0) :
+ this(horizontalLeft, vertical, horizontalRight, vertical)
+}
+
+/**
+ * Replaces the alpha component of a color with the given alpha value.
+ */
+@ColorInt
+fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
+ return Color.argb(
+ alpha,
+ Color.red(color),
+ Color.green(color),
+ Color.blue(color)
+ )
+}
+
+/**
+ * Creates a RippleDrawable with specified color, corner radius, and insets.
+ */
+fun createRippleDrawable(
+ @ColorInt color: Int,
+ cornerRadius: Int,
+ drawableInsets: DrawableInsets,
+): RippleDrawable {
+ return RippleDrawable(
+ ColorStateList(
+ arrayOf(
+ intArrayOf(android.R.attr.state_hovered),
+ intArrayOf(android.R.attr.state_pressed),
+ intArrayOf(),
+ ),
+ intArrayOf(
+ replaceColorAlpha(color, OPACITY_11),
+ replaceColorAlpha(color, OPACITY_15),
+ Color.TRANSPARENT,
+ )
+ ),
+ null /* content */,
+ LayerDrawable(arrayOf(
+ ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { cornerRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = Color.WHITE
+ }
+ )).apply {
+ require(numberOfLayers == 1) { "Must only contain one layer" }
+ setLayerInset(0 /* index */,
+ drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
+ }
+ )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 870c894..eb8b617 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -61,6 +61,8 @@
import com.android.wm.shell.windowdecor.common.OPACITY_55
import com.android.wm.shell.windowdecor.common.OPACITY_65
import com.android.wm.shell.windowdecor.common.Theme
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
@@ -635,61 +637,10 @@
)
}
- @ColorInt
- private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
- return Color.argb(
- alpha,
- Color.red(color),
- Color.green(color),
- Color.blue(color)
- )
- }
-
- private fun createRippleDrawable(
- @ColorInt color: Int,
- cornerRadius: Int,
- drawableInsets: DrawableInsets,
- ): RippleDrawable {
- return RippleDrawable(
- ColorStateList(
- arrayOf(
- intArrayOf(android.R.attr.state_hovered),
- intArrayOf(android.R.attr.state_pressed),
- intArrayOf(),
- ),
- intArrayOf(
- replaceColorAlpha(color, OPACITY_11),
- replaceColorAlpha(color, OPACITY_15),
- Color.TRANSPARENT
- )
- ),
- null /* content */,
- LayerDrawable(arrayOf(
- ShapeDrawable().apply {
- shape = RoundRectShape(
- FloatArray(8) { cornerRadius.toFloat() },
- null /* inset */,
- null /* innerRadii */
- )
- paint.color = Color.WHITE
- }
- )).apply {
- require(numberOfLayers == 1) { "Must only contain one layer" }
- setLayerInset(0 /* index */,
- drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
- }
- )
- }
-
private enum class SizeToggleDirection {
MAXIMIZE, RESTORE
}
- private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
- constructor(vertical: Int = 0, horizontal: Int = 0) :
- this(horizontal, vertical, horizontal, vertical)
- }
-
private data class Header(
val type: Type,
val appTheme: Theme,
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index 2800839..d82c066 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -18,9 +18,9 @@
import android.tools.NavBar
import android.tools.Rotation
-import com.android.internal.R
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assume
import org.junit.Before
@@ -42,8 +42,8 @@
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
// Skip the test when the drag-to-maximize is enabled on this device.
- Assume.assumeFalse(Flags.enableDragToMaximize() &&
- instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+ Assume.assumeFalse(
+ DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
testApp.enterDesktopMode(wmHelper, device)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
index 60a0fb5..675b63c 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
@@ -23,12 +23,12 @@
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
-import com.android.internal.R
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.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assume
import org.junit.Before
@@ -54,8 +54,8 @@
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
// Skip the test when the drag-to-maximize is disabled on this device.
- Assume.assumeTrue(Flags.enableDragToMaximize() &&
- instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+ Assume.assumeTrue(
+ DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
index 81c46f1..b9a5e4a9 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
@@ -25,6 +25,7 @@
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import android.util.DisplayMetrics
+import android.window.DesktopExperienceFlags
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -64,7 +65,7 @@
@Before
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
- Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching())
+ Assume.assumeTrue(DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue)
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 7f48499..e39fa3a 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -22,7 +22,6 @@
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.traces.component.ComponentNameMatcher
-import android.tools.traces.component.EdgeExtensionComponentMatcher
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
@@ -99,7 +98,6 @@
ComponentNameMatcher.SPLASH_SCREEN,
ComponentNameMatcher.SNAPSHOT,
ComponentNameMatcher.IME_SNAPSHOT,
- EdgeExtensionComponentMatcher(),
magnifierLayer,
popupWindowLayer
)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index 0ff7230..f0c97d3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -101,7 +101,7 @@
private fun testDisplayWindowingModeSwitch(
defaultWindowingMode: Int,
extendedDisplayEnabled: Boolean,
- expectTransition: Boolean,
+ expectToSwitch: Boolean,
) {
defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
@@ -113,10 +113,14 @@
settingsSession.use {
connectExternalDisplay()
- defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ if (expectToSwitch) {
+ // Assumes [connectExternalDisplay] properly triggered the switching transition.
+ // Will verify the transition later along with [disconnectExternalDisplay].
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ }
disconnectExternalDisplay()
- if (expectTransition) {
+ if (expectToSwitch) {
val arg = argumentCaptor<WindowContainerTransaction>()
verify(transitions, times(2))
.startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
@@ -139,7 +143,7 @@
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
extendedDisplayEnabled = false,
- expectTransition = false,
+ expectToSwitch = false,
)
}
@@ -148,7 +152,7 @@
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
extendedDisplayEnabled = true,
- expectTransition = true,
+ expectToSwitch = true,
)
}
@@ -157,7 +161,7 @@
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
extendedDisplayEnabled = true,
- expectTransition = false,
+ expectToSwitch = false,
)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 93eb396..04acaef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -261,6 +261,7 @@
@Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var mockDisplayContext: Context
+ @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
@@ -431,6 +432,7 @@
desksTransitionsObserver,
userProfileContexts,
desktopModeCompatPolicy,
+ dragToDisplayTransitionHandler,
)
@After
@@ -2069,6 +2071,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToFullscreen_fromDeskWithMultipleTasks_deactivatesDesk() {
+ val deskId = 1
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+
+ controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ verify(desksOrganizer).deactivateDesk(wct, deskId = deskId)
+ }
+
+ @Test
fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
val task = setUpFreeformTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2278,7 +2295,10 @@
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -2305,29 +2325,6 @@
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- // Setup task2
- setUpFreeformTask()
-
- val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
- assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode =
- WINDOWING_MODE_FULLSCREEN
-
- controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
-
- val wct = getLatestExitDesktopWct()
- val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
- assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
- verify(desktopModeEnterExitTransitionListener)
- .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
- // Does not remove wallpaper activity, as desktop still has a visible desktop task
- wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false))
- }
-
- @Test
fun moveToFullscreen_nonExistentTask_doesNothing() {
controller.moveToFullscreen(999, transitionSource = UNKNOWN)
verifyExitDesktopWCTNotExecuted()
@@ -4455,7 +4452,10 @@
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -4480,27 +4480,6 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- val task3 = setUpFreeformTask()
-
- task1.isFocused = false
- task2.isFocused = true
- task3.isFocused = false
- controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
- val wct = getLatestExitDesktopWct()
- val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
- assertThat(taskChange.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
- // Does not remove wallpaper activity
- wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -5031,7 +5010,7 @@
Mockito.argThat { wct ->
return@argThat wct.hierarchyOps[0].isReparent
},
- eq(null),
+ eq(dragToDisplayTransitionHandler),
)
}
@@ -5225,6 +5204,10 @@
}
@Test
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -5249,6 +5232,24 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun enterSplit_wasInDesk_deactivatesDesk() {
+ val deskId = 5
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val transition = Binder()
+ whenever(splitScreenController.requestEnterSplitSelect(eq(task), any(), any(), any()))
+ .thenReturn(transition)
+
+ controller.requestSplit(task, leftOrTop = false)
+
+ verify(desksOrganizer).deactivateDesk(any(), eq(deskId))
+ verify(desksTransitionsObserver)
+ .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId))
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
fun newWindow_fromFullscreenOpensInSplit() {
setUpLandscapeDisplay()
@@ -6601,6 +6602,7 @@
bounds: Rect? = null,
active: Boolean = true,
background: Boolean = false,
+ deskId: Int? = null,
): RunningTaskInfo {
val task = createFreeformTask(displayId, bounds)
val activityInfo = ActivityInfo()
@@ -6613,7 +6615,11 @@
} else {
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
}
- taskRepository.addTask(displayId, task.taskId, isVisible = active)
+ if (deskId != null) {
+ taskRepository.addTaskToDesk(displayId, deskId, task.taskId, isVisible = active)
+ } else {
+ taskRepository.addTask(displayId, task.taskId, isVisible = active)
+ }
if (!background) {
runningTasks.add(task)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
new file mode 100644
index 0000000..51c3029
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for {@link DragToDisplayTransitionHandler}
+ *
+ * Usage: atest WMShellUnitTests:DragToDisplayTransitionHandlerTest
+ */
+class DragToDisplayTransitionHandlerTest {
+ private lateinit var handler: DragToDisplayTransitionHandler
+ private val mockTransition: IBinder = mock()
+ private val mockRequestInfo: TransitionRequestInfo = mock()
+ private val mockTransitionInfo: TransitionInfo = mock()
+ private val mockStartTransaction: SurfaceControl.Transaction = mock()
+ private val mockFinishTransaction: SurfaceControl.Transaction = mock()
+ private val mockFinishCallback: Transitions.TransitionFinishCallback = mock()
+
+ @Before
+ fun setUp() {
+ handler = DragToDisplayTransitionHandler()
+ whenever(mockStartTransaction.setWindowCrop(any(), any(), any()))
+ .thenReturn(mockStartTransaction)
+ whenever(mockFinishTransaction.setWindowCrop(any(), any(), any()))
+ .thenReturn(mockFinishTransaction)
+ }
+
+ @Test
+ fun handleRequest_anyRequest_returnsNull() {
+ val result = handler.handleRequest(mockTransition, mockRequestInfo)
+ assert(result == null)
+ }
+
+ @Test
+ fun startAnimation_verifyTransformationsApplied() {
+ val mockChange1 = mock<TransitionInfo.Change>()
+ val leash1 = mock<SurfaceControl>()
+ val endBounds1 = Rect(0, 0, 50, 50)
+ val endPosition1 = Point(5, 5)
+
+ whenever(mockChange1.leash).doReturn(leash1)
+ whenever(mockChange1.endAbsBounds).doReturn(endBounds1)
+ whenever(mockChange1.endRelOffset).doReturn(endPosition1)
+
+ val mockChange2 = mock<TransitionInfo.Change>()
+ val leash2 = mock<SurfaceControl>()
+ val endBounds2 = Rect(100, 100, 200, 150)
+ val endPosition2 = Point(15, 25)
+
+ whenever(mockChange2.leash).doReturn(leash2)
+ whenever(mockChange2.endAbsBounds).doReturn(endBounds2)
+ whenever(mockChange2.endRelOffset).doReturn(endPosition2)
+
+ whenever(mockTransitionInfo.changes).doReturn(listOf(mockChange1, mockChange2))
+
+ handler.startAnimation(
+ mockTransition,
+ mockTransitionInfo,
+ mockStartTransaction,
+ mockFinishTransaction,
+ mockFinishCallback,
+ )
+
+ verify(mockStartTransaction).setWindowCrop(leash1, endBounds1.width(), endBounds1.height())
+ verify(mockStartTransaction)
+ .setPosition(leash1, endPosition1.x.toFloat(), endPosition1.y.toFloat())
+ verify(mockStartTransaction).setWindowCrop(leash2, endBounds2.width(), endBounds2.height())
+ verify(mockStartTransaction)
+ .setPosition(leash2, endPosition2.x.toFloat(), endPosition2.y.toFloat())
+ verify(mockStartTransaction).apply()
+ verify(mockFinishCallback).onTransitionFinished(null)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 10c2862..e9c4c31 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -38,6 +38,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -48,6 +50,7 @@
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -58,6 +61,7 @@
import android.app.PendingIntent;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -196,6 +200,7 @@
when(token.asBinder()).thenReturn(mBinder);
when(mRunningTaskInfo.getToken()).thenReturn(token);
when(mTaskOrganizer.getRunningTaskInfo(mTaskId)).thenReturn(mRunningTaskInfo);
+ when(mTaskOrganizer.startNewTransition(anyInt(), any())).thenReturn(new Binder());
when(mRootTDAOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(mDisplayAreaInfo);
when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1);
@@ -557,6 +562,60 @@
assertFalse(c != null && c.getWindowingMode() == WINDOWING_MODE_FULLSCREEN);
}
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testRequestEnterSplit_didNotEnterSplitSelect_doesNotApplyTransaction() {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mStageCoordinator.registerSplitSelectListener(
+ new TestSplitSelectListener(/* alwaysEnter = */ false));
+
+ final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+ SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+ assertNull(transition);
+ verify(mTaskOrganizer, never()).applyTransaction(wct);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testRequestEnterSplit_enteredSplitSelect_appliesTransaction() {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mStageCoordinator.registerSplitSelectListener(
+ new TestSplitSelectListener(/* alwaysEnter = */ true));
+
+ final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+ SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+ assertNull(transition);
+ verify(mTaskOrganizer).applyTransaction(wct);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testRequestEnterSplit_didNotEnterSplitSelect_doesNotStartTransition() {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mStageCoordinator.registerSplitSelectListener(
+ new TestSplitSelectListener(/* alwaysEnter = */ false));
+
+ final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+ SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+ assertNull(transition);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testRequestEnterSplit_enteredSplitSelect_startsTransition() {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mStageCoordinator.registerSplitSelectListener(
+ new TestSplitSelectListener(/* alwaysEnter = */ true));
+
+ final IBinder transition = mStageCoordinator.requestEnterSplitSelect(mRunningTaskInfo, wct,
+ SPLIT_POSITION_TOP_OR_LEFT, new Rect(0, 0, 100, 100));
+
+ assertNotNull(transition);
+ }
+
private Transitions createTestTransitions() {
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
@@ -566,4 +625,18 @@
shellInit.init();
return t;
}
+
+ private static class TestSplitSelectListener implements SplitScreen.SplitSelectListener {
+ private final boolean mAlwaysEnter;
+
+ TestSplitSelectListener(boolean alwaysEnter) {
+ mAlwaysEnter = alwaysEnter;
+ }
+
+ @Override
+ public boolean onRequestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+ int splitPosition, Rect taskBounds) {
+ return mAlwaysEnter;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 6f73db0..6773307 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1316,9 +1316,11 @@
mTransactionPool, createTestDisplayController(), mMainExecutor,
mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
mock(FocusTransitionObserver.class));
+ final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class);
+ doReturn(mContext).when(mockRecentsTaskController).getContext();
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
- mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+ mockRecentsTaskController, mock(HomeTransitionObserver.class));
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
shellInit.init();
diff --git a/location/java/android/location/GnssMeasurement.java b/location/java/android/location/GnssMeasurement.java
index 200d4ef..6ae73a2 100644
--- a/location/java/android/location/GnssMeasurement.java
+++ b/location/java/android/location/GnssMeasurement.java
@@ -1484,6 +1484,10 @@
* in an open sky test - the important aspect of this output is that changes in this value are
* indicative of changes on input signal power in the frequency band for this measurement.
*
+ * <p> This field is part of the GnssMeasurement object so it is only reported when the GNSS
+ * measurement is reported. E.g., when a GNSS signal is too weak to be acquired, the AGC value
+ * is not reported.
+ *
* <p> The value is only available if {@link #hasAutomaticGainControlLevelDb()} is {@code true}
*
* @deprecated Use {@link GnssMeasurementsEvent#getGnssAutomaticGainControls()} instead.
diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java
index 4fc2ee8..8cdfd01 100644
--- a/location/java/android/location/GnssMeasurementsEvent.java
+++ b/location/java/android/location/GnssMeasurementsEvent.java
@@ -158,6 +158,14 @@
/**
* Gets the collection of {@link GnssAutomaticGainControl} associated with the
* current event.
+ *
+ * <p>This field must be reported when the GNSS measurement engine is running, even when the
+ * GnssMeasurement or GnssClock fields are not reported yet. E.g., when a GNSS signal is too
+ * weak to be acquired, the AGC value must still be reported.
+ *
+ * <p>For devices that do not support this field, an empty collection is returned. In that case,
+ * please use {@link GnssMeasurement#hasAutomaticGainControlLevelDb()}
+ * and {@link GnssMeasuremen#getAutomaticGainControlLevelDb()}.
*/
@NonNull
public Collection<GnssAutomaticGainControl> getGnssAutomaticGainControls() {
diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
index 8b6194f..fb89973 100644
--- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
+++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -28,7 +28,6 @@
import android.util.Log;
import com.android.internal.annotations.KeepForWeakReference;
-import com.android.internal.telephony.flags.Flags;
import java.util.concurrent.TimeUnit;
@@ -146,17 +145,12 @@
< emergencyExtensionMillis);
boolean isInEmergencyCallback = false;
boolean isInEmergencySmsMode = false;
- if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+ }
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
- } else {
- PackageManager pm = mContext.getPackageManager();
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
- isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
- }
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
- isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
- }
}
return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
|| isInEmergencySmsMode;
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 6d4f0b4..846448b 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -39,3 +39,12 @@
}
is_exported: true
}
+
+flag {
+ namespace: "media_projection"
+ name: "app_content_sharing"
+ description: "Enable apps to share some sub-surface"
+ bug: "379989921"
+ is_exported: true
+}
+
diff --git a/native/android/OWNERS b/native/android/OWNERS
index 3ea2d35..1e8d30d 100644
--- a/native/android/OWNERS
+++ b/native/android/OWNERS
@@ -6,8 +6,7 @@
# Networking
per-file libandroid_net.map.txt, net.c = set noparent
-per-file libandroid_net.map.txt, net.c = codewiz@google.com, jchalard@google.com, junyulai@google.com
-per-file libandroid_net.map.txt, net.c = lorenzo@google.com, reminv@google.com, satk@google.com
+per-file libandroid_net.map.txt, net.c = file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking
# Fonts
per-file system_fonts.cpp = file:/graphics/java/android/graphics/fonts/OWNERS
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
index 572fc36..3113129 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionAssociationActivity.java
@@ -452,7 +452,7 @@
}
private void stopDiscovery() {
- if (!mRequest.isSelfManaged()) {
+ if (mRequest != null && !mRequest.isSelfManaged()) {
CompanionDeviceDiscoveryService.stop(this);
}
}
diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
index 9d037e9..806580b 100644
--- a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
+++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
@@ -17,20 +17,20 @@
package com.android.settingslib.widget
import android.content.Context
-import android.os.Build
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
-import androidx.annotation.RequiresApi
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.android.settingslib.widget.preference.intro.R
-class IntroPreference @JvmOverloads constructor(
+class IntroPreference
+@JvmOverloads
+constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ defStyleRes: Int = 0,
) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin {
private var isCollapsable: Boolean = true
@@ -66,9 +66,9 @@
/**
* Sets whether the summary is collapsable.
+ *
* @param collapsable True if the summary should be collapsable, false otherwise.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setCollapsable(collapsable: Boolean) {
isCollapsable = collapsable
minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
@@ -77,9 +77,9 @@
/**
* Sets the minimum number of lines to display when collapsed.
+ *
* @param lines The minimum number of lines.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setMinLines(lines: Int) {
minLines = lines.coerceIn(1, DEFAULT_MAX_LINES)
notifyChanged()
@@ -87,9 +87,9 @@
/**
* Sets the action when clicking on the hyperlink in the text.
+ *
* @param listener The click listener for hyperlink.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setHyperlinkListener(listener: View.OnClickListener) {
if (hyperlinkListener != listener) {
hyperlinkListener = listener
@@ -99,9 +99,9 @@
/**
* Sets the action when clicking on the learn more view.
+ *
* @param listener The click listener for learn more.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setLearnMoreAction(listener: View.OnClickListener) {
if (learnMoreListener != listener) {
learnMoreListener = listener
@@ -111,9 +111,9 @@
/**
* Sets the text of learn more view.
+ *
* @param text The text of learn more.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setLearnMoreText(text: CharSequence) {
if (!TextUtils.equals(learnMoreText, text)) {
learnMoreText = text
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 1cb8005..02bef9f 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -59,8 +59,6 @@
private val preferenceHierarchy: PreferenceHierarchy,
) : KeyedDataObservable<String>() {
- private val mainExecutor = HandlerExecutor.main
-
private val preferenceLifecycleContext =
object : PreferenceLifecycleContext(context) {
override val lifecycleScope: LifecycleCoroutineScope
@@ -88,11 +86,11 @@
private val preferences: ImmutableMap<String, PreferenceHierarchyNode>
private val dependencies: ImmutableMultimap<String, String>
private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider>
- private val storages = mutableMapOf<String, KeyedObservable<String>>()
+ private val observables = mutableMapOf<String, KeyedObservable<String>>()
private val preferenceObserver: KeyedObserver<String?>
- private val storageObserver =
+ private val observer =
KeyedObserver<String> { key, reason ->
if (DataChangeReason.isDataChange(reason)) {
notifyChange(key, PreferenceChangeReason.VALUE)
@@ -133,15 +131,19 @@
this.dependencies = dependenciesBuilder.build()
this.lifecycleAwarePreferences = lifecycleAwarePreferences.toTypedArray()
+ val executor = HandlerExecutor.main
preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
- addObserver(preferenceObserver, mainExecutor)
+ addObserver(preferenceObserver, executor)
preferenceScreen.forEachRecursively {
- it.preferenceDataStore?.findKeyValueStore()?.let { keyValueStore ->
- val key = it.key
- storages[key] = keyValueStore
- keyValueStore.addObserver(key, storageObserver, mainExecutor)
- }
+ val key = it.key ?: return@forEachRecursively
+ @Suppress("UNCHECKED_CAST")
+ val observable =
+ it.preferenceDataStore?.findKeyValueStore()
+ ?: (preferences[key]?.metadata as? KeyedObservable<String>)
+ ?: return@forEachRecursively
+ observables[key] = observable
+ observable.addObserver(key, observer, executor)
}
}
@@ -212,7 +214,7 @@
fun onDestroy() {
removeObserver(preferenceObserver)
- for ((key, storage) in storages) storage.removeObserver(key, storageObserver)
+ for ((key, observable) in observables) observable.removeObserver(key, observer)
for (preference in lifecycleAwarePreferences) {
preference.onDestroy(preferenceLifecycleContext)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index ebd5a1d..3625c00 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -29,6 +29,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telephony.TelephonyManager;
import android.util.Log;
@@ -37,6 +38,8 @@
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.R;
+import com.android.settingslib.flags.Flags;
+import com.android.settingslib.utils.ThreadUtils;
import java.util.Collection;
import java.util.HashMap;
@@ -65,6 +68,7 @@
private final android.os.Handler mReceiverHandler;
private final UserHandle mUserHandle;
private final Context mContext;
+ private boolean mIsWorkProfile = false;
interface Handler {
void onReceive(Context context, Intent intent, BluetoothDevice device);
@@ -140,6 +144,9 @@
addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler());
registerAdapterIntentReceiver();
+
+ UserManager userManager = context.getSystemService(UserManager.class);
+ mIsWorkProfile = userManager != null && userManager.isManagedProfile();
}
/** Register to start receiving callbacks for Bluetooth events. */
@@ -220,20 +227,32 @@
callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
}
+ if (mIsWorkProfile) {
+ Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile");
+ return;
+ }
+
+ LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null
+ : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
+ LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null
+ : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
// Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when
// audio sharing is enabled.
if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& state == BluetoothAdapter.STATE_DISCONNECTED
- && BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
- LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
- if (profileManager != null
- && profileManager.getLeAudioBroadcastProfile() != null
- && profileManager.getLeAudioBroadcastProfile().isProfileReady()
- && profileManager.getLeAudioBroadcastAssistantProfile() != null
- && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) {
- Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
- profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded();
- }
+ && BluetoothUtils.isAudioSharingUIAvailable(mContext)
+ && broadcast != null && assistant != null && broadcast.isProfileReady()
+ && assistant.isProfileReady()) {
+ Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
+ broadcast.updateFallbackActiveDeviceIfNeeded();
+ }
+ // Dispatch handleOnProfileStateChanged to local broadcast profile
+ if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
+ && broadcast != null
+ && state == BluetoothAdapter.STATE_CONNECTED) {
+ Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile");
+ var unused = ThreadUtils.postOnBackgroundThread(
+ () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager));
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 31948e4..e78a692 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -719,6 +719,30 @@
}
}
+ /** Check if the {@link CachedBluetoothDevice} is a media device */
+ @WorkerThread
+ public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof A2dpProfile
+ || profile instanceof HearingAidProfile
+ || profile instanceof LeAudioProfile
+ || profile instanceof HeadsetProfile);
+ }
+
+ /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
+ @WorkerThread
+ public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof LeAudioProfile
+ && profile.isEnabled(cachedDevice.getDevice()));
+ }
+
/** Returns if the broadcast is on-going. */
@WorkerThread
public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index f18a2da..08f7806 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -54,6 +54,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
@@ -64,6 +65,7 @@
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -107,6 +109,7 @@
private static final String SETTINGS_PKG = "com.android.settings";
private static final String SYSUI_PKG = "com.android.systemui";
private static final String TAG = "LocalBluetoothLeBroadcast";
+ private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID";
private static final boolean DEBUG = BluetoothUtils.D;
private static final String VALID_PASSWORD_CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,"
@@ -120,6 +123,7 @@
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
+ private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s
private static final Uri[] SETTINGS_URIS =
new Uri[] {
Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME),
@@ -1283,4 +1287,87 @@
UserManager userManager = context.getSystemService(UserManager.class);
return userManager != null && userManager.isManagedProfile();
}
+
+ /** Handle profile connected for {@link CachedBluetoothDevice}. */
+ @WorkerThread
+ public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice,
+ int bluetoothProfile, @Nullable LocalBluetoothManager btManager) {
+ if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) {
+ Log.d(TAG, "Skip handleProfileConnected, flag off");
+ return;
+ }
+ if (!SYSUI_PKG.equals(mContext.getPackageName())) {
+ Log.d(TAG, "Skip handleProfileConnected, not a valid caller");
+ return;
+ }
+ if (!BluetoothUtils.isMediaDevice(cachedDevice)) {
+ Log.d(TAG, "Skip handleProfileConnected, not a media device");
+ return;
+ }
+ Timestamp bondTimestamp = cachedDevice.getBondTimestamp();
+ if (bondTimestamp != null) {
+ long diff = System.currentTimeMillis() - bondTimestamp.getTime();
+ if (diff <= JUST_BOND_MILLIS_THRESHOLD) {
+ Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff);
+ return;
+ }
+ }
+ if (!isEnabled(null)) {
+ Log.d(TAG, "Skip handleProfileConnected, not broadcasting");
+ return;
+ }
+ BluetoothDevice device = cachedDevice.getDevice();
+ if (device == null) {
+ Log.d(TAG, "Skip handleProfileConnected, null device");
+ return;
+ }
+ // TODO: sync source in a reasonable place
+ if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) {
+ Log.d(TAG, "Skip handleProfileConnected, already has source");
+ return;
+ }
+ if (isAutoRejoinDevice(device)) {
+ Log.d(TAG, "Skip handleProfileConnected, auto rejoin device");
+ return;
+ }
+ boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice);
+ // For eligible (LE audio) remote device, we only check assistant profile connected.
+ if (isLeAudioSupported
+ && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+ Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile");
+ return;
+ }
+ boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
+ // For ineligible (classic) remote device, we only check its first connected profile.
+ if (!isLeAudioSupported && !isFirstConnectedProfile) {
+ Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile");
+ return;
+ }
+
+ Intent intent = new Intent(
+ LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED);
+ intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device);
+ intent.setPackage(SETTINGS_PKG);
+ Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress());
+
+ mContext.sendBroadcast(intent);
+ }
+
+ private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) {
+ String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice,
+ AUTO_REJOIN_BROADCAST_TAG);
+ return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue,
+ String.valueOf(getLatestBroadcastId()));
+ }
+
+ private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice,
+ int bluetoothProfile) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .noneMatch(
+ profile ->
+ profile.getProfileId() != bluetoothProfile
+ && profile.getConnectionStatus(cachedDevice.getDevice())
+ == BluetoothProfile.STATE_CONNECTED);
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
index ae17acb..8bb41cc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
@@ -16,8 +16,8 @@
package com.android.settingslib.qrcode;
+import android.annotation.NonNull;
import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
@@ -75,12 +75,29 @@
@VisibleForTesting
Camera mCamera;
+ Camera.CameraInfo mCameraInfo;
+
+ /**
+ * The size of the preview image as requested to camera, e.g. 1920x1080.
+ */
private Size mPreviewSize;
+
+ /**
+ * Whether the preview image would be displayed in "portrait" (width less
+ * than height) orientation in current display orientation.
+ *
+ * Note that we don't distinguish between a rotation of 90 degrees or 270
+ * degrees here, since we center crop all the preview.
+ *
+ * TODO: Handle external camera / multiple display, this likely requires
+ * migrating to newer Camera2 API.
+ */
+ private boolean mPreviewInPortrait;
+
private WeakReference<Context> mContext;
private ScannerCallback mScannerCallback;
private MultiFormatReader mReader;
private DecodingTask mDecodeTask;
- private int mCameraOrientation;
@VisibleForTesting
Camera.Parameters mParameters;
@@ -152,8 +169,14 @@
* @param previewSize Is the preview size set by camera
* @param cameraOrientation Is the orientation of current Camera
* @return The rectangle would like to crop from the camera preview shot.
+ * @deprecated This is no longer used, and the frame position is
+ * automatically calculated from the preview size and the
+ * background View size.
*/
- Rect getFramePosition(Size previewSize, int cameraOrientation);
+ @Deprecated
+ default @NonNull Rect getFramePosition(@NonNull Size previewSize, int cameraOrientation) {
+ throw new AssertionError("getFramePosition shouldn't be used");
+ }
/**
* Sets the transform to associate with preview area.
@@ -172,6 +195,41 @@
boolean isValid(String qrCode);
}
+ private boolean setPreviewDisplayOrientation() {
+ if (mContext.get() == null) {
+ return false;
+ }
+
+ final WindowManager winManager =
+ (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
+ final int rotation = winManager.getDefaultDisplay().getRotation();
+ int degrees = 0;
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ }
+ int rotateDegrees = 0;
+ if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ rotateDegrees = (mCameraInfo.orientation + degrees) % 360;
+ rotateDegrees = (360 - rotateDegrees) % 360; // compensate the mirror
+ } else {
+ rotateDegrees = (mCameraInfo.orientation - degrees + 360) % 360;
+ }
+ mCamera.setDisplayOrientation(rotateDegrees);
+ mPreviewInPortrait = (rotateDegrees == 90 || rotateDegrees == 270);
+ return true;
+ }
+
@VisibleForTesting
void setCameraParameter() {
mParameters = mCamera.getParameters();
@@ -195,37 +253,39 @@
mCamera.setParameters(mParameters);
}
- private boolean startPreview() {
- if (mContext.get() == null) {
- return false;
- }
+ /**
+ * Set transform matrix to crop and center the preview picture.
+ */
+ private void setTransformationMatrix() {
+ final Size previewDisplaySize = rotateIfPortrait(mPreviewSize);
+ final Size viewSize = mScannerCallback.getViewSize();
+ final Rect cropRegion = calculateCenteredCrop(previewDisplaySize, viewSize);
- final WindowManager winManager =
- (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
- final int rotation = winManager.getDefaultDisplay().getRotation();
- int degrees = 0;
- switch (rotation) {
- case Surface.ROTATION_0:
- degrees = 0;
- break;
- case Surface.ROTATION_90:
- degrees = 90;
- break;
- case Surface.ROTATION_180:
- degrees = 180;
- break;
- case Surface.ROTATION_270:
- degrees = 270;
- break;
- }
- final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360;
- mCamera.setDisplayOrientation(rotateDegrees);
+ // Note that strictly speaking, since the preview is mirrored in front
+ // camera case, we should also mirror the crop region here. But since
+ // we're cropping at the center, mirroring would result in the same
+ // crop region other than small off-by-one error from floating point
+ // calculation and wouldn't be noticeable.
+
+ // Calculate transformation matrix.
+ float scaleX = previewDisplaySize.getWidth() / (float) cropRegion.width();
+ float scaleY = previewDisplaySize.getHeight() / (float) cropRegion.height();
+ float translateX = -cropRegion.left / (float) cropRegion.width() * viewSize.getWidth();
+ float translateY = -cropRegion.top / (float) cropRegion.height() * viewSize.getHeight();
+
+ // Set the transform matrix.
+ final Matrix matrix = new Matrix();
+ matrix.setScale(scaleX, scaleY);
+ matrix.postTranslate(translateX, translateY);
+ mScannerCallback.setTransform(matrix);
+ }
+
+ private void startPreview() {
mCamera.startPreview();
if (Camera.Parameters.FOCUS_MODE_AUTO.equals(mParameters.getFocusMode())) {
mCamera.autoFocus(/* Camera.AutoFocusCallback */ null);
sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS);
}
- return true;
}
private class DecodingTask extends AsyncTask<Void, Void, String> {
@@ -300,7 +360,7 @@
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
releaseCamera();
mCamera = Camera.open(i);
- mCameraOrientation = cameraInfo.orientation;
+ mCameraInfo = cameraInfo;
break;
}
}
@@ -309,7 +369,7 @@
Camera.getCameraInfo(0, cameraInfo);
releaseCamera();
mCamera = Camera.open(0);
- mCameraOrientation = cameraInfo.orientation;
+ mCameraInfo = cameraInfo;
}
} catch (RuntimeException e) {
Log.e(TAG, "Fail to open camera: " + e);
@@ -323,11 +383,12 @@
throw new IOException("Cannot find available camera");
}
mCamera.setPreviewTexture(surface);
+ if (!setPreviewDisplayOrientation()) {
+ throw new IOException("Lost context");
+ }
setCameraParameter();
setTransformationMatrix();
- if (!startPreview()) {
- throw new IOException("Lost contex");
- }
+ startPreview();
} catch (IOException ioe) {
Log.e(TAG, "Fail to startPreview camera: " + ioe);
mCamera = null;
@@ -345,32 +406,30 @@
}
}
- /** Set transform matrix to crop and center the preview picture */
- private void setTransformationMatrix() {
- final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_PORTRAIT;
-
- final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight();
- final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth();
- final float ratioPreview = (float) getRatio(previewWidth, previewHeight);
-
- // Calculate transformation matrix.
- float scaleX = 1.0f;
- float scaleY = 1.0f;
- if (previewWidth > previewHeight) {
- scaleY = scaleX / ratioPreview;
+ /**
+ * Calculates the crop region in `previewSize` to have the same aspect
+ * ratio as `viewSize` and center aligned.
+ */
+ private Rect calculateCenteredCrop(Size previewSize, Size viewSize) {
+ final double previewRatio = getRatio(previewSize);
+ final double viewRatio = getRatio(viewSize);
+ int width;
+ int height;
+ if (previewRatio > viewRatio) {
+ width = previewSize.getWidth();
+ height = (int) Math.round(width * viewRatio);
} else {
- scaleX = scaleY / ratioPreview;
+ height = previewSize.getHeight();
+ width = (int) Math.round(height / viewRatio);
}
-
- // Set the transform matrix.
- final Matrix matrix = new Matrix();
- matrix.setScale(scaleX, scaleY);
- mScannerCallback.setTransform(matrix);
+ final int left = (previewSize.getWidth() - width) / 2;
+ final int top = (previewSize.getHeight() - height) / 2;
+ return new Rect(left, top, left + width, top + height);
}
private QrYuvLuminanceSource getFrameImage(byte[] imageData) {
- final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation);
+ final Size viewSize = mScannerCallback.getViewSize();
+ final Rect frame = calculateCenteredCrop(mPreviewSize, rotateIfPortrait(viewSize));
final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData,
mPreviewSize.getWidth(), mPreviewSize.getHeight());
return (QrYuvLuminanceSource)
@@ -398,17 +457,18 @@
*/
private Size getBestPreviewSize(Camera.Parameters parameters) {
final double minRatioDiffPercent = 0.1;
- final Size windowSize = mScannerCallback.getViewSize();
- final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight());
+ final Size viewSize = rotateIfPortrait(mScannerCallback.getViewSize());
+ final double viewRatio = getRatio(viewSize);
double bestChoiceRatio = 0;
Size bestChoice = new Size(0, 0);
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
- double ratio = getRatio(size.width, size.height);
+ final Size newSize = toAndroidSize(size);
+ final double ratio = getRatio(newSize);
if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight()
- && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent
- || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) {
- bestChoice = new Size(size.width, size.height);
- bestChoiceRatio = getRatio(size.width, size.height);
+ && (Math.abs(bestChoiceRatio - viewRatio) / viewRatio > minRatioDiffPercent
+ || Math.abs(ratio - viewRatio) / viewRatio <= minRatioDiffPercent)) {
+ bestChoice = newSize;
+ bestChoiceRatio = ratio;
}
}
return bestChoice;
@@ -419,25 +479,26 @@
* picture size and aspect ratio to choose the best one.
*/
private Size getBestPictureSize(Camera.Parameters parameters) {
- final Camera.Size previewSize = parameters.getPreviewSize();
- final double previewRatio = getRatio(previewSize.width, previewSize.height);
+ final Size previewSize = mPreviewSize;
+ final double previewRatio = getRatio(previewSize);
List<Size> bestChoices = new ArrayList<>();
final List<Size> similarChoices = new ArrayList<>();
// Filter by ratio
- for (Camera.Size size : parameters.getSupportedPictureSizes()) {
- double ratio = getRatio(size.width, size.height);
+ for (Camera.Size picSize : parameters.getSupportedPictureSizes()) {
+ final Size size = toAndroidSize(picSize);
+ final double ratio = getRatio(size);
if (ratio == previewRatio) {
- bestChoices.add(new Size(size.width, size.height));
+ bestChoices.add(size);
} else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) {
- similarChoices.add(new Size(size.width, size.height));
+ similarChoices.add(size);
}
}
if (bestChoices.size() == 0 && similarChoices.size() == 0) {
Log.d(TAG, "No proper picture size, return default picture size");
Camera.Size defaultPictureSize = parameters.getPictureSize();
- return new Size(defaultPictureSize.width, defaultPictureSize.height);
+ return toAndroidSize(defaultPictureSize);
}
if (bestChoices.size() == 0) {
@@ -447,7 +508,7 @@
// Get the best by area
int bestAreaDifference = Integer.MAX_VALUE;
Size bestChoice = null;
- final int previewArea = previewSize.width * previewSize.height;
+ final int previewArea = previewSize.getWidth() * previewSize.getHeight();
for (Size size : bestChoices) {
int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea);
if (areaDifference < bestAreaDifference) {
@@ -458,8 +519,20 @@
return bestChoice;
}
- private double getRatio(double x, double y) {
- return (x < y) ? x / y : y / x;
+ private Size rotateIfPortrait(Size size) {
+ if (mPreviewInPortrait) {
+ return new Size(size.getHeight(), size.getWidth());
+ } else {
+ return size;
+ }
+ }
+
+ private double getRatio(Size size) {
+ return size.getHeight() / (double) size.getWidth();
+ }
+
+ private Size toAndroidSize(Camera.Size size) {
+ return new Size(size.width, size.height);
}
@VisibleForTesting
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index b86f4b3..eac69234 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -23,7 +23,6 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -38,12 +37,14 @@
import android.content.IntentFilter;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.TelephonyManager;
import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.utils.ThreadUtils;
import org.junit.Before;
import org.junit.Rule;
@@ -54,6 +55,8 @@
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
import org.robolectric.shadow.api.Shadow;
import java.util.ArrayList;
@@ -61,7 +64,7 @@
import java.util.List;
@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowBluetoothAdapter.class})
+@Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class})
public class BluetoothEventManagerTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -100,6 +103,8 @@
private BluetoothUtils.ErrorListener mErrorListener;
@Mock
private LocalBluetoothLeBroadcast mBroadcast;
+ @Mock
+ private UserManager mUserManager;
private Context mContext;
private Intent mIntent;
@@ -130,6 +135,7 @@
mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1);
mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2);
mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
BluetoothUtils.setErrorListener(mErrorListener);
}
@@ -196,6 +202,7 @@
* callback.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() {
mBluetoothEventManager.registerCallback(mBluetoothCallback);
@@ -208,10 +215,12 @@
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off.
*/
@Test
- public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -219,16 +228,19 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not
- * support audio sharing.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support
+ * audio sharing.
*/
@Test
- public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -236,7 +248,8 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
@@ -245,6 +258,7 @@
* not ready.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
false, /* workProfile= */ false);
@@ -253,7 +267,7 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -262,6 +276,7 @@
* other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
@@ -270,16 +285,17 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
}
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for
- * work profile.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile.
*/
@Test
- public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ true);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -287,7 +303,8 @@
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
@@ -296,7 +313,8 @@
* disconnected and audio sharing is enabled.
*/
@Test
- public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -305,6 +323,27 @@
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
+ }
+
+ /**
+ * dispatchProfileConnectionStateChanged should call {@link
+ * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and
+ * audio sharing is enabled.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() {
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ false);
+ mBluetoothEventManager.dispatchProfileConnectionStateChanged(
+ mCachedBluetoothDevice,
+ BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager);
}
private void setUpAudioSharing(boolean enableFlag, boolean enableFeature,
@@ -325,13 +364,19 @@
LocalBluetoothLeBroadcastAssistant assistant =
mock(LocalBluetoothLeBroadcastAssistant.class);
when(assistant.isProfileReady()).thenReturn(enableProfile);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
- UserManager userManager = mock(UserManager.class);
- when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
- when(userManager.isManagedProfile()).thenReturn(workProfile);
+ when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+ when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
+ when(mUserManager.isManagedProfile()).thenReturn(workProfile);
+ if (workProfile) {
+ mBluetoothEventManager =
+ new BluetoothEventManager(
+ mLocalAdapter,
+ mBtManager,
+ mCachedDeviceManager,
+ mContext,
+ /* handler= */ null,
+ /* userHandle= */ null);
+ }
}
@Test
@@ -665,4 +710,12 @@
verify(mBluetoothCallback).onAutoOnStateChanged(anyInt());
}
+
+ @Implements(value = ThreadUtils.class)
+ public static class ShadowThreadUtils {
+ @Implementation
+ protected static void postOnBackgroundThread(Runnable runnable) {
+ runnable.run();
+ }
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 0325c0e..b781412 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -1349,6 +1349,36 @@
}
@Test
+ public void isMediaDevice_returnsFalse() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant));
+ assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isMediaDevice_returnsTrue() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isLeAudioSupported_returnsFalse() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false);
+
+ assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isLeAudioSupported_returnsTrue() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true);
+
+ assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue();
+ }
+
+ @Test
public void isTemporaryBondDevice_hasMetadata_returnsTrue() {
when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
.thenReturn(TEMP_BOND_METADATA.getBytes());
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
index a9fd380..76b6aa8 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
@@ -28,6 +28,7 @@
public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowColorDisplayManager {
private boolean mIsReduceBrightColorsActivated;
+ private int mColorMode;
@Implementation
@SystemApi
@@ -43,4 +44,13 @@
return mIsReduceBrightColorsActivated;
}
+ @Implementation
+ public int getColorMode() {
+ return mColorMode;
+ }
+
+ @Implementation
+ public void setColorMode(int colorMode) {
+ mColorMode = colorMode;
+ }
}
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index cc01071..1362ffe 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -24,6 +24,17 @@
"name": "SystemUIGoogleTests"
},
{
+ "name": "SystemUIClocksTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
// Permission indicators
"name": "CtsPermissionUiTestCases",
"options": [
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
index 71ec63c..84370ed 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
@@ -31,6 +31,7 @@
import com.android.compose.theme.typography.TypefaceNames
import com.android.compose.theme.typography.TypefaceTokens
import com.android.compose.theme.typography.TypographyTokens
+import com.android.compose.theme.typography.VariableFontTypeScaleEmphasizedTokens
import com.android.compose.theme.typography.platformTypography
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.compose.windowsizeclass.calculateWindowSizeClass
@@ -44,9 +45,15 @@
val colorScheme = remember(context, isDarkTheme) { platformColorScheme(isDarkTheme, context) }
val androidColorScheme = remember(context) { AndroidColorScheme(context) }
val typefaceNames = remember(context) { TypefaceNames.get(context) }
+ val typefaceTokens = remember(typefaceNames) { TypefaceTokens(typefaceNames) }
val typography =
- remember(typefaceNames) {
- platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
+ remember(typefaceTokens) {
+ platformTypography(
+ TypographyTokens(
+ TypeScaleTokens(typefaceTokens),
+ VariableFontTypeScaleEmphasizedTokens(typefaceTokens),
+ )
+ )
}
val windowSizeClass = calculateWindowSizeClass()
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
index 1ce1ae3..652f946 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
@@ -16,6 +16,7 @@
package com.android.compose.theme.typography
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
@@ -25,6 +26,7 @@
* Do not use directly and call [MaterialTheme.typography] instead to access the different text
* styles.
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
return Typography(
displayLarge = typographyTokens.displayLarge,
@@ -42,5 +44,21 @@
labelLarge = typographyTokens.labelLarge,
labelMedium = typographyTokens.labelMedium,
labelSmall = typographyTokens.labelSmall,
+ // GSF emphasized tokens
+ displayLargeEmphasized = typographyTokens.displayLargeEmphasized,
+ displayMediumEmphasized = typographyTokens.displayMediumEmphasized,
+ displaySmallEmphasized = typographyTokens.displaySmallEmphasized,
+ headlineLargeEmphasized = typographyTokens.headlineLargeEmphasized,
+ headlineMediumEmphasized = typographyTokens.headlineMediumEmphasized,
+ headlineSmallEmphasized = typographyTokens.headlineSmallEmphasized,
+ titleLargeEmphasized = typographyTokens.titleLargeEmphasized,
+ titleMediumEmphasized = typographyTokens.titleMediumEmphasized,
+ titleSmallEmphasized = typographyTokens.titleSmallEmphasized,
+ bodyLargeEmphasized = typographyTokens.bodyLargeEmphasized,
+ bodyMediumEmphasized = typographyTokens.bodyMediumEmphasized,
+ bodySmallEmphasized = typographyTokens.bodySmallEmphasized,
+ labelLargeEmphasized = typographyTokens.labelLargeEmphasized,
+ labelMediumEmphasized = typographyTokens.labelMediumEmphasized,
+ labelSmallEmphasized = typographyTokens.labelSmallEmphasized,
)
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
index 13acfd6..280b8d9 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
@@ -34,6 +34,29 @@
private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
+ // Google Sans Flex emphasized styles
+ private val displayLargeEmphasizedFont =
+ DeviceFontFamilyName("variable-display-large-emphasized")
+ private val displayMediumEmphasizedFont =
+ DeviceFontFamilyName("variable-display-medium-emphasized")
+ private val displaySmallEmphasizedFont =
+ DeviceFontFamilyName("variable-display-small-emphasized")
+ private val headlineLargeEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-large-emphasized")
+ private val headlineMediumEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-medium-emphasized")
+ private val headlineSmallEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-small-emphasized")
+ private val titleLargeEmphasizedFont = DeviceFontFamilyName("variable-title-large-emphasized")
+ private val titleMediumEmphasizedFont = DeviceFontFamilyName("variable-title-medium-emphasized")
+ private val titleSmallEmphasizedFont = DeviceFontFamilyName("variable-title-small-emphasized")
+ private val bodyLargeEmphasizedFont = DeviceFontFamilyName("variable-body-large-emphasized")
+ private val bodyMediumEmphasizedFont = DeviceFontFamilyName("variable-body-medium-emphasized")
+ private val bodySmallEmphasizedFont = DeviceFontFamilyName("variable-body-small-emphasized")
+ private val labelLargeEmphasizedFont = DeviceFontFamilyName("variable-label-large-emphasized")
+ private val labelMediumEmphasizedFont = DeviceFontFamilyName("variable-label-medium-emphasized")
+ private val labelSmallEmphasizedFont = DeviceFontFamilyName("variable-label-small-emphasized")
+
val brand =
FontFamily(
Font(brandFont, weight = WeightMedium),
@@ -44,6 +67,22 @@
Font(plainFont, weight = WeightMedium),
Font(plainFont, weight = WeightRegular),
)
+
+ val displayLargeEmphasized = FontFamily(Font(displayLargeEmphasizedFont))
+ val displayMediumEmphasized = FontFamily(Font(displayMediumEmphasizedFont))
+ val displaySmallEmphasized = FontFamily(Font(displaySmallEmphasizedFont))
+ val headlineLargeEmphasized = FontFamily(Font(headlineLargeEmphasizedFont))
+ val headlineMediumEmphasized = FontFamily(Font(headlineMediumEmphasizedFont))
+ val headlineSmallEmphasized = FontFamily(Font(headlineSmallEmphasizedFont))
+ val titleLargeEmphasized = FontFamily(Font(titleLargeEmphasizedFont))
+ val titleMediumEmphasized = FontFamily(Font(titleMediumEmphasizedFont))
+ val titleSmallEmphasized = FontFamily(Font(titleSmallEmphasizedFont))
+ val bodyLargeEmphasized = FontFamily(Font(bodyLargeEmphasizedFont))
+ val bodyMediumEmphasized = FontFamily(Font(bodyMediumEmphasizedFont))
+ val bodySmallEmphasized = FontFamily(Font(bodySmallEmphasizedFont))
+ val labelLargeEmphasized = FontFamily(Font(labelLargeEmphasizedFont))
+ val labelMediumEmphasized = FontFamily(Font(labelMediumEmphasizedFont))
+ val labelSmallEmphasized = FontFamily(Font(labelSmallEmphasizedFont))
}
internal data class TypefaceNames
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
index 38aadb8..4115647 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
@@ -18,7 +18,10 @@
import androidx.compose.ui.text.TextStyle
-internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
+internal class TypographyTokens(
+ typeScaleTokens: TypeScaleTokens,
+ variableTypeScaleTokens: VariableFontTypeScaleEmphasizedTokens,
+) {
val bodyLarge =
TextStyle(
fontFamily = typeScaleTokens.bodyLargeFont,
@@ -139,4 +142,112 @@
lineHeight = typeScaleTokens.titleSmallLineHeight,
letterSpacing = typeScaleTokens.titleSmallTracking,
)
+ // GSF emphasized styles
+ // note: we don't need to define fontWeight or axes values because they are pre-defined
+ // as part of the font family in fonts_customization.xml (for performance optimization)
+ val displayLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displayLargeFont,
+ fontSize = variableTypeScaleTokens.displayLargeSize,
+ lineHeight = variableTypeScaleTokens.displayLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.displayLargeTracking,
+ )
+ val displayMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displayMediumFont,
+ fontSize = variableTypeScaleTokens.displayMediumSize,
+ lineHeight = variableTypeScaleTokens.displayMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.displayMediumTracking,
+ )
+ val displaySmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displaySmallFont,
+ fontSize = variableTypeScaleTokens.displaySmallSize,
+ lineHeight = variableTypeScaleTokens.displaySmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.displaySmallTracking,
+ )
+ val headlineLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineLargeFont,
+ fontSize = variableTypeScaleTokens.headlineLargeSize,
+ lineHeight = variableTypeScaleTokens.headlineLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineLargeTracking,
+ )
+ val headlineMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineMediumFont,
+ fontSize = variableTypeScaleTokens.headlineMediumSize,
+ lineHeight = variableTypeScaleTokens.headlineMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineMediumTracking,
+ )
+ val headlineSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineSmallFont,
+ fontSize = variableTypeScaleTokens.headlineSmallSize,
+ lineHeight = variableTypeScaleTokens.headlineSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineSmallTracking,
+ )
+ val titleLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleLargeFont,
+ fontSize = variableTypeScaleTokens.titleLargeSize,
+ lineHeight = variableTypeScaleTokens.titleLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleLargeTracking,
+ )
+ val titleMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleMediumFont,
+ fontSize = variableTypeScaleTokens.titleMediumSize,
+ lineHeight = variableTypeScaleTokens.titleMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleMediumTracking,
+ )
+ val titleSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleSmallFont,
+ fontSize = variableTypeScaleTokens.titleSmallSize,
+ lineHeight = variableTypeScaleTokens.titleSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleSmallTracking,
+ )
+ val bodyLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodyLargeFont,
+ fontSize = variableTypeScaleTokens.bodyLargeSize,
+ lineHeight = variableTypeScaleTokens.bodyLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodyLargeTracking,
+ )
+ val bodyMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodyMediumFont,
+ fontSize = variableTypeScaleTokens.bodyMediumSize,
+ lineHeight = variableTypeScaleTokens.bodyMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodyMediumTracking,
+ )
+ val bodySmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodySmallFont,
+ fontSize = variableTypeScaleTokens.bodySmallSize,
+ lineHeight = variableTypeScaleTokens.bodySmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodySmallTracking,
+ )
+ val labelLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelLargeFont,
+ fontSize = variableTypeScaleTokens.labelLargeSize,
+ lineHeight = variableTypeScaleTokens.labelLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelLargeTracking,
+ )
+ val labelMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelMediumFont,
+ fontSize = variableTypeScaleTokens.labelMediumSize,
+ lineHeight = variableTypeScaleTokens.labelMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelMediumTracking,
+ )
+ val labelSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelSmallFont,
+ fontSize = variableTypeScaleTokens.labelSmallSize,
+ lineHeight = variableTypeScaleTokens.labelSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelSmallTracking,
+ )
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
new file mode 100644
index 0000000..52b9390
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.theme.typography
+
+import androidx.compose.ui.unit.sp
+
+internal class VariableFontTypeScaleEmphasizedTokens(typefaceTokens: TypefaceTokens) {
+ val bodyLargeFont = typefaceTokens.bodyLargeEmphasized
+ val bodyLargeLineHeight = 24.0.sp
+ val bodyLargeSize = 16.sp
+ val bodyLargeTracking = 0.0.sp
+ val bodyMediumFont = typefaceTokens.bodyMediumEmphasized
+ val bodyMediumLineHeight = 20.0.sp
+ val bodyMediumSize = 14.sp
+ val bodyMediumTracking = 0.0.sp
+ val bodySmallFont = typefaceTokens.bodySmallEmphasized
+ val bodySmallLineHeight = 16.0.sp
+ val bodySmallSize = 12.sp
+ val bodySmallTracking = 0.0.sp
+ val displayLargeFont = typefaceTokens.displayLargeEmphasized
+ val displayLargeLineHeight = 64.0.sp
+ val displayLargeSize = 57.sp
+ val displayLargeTracking = 0.0.sp
+ val displayMediumFont = typefaceTokens.displayMediumEmphasized
+ val displayMediumLineHeight = 52.0.sp
+ val displayMediumSize = 45.sp
+ val displayMediumTracking = 0.0.sp
+ val displaySmallFont = typefaceTokens.displaySmallEmphasized
+ val displaySmallLineHeight = 44.0.sp
+ val displaySmallSize = 36.sp
+ val displaySmallTracking = 0.0.sp
+ val headlineLargeFont = typefaceTokens.headlineLargeEmphasized
+ val headlineLargeLineHeight = 40.0.sp
+ val headlineLargeSize = 32.sp
+ val headlineLargeTracking = 0.0.sp
+ val headlineMediumFont = typefaceTokens.headlineMediumEmphasized
+ val headlineMediumLineHeight = 36.0.sp
+ val headlineMediumSize = 28.sp
+ val headlineMediumTracking = 0.0.sp
+ val headlineSmallFont = typefaceTokens.headlineSmallEmphasized
+ val headlineSmallLineHeight = 32.0.sp
+ val headlineSmallSize = 24.sp
+ val headlineSmallTracking = 0.0.sp
+ val labelLargeFont = typefaceTokens.labelLargeEmphasized
+ val labelLargeLineHeight = 20.0.sp
+ val labelLargeSize = 14.sp
+ val labelLargeTracking = 0.0.sp
+ val labelMediumFont = typefaceTokens.labelMediumEmphasized
+ val labelMediumLineHeight = 16.0.sp
+ val labelMediumSize = 12.sp
+ val labelMediumTracking = 0.0.sp
+ val labelSmallFont = typefaceTokens.labelSmallEmphasized
+ val labelSmallLineHeight = 16.0.sp
+ val labelSmallSize = 11.sp
+ val labelSmallTracking = 0.0.sp
+ val titleLargeFont = typefaceTokens.titleLargeEmphasized
+ val titleLargeLineHeight = 28.0.sp
+ val titleLargeSize = 22.sp
+ val titleLargeTracking = 0.0.sp
+ val titleMediumFont = typefaceTokens.titleMediumEmphasized
+ val titleMediumLineHeight = 24.0.sp
+ val titleMediumSize = 16.sp
+ val titleMediumTracking = 0.0.sp
+ val titleSmallFont = typefaceTokens.titleSmallEmphasized
+ val titleSmallLineHeight = 20.0.sp
+ val titleSmallSize = 14.sp
+ val titleSmallTracking = 0.0.sp
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
index 48dee24..f1b273a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
@@ -24,6 +24,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
@@ -102,6 +103,8 @@
viewModel,
dialogFactory,
Modifier.element(Bouncer.Elements.Content)
+ // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+ .testTag(Bouncer.Elements.Content.testTag)
.overscroll(verticalOverscrollEffect)
.sysuiResTag(Bouncer.TestTags.Root)
.fillMaxSize(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 5e61af6..aa07370 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -19,6 +19,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
@@ -55,7 +56,11 @@
@Composable
override fun ContentScope.Content(modifier: Modifier) {
- LockscreenScene(lockscreenContent = lockscreenContent, modifier = modifier)
+ LockscreenScene(
+ lockscreenContent = lockscreenContent,
+ // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+ modifier = modifier.testTag(key.rootElementKey.testTag),
+ )
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index b11c83c..4b3ebc2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -35,9 +35,9 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon as MaterialIcon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -73,11 +73,15 @@
import com.android.systemui.res.R
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VolumeSlider(
state: SliderState,
@@ -102,17 +106,6 @@
return
}
- val value by valueState(state)
- val interactionSource = remember { MutableInteractionSource() }
- val hapticsViewModel: SliderHapticsViewModel? =
- setUpHapticsViewModel(
- value,
- state.valueRange,
- state.hapticFilter,
- interactionSource,
- hapticsViewModelFactory,
- )
-
Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -134,60 +127,30 @@
)
button?.invoke()
}
+
Slider(
- value = value,
+ value = state.value,
valueRange = state.valueRange,
- onValueChange = { newValue ->
- hapticsViewModel?.addVelocityDataPoint(newValue)
- onValueChange(newValue)
- },
- onValueChangeFinished = {
- hapticsViewModel?.onValueChangeEnded()
- onValueChangeFinished?.invoke()
- },
- enabled = state.isEnabled,
+ onValueChanged = onValueChange,
+ onValueChangeFinished = { onValueChangeFinished?.invoke() },
+ isEnabled = state.isEnabled,
+ stepDistance = state.a11yStep,
+ accessibilityParams =
+ AccessibilityParams(
+ label = state.label,
+ disabledMessage = state.disabledMessage,
+ currentStateDescription = state.a11yStateDescription,
+ ),
+ haptics =
+ hapticsViewModelFactory?.let {
+ Haptics.Enabled(
+ hapticsViewModelFactory = it,
+ hapticFilter = state.hapticFilter,
+ orientation = Orientation.Horizontal,
+ )
+ } ?: Haptics.Disabled,
modifier =
- Modifier.height(40.dp)
- .padding(top = 4.dp, bottom = 12.dp)
- .sysuiResTag(state.label)
- .clearAndSetSemantics {
- if (state.isEnabled) {
- contentDescription = state.label
- state.a11yClickDescription?.let {
- customActions =
- listOf(
- CustomAccessibilityAction(it) {
- onIconTapped()
- true
- }
- )
- }
-
- state.a11yStateDescription?.let { stateDescription = it }
- progressBarRangeInfo =
- ProgressBarRangeInfo(state.value, state.valueRange)
- } else {
- disabled()
- contentDescription =
- state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
- }
- setProgress { targetValue ->
- val targetDirection =
- when {
- targetValue > value -> 1
- targetValue < value -> -1
- else -> 0
- }
-
- val newValue =
- (value + targetDirection * state.a11yStep).coerceIn(
- state.valueRange.start,
- state.valueRange.endInclusive,
- )
- onValueChange(newValue)
- true
- }
- },
+ Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label),
)
state.disabledMessage?.let { disabledMessage ->
AnimatedVisibility(visible = !state.isEnabled) {
@@ -348,7 +311,7 @@
}
@Composable
-fun setUpHapticsViewModel(
+private fun setUpHapticsViewModel(
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
hapticFilter: SliderHapticFeedbackFilter,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 907b5bc..05958a2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -169,7 +169,7 @@
Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
}
.then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
- .testTag(key.testTag)
+ .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
}
/**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 53d0ee1..404f1b2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -66,6 +66,8 @@
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
+ // TODO(b/240432457) Remove this once test utils can access the internal STLForTesting().
+ implicitTestTags: Boolean = false,
builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
) {
SceneTransitionLayoutForTesting(
@@ -74,6 +76,7 @@
swipeSourceDetector,
swipeDetector,
transitionInterceptionThreshold,
+ implicitTestTags = implicitTestTags,
onLayoutImpl = null,
builder = builder,
)
@@ -727,10 +730,8 @@
}
/**
- * An internal version of [SceneTransitionLayout] to be used for tests.
- *
- * Important: You should use this only in tests and if you need to access the underlying
- * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ * An internal version of [SceneTransitionLayout] to be used for tests, that provides access to the
+ * internal [SceneTransitionLayoutImpl] and implicitly tags all scenes and elements.
*/
@Composable
internal fun SceneTransitionLayoutForTesting(
@@ -743,6 +744,7 @@
sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
ancestors: List<Ancestor> = remember { emptyList() },
lookaheadScope: LookaheadScope? = null,
+ implicitTestTags: Boolean = true,
builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
) {
val density = LocalDensity.current
@@ -767,6 +769,7 @@
directionChangeSlop = directionChangeSlop,
defaultEffectFactory = defaultEffectFactory,
decayAnimationSpec = decayAnimationSpec,
+ implicitTestTags = implicitTestTags,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 53996d2..e3c4eb0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -122,6 +122,9 @@
* This is used to enable transformations and shared elements across NestedSTLs.
*/
internal val ancestors: List<Ancestor> = emptyList(),
+
+ /** Whether elements and scene should be tagged using `Modifier.testTag`. */
+ internal val implicitTestTags: Boolean = false,
lookaheadScope: LookaheadScope? = null,
defaultEffectFactory: OverscrollFactory,
) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 9ca45fe..149a9e7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -173,7 +173,7 @@
.thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
Modifier.container(containerState)
}
- .testTag(key.testTag)
+ .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
) {
CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
scope.content()
@@ -301,6 +301,7 @@
sharedElementMap = layoutImpl.elements,
ancestors = ancestors,
lookaheadScope = layoutImpl.lookaheadScope,
+ implicitTestTags = layoutImpl.implicitTestTags,
)
}
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 338fb9b..86cbfe4 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -227,7 +227,7 @@
to = SceneB,
transitionLayout = { state ->
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
// Transformed element
@@ -633,7 +633,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) }
scene(SceneB) {}
}
@@ -674,7 +674,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(Modifier.fillMaxSize())
}
@@ -734,7 +734,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(
Modifier.overscroll(verticalOverscrollEffect)
@@ -834,7 +834,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(Modifier.fillMaxSize())
}
@@ -893,7 +893,7 @@
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(
+ SceneTransitionLayoutForTesting(
state = state,
modifier = Modifier.size(layoutWidth, layoutHeight),
) {
@@ -970,7 +970,7 @@
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- SceneTransitionLayout(
+ SceneTransitionLayoutForTesting(
state = state,
modifier = Modifier.size(layoutWidth, layoutHeight),
) {
@@ -1057,7 +1057,7 @@
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
Box(
@@ -1374,7 +1374,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA) {
Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
}
@@ -1742,7 +1742,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Foo(offset = 0.dp) }
scene(SceneB) { Foo(offset = 20.dp) }
scene(SceneC) { Foo(offset = 40.dp) }
@@ -1828,7 +1828,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
// Define A after B so that Foo is placed in A during A <=> B.
@@ -1887,7 +1887,7 @@
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) { Foo() }
scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index 04c762f..98ecb64 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -90,7 +90,7 @@
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA) { Foo() }
}
@@ -132,7 +132,7 @@
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA) { Foo() }
overlay(OverlayB) { Foo() }
@@ -230,7 +230,7 @@
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } }
overlay(OverlayA) { MovableBar() }
overlay(OverlayB) { MovableBar() }
@@ -302,7 +302,7 @@
}
var alignment by mutableStateOf(Alignment.Center)
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA, alignment = alignment) { Foo() }
}
@@ -761,7 +761,7 @@
val movableElementChildTag = "movableElementChildTag"
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
MovableElement(key, Modifier) {
content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index 2bf2358..366b11d 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -250,7 +250,7 @@
}
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) }
overlay(OverlayA) { Box(Modifier.fillMaxSize()) }
overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index d7f7a51..fa7661b6 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -97,7 +97,7 @@
MutableSceneTransitionLayoutStateForTests(SceneA, EmptyTestTransitions)
}
- SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) {
+ SceneTransitionLayoutForTesting(state = layoutState, modifier = Modifier.size(LayoutSize)) {
scene(SceneA, userActions = mapOf(Back to SceneB)) {
Box(Modifier.fillMaxSize()) {
SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 751b314..11abbbe 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -763,7 +763,7 @@
var touchSlop = 0f
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
Box(Modifier.fillMaxSize())
}
@@ -837,7 +837,7 @@
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
Box(Modifier.fillMaxSize())
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
index bb511bc..8b56892 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
@@ -40,7 +40,7 @@
import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests
import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
import com.android.compose.animation.scene.SceneTransitions
import com.android.compose.animation.scene.TestScenes
import com.android.compose.animation.scene.testNestedTransition
@@ -114,7 +114,7 @@
@Composable
(states: List<MutableSceneTransitionLayoutState>) -> Unit =
{ states ->
- SceneTransitionLayout(states[0]) {
+ SceneTransitionLayoutForTesting(states[0]) {
scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) })
scene(
TestScenes.SceneB,
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index 6d47bab..e56d1be 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -30,5 +30,7 @@
content: @Composable ContentScope.() -> Unit,
) {
val state = rememberMutableSceneTransitionLayoutState(currentScene)
- SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
+ SceneTransitionLayout(state, modifier, implicitTestTags = true) {
+ scene(currentScene, content = content)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index f94a7ed..a362a37 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -137,7 +137,7 @@
},
changeState = changeState,
transitionLayout = { state ->
- SceneTransitionLayout(state, layoutModifier) {
+ SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
@@ -163,7 +163,7 @@
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(fromScene) { fromSceneContent() }
overlay(overlay) { overlayContent() }
}
@@ -191,7 +191,7 @@
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(toScene) { toSceneContent() }
overlay(overlay) { overlayContent() }
}
@@ -223,7 +223,7 @@
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(currentScene) { currentSceneContent() }
overlay(from, alignment = fromAlignment) { fromContent() }
overlay(to, alignment = toAlignment) { toContent() }
@@ -273,7 +273,7 @@
}
}
- SceneTransitionLayout(state, layoutModifier) {
+ SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index aad1276..654478a 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -28,6 +28,7 @@
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.FlexClockController.Companion.AXIS_PRESETS
import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes
private val TAG = DefaultClockProvider::class.simpleName
@@ -98,16 +99,16 @@
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- val fontAxes =
- if (!isClockReactiveVariantsEnabled) listOf()
- else getDefaultAxes(settings).merge(settings.axes)
return ClockPickerConfig(
settings.clockId ?: DEFAULT_CLOCK_ID,
resources.getString(R.string.clock_default_name),
resources.getString(R.string.clock_default_description),
resources.getDrawable(R.drawable.clock_default_thumbnail, null),
isReactiveToTone = true,
- axes = fontAxes,
+ axes =
+ if (!isClockReactiveVariantsEnabled) emptyList()
+ else getDefaultAxes(settings).merge(settings.axes),
+ axisPresets = if (!isClockReactiveVariantsEnabled) emptyList() else AXIS_PRESETS,
)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index ac1c5a8..1a1033b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -132,7 +132,7 @@
listOf(
GSFAxes.WEIGHT.toClockAxis(
type = AxisType.Float,
- currentValue = 400f,
+ currentValue = 475f,
name = "Weight",
description = "Glyph Weight",
),
@@ -161,5 +161,59 @@
GSFAxes.ROUND.toClockAxisSetting(100f),
GSFAxes.SLANT.toClockAxisSetting(0f),
)
+
+ val AXIS_PRESETS =
+ listOf(
+ FONT_AXES.map { it.toSetting() },
+ LEGACY_FLEX_SETTINGS,
+ listOf( // Porcelain
+ GSFAxes.WEIGHT.toClockAxisSetting(500f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Midnight
+ GSFAxes.WEIGHT.toClockAxisSetting(300f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-10f),
+ ),
+ listOf( // Sterling
+ GSFAxes.WEIGHT.toClockAxisSetting(1000f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Smoky Green
+ GSFAxes.WEIGHT.toClockAxisSetting(150f),
+ GSFAxes.WIDTH.toClockAxisSetting(50f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Iris
+ GSFAxes.WEIGHT.toClockAxisSetting(500f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Margarita
+ GSFAxes.WEIGHT.toClockAxisSetting(300f),
+ GSFAxes.WIDTH.toClockAxisSetting(30f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-10f),
+ ),
+ listOf( // Raspberry
+ GSFAxes.WEIGHT.toClockAxisSetting(700f),
+ GSFAxes.WIDTH.toClockAxisSetting(140f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-7f),
+ ),
+ listOf( // Ultra Blue
+ GSFAxes.WEIGHT.toClockAxisSetting(850f),
+ GSFAxes.WIDTH.toClockAxisSetting(130f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
index 781e416..ede29d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -26,6 +26,9 @@
import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
@@ -173,6 +176,58 @@
}
@Test
+ fun onCreateUpdatedSession_ignoreNullPackageNameSessions() =
+ kosmos.runTest {
+ val nullPackageSession =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = null
+ appIcon = icon1
+ }
+
+ val wellFormedSession =
+ SessionInfo().apply {
+ sessionId = 2
+ appPackageName = "pkg_name"
+ appIcon = icon2
+ }
+
+ defaultSessions = listOf(wellFormedSession)
+
+ whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(nullPackageSession)
+ whenever(packageInstaller.getSessionInfo(2)).thenReturn(wellFormedSession)
+
+ val packageInstallerMonitor =
+ PackageInstallerMonitor(
+ handler,
+ backgroundScope,
+ logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+ packageInstaller,
+ )
+
+ val sessions by collectLastValue(packageInstallerMonitor.installSessionsForPrimaryUser)
+
+ // Verify flow updated with the new session
+ assertThat(sessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // New session added
+ callback.onCreated(nullPackageSession.sessionId)
+
+ // Verify flow updated with the new session
+ assertThat(sessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+ }
+
+ @Test
fun installSessions_newSessionsAreAdded() =
testScope.runTest {
val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
index e53155d..ed73d89 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -21,6 +21,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalSmartspaceRepository
import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -28,12 +30,12 @@
import com.android.systemui.communal.domain.interactor.setCommunalEnabled
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,46 +44,64 @@
@EnableFlags(FLAG_COMMUNAL_HUB)
@RunWith(AndroidJUnit4::class)
class CommunalOngoingContentStartableTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val mediaRepository = kosmos.fakeCommunalMediaRepository
- private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
+ private var showUmoOnHub = true
- private lateinit var underTest: CommunalOngoingContentStartable
+ private val Kosmos.underTest by
+ Kosmos.Fixture {
+ CommunalOngoingContentStartable(
+ bgScope = applicationCoroutineScope,
+ communalInteractor = communalInteractor,
+ communalMediaRepository = communalMediaRepository,
+ communalSettingsInteractor = communalSettingsInteractor,
+ communalSmartspaceRepository = communalSmartspaceRepository,
+ showUmoOnHub = showUmoOnHub,
+ )
+ }
@Before
fun setUp() {
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- underTest =
- CommunalOngoingContentStartable(
- bgScope = kosmos.applicationCoroutineScope,
- communalInteractor = kosmos.communalInteractor,
- communalMediaRepository = mediaRepository,
- communalSettingsInteractor = kosmos.communalSettingsInteractor,
- communalSmartspaceRepository = smartspaceRepository,
- )
}
@Test
- fun testListenForOngoingContentWhenCommunalIsEnabled() =
- testScope.runTest {
+ fun testListenForOngoingContent() =
+ kosmos.runTest {
underTest.start()
- runCurrent()
- assertThat(mediaRepository.isListening()).isFalse()
- assertThat(smartspaceRepository.isListening()).isFalse()
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
kosmos.setCommunalEnabled(true)
- runCurrent()
- assertThat(mediaRepository.isListening()).isTrue()
- assertThat(smartspaceRepository.isListening()).isTrue()
+ assertThat(fakeCommunalMediaRepository.isListening()).isTrue()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
kosmos.setCommunalEnabled(false)
- runCurrent()
- assertThat(mediaRepository.isListening()).isFalse()
- assertThat(smartspaceRepository.isListening()).isFalse()
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+ }
+
+ @Test
+ fun testListenForOngoingContent_showUmoFalse() =
+ kosmos.runTest {
+ showUmoOnHub = false
+ underTest.start()
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+
+ kosmos.setCommunalEnabled(true)
+
+ // Media listening does not start when UMO is disabled.
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
+
+ kosmos.setCommunalEnabled(false)
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
index 943ada9..4e14fec 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
@@ -18,9 +18,6 @@
import android.animation.Animator
import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
import android.testing.TestableLooper
import android.view.View
import android.widget.SeekBar
@@ -33,7 +30,6 @@
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
-import java.util.Locale
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -65,11 +61,11 @@
fun setUp() {
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_enabled_seekbar_height,
- enabledHeight,
+ enabledHeight
)
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_disabled_seekbar_height,
- disabledHeight,
+ disabledHeight
)
seekBarView = SeekBar(context)
@@ -114,31 +110,14 @@
@Test
fun seekBarProgress() {
- val elapsedTime = 3000
- val duration = (1.5 * 60 * 60 * 1000).toInt()
// WHEN part of the track has been played
- val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true)
+ val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true)
observer.onChanged(data)
// THEN seek bar shows the progress
- assertThat(seekBarView.progress).isEqualTo(elapsedTime)
- assertThat(seekBarView.max).isEqualTo(duration)
+ assertThat(seekBarView.progress).isEqualTo(3000)
+ assertThat(seekBarView.max).isEqualTo(120000)
- val expectedProgress =
- MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(Measure(3, MeasureUnit.SECOND))
- val expectedDuration =
- MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(
- Measure(1, MeasureUnit.HOUR),
- Measure(30, MeasureUnit.MINUTE),
- Measure(0, MeasureUnit.SECOND),
- )
- val desc =
- context.getString(
- R.string.controls_media_seekbar_description,
- expectedProgress,
- expectedDuration,
- )
+ val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
assertThat(seekBarView.contentDescription).isEqualTo(desc)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 917f356..80ce43d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -65,8 +65,7 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
-
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
index fba6151..da3cebd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
@@ -97,6 +98,7 @@
testCoroutineDispatcher,
testCoroutineDispatcher,
testScope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelImplTest"),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index 3db5efc..261e3de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -26,8 +26,6 @@
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
-import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.statusbar.connectivity.AccessPointController
@@ -39,11 +37,8 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
-import org.mockito.kotlin.any
import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
@SmallTest
@EnabledOnRavenwood
@@ -56,31 +51,17 @@
private lateinit var internetDialogManager: InternetDialogManager
private lateinit var controller: AccessPointController
- private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
- private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory
- private lateinit var internetDetailsViewModel: InternetDetailsViewModel
@Before
fun setup() {
internetDialogManager = mock<InternetDialogManager>()
controller = mock<AccessPointController>()
- internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>()
- internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>()
- internetDetailsViewModel =
- InternetDetailsViewModel(
- onLongClick = {},
- accessPointController = mock<AccessPointController>(),
- contentManagerFactory = internetDetailsContentManagerFactory,
- )
- whenever(internetDetailsViewModelFactory.create(any())).thenReturn(internetDetailsViewModel)
-
underTest =
InternetTileUserActionInteractor(
kosmos.testScope.coroutineContext,
internetDialogManager,
controller,
inputHandler,
- internetDetailsViewModelFactory,
)
}
@@ -127,12 +108,4 @@
assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
}
}
-
- @Test
- fun detailsViewModel() =
- kosmos.testScope.runTest {
- assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet")
- assertThat(underTest.detailsViewModel.getSubTitle())
- .isEqualTo("Tab a network to connect")
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
index 0598a8b..4e9b635 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -171,21 +172,6 @@
.isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
}
- @Test
- fun tileDetails() =
- testScope.runTest {
- assertThat(tileUserActionInteractor.detailsViewModel).isNotNull()
- assertThat(tileUserActionInteractor.detailsViewModel?.getTitle())
- .isEqualTo("FakeQSTileUserActionInteractor")
- assertThat(underTest.detailsViewModel).isNotNull()
- assertThat(underTest.detailsViewModel?.getTitle())
- .isEqualTo("FakeQSTileUserActionInteractor")
-
- tileUserActionInteractor.detailsViewModel = null
- assertThat(tileUserActionInteractor.detailsViewModel).isNull()
- assertThat(underTest.detailsViewModel).isNull()
- }
-
private fun createViewModel(
scope: TestScope,
config: QSTileConfig = tileConfig,
@@ -209,6 +195,7 @@
testCoroutineDispatcher,
testCoroutineDispatcher,
scope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelTest"),
)
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
index ece21e1..166e950 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -253,5 +254,6 @@
testCoroutineDispatcher,
testCoroutineDispatcher,
scope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelUserInputTest"),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index c69ebab..baf0aeb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -61,8 +61,7 @@
usingMediaInComposeFragment = false // This is not for the compose fragment
}
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
-
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 559e363..d3f5923 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -73,9 +73,8 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val fakeSceneDataSource = kosmos.fakeSceneDataSource
-
- private val underTest = kosmos.sceneInteractor
+ private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+ private val underTest by lazy { kosmos.sceneInteractor }
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 4a011c0..ccc876c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -50,11 +50,10 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val configurationRepository = kosmos.fakeConfigurationRepository
- private val keyguardRepository = kosmos.fakeKeyguardRepository
- private val sceneInteractor = kosmos.sceneInteractor
+ private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+ private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
-
private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl }
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 37b4688..a832f48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -15,7 +15,9 @@
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.runCurrent
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -51,12 +53,11 @@
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class ShadeHeaderViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
- private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
- private val sceneInteractor = kosmos.sceneInteractor
- private val deviceEntryInteractor = kosmos.deviceEntryInteractor
-
+ private val mobileIconsInteractor by lazy { kosmos.fakeMobileIconsInteractor }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
private val underTest by lazy { kosmos.shadeHeaderViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 3d8da61..70df82d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -22,6 +22,7 @@
import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYSUI_CALLBACKS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -556,9 +557,9 @@
@Test
public void testImmersiveModeChanged() {
final int displayAreaId = 10;
- mCommandQueue.immersiveModeChanged(displayAreaId, true);
+ mCommandQueue.immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
waitForIdleSync();
- verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+ verify(mCallbacks).immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
index 83e26c4..5d8b68e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/BundleEntryTest.kt
@@ -65,8 +65,8 @@
@Test
@EnableFlags(NotificationBundleUi.FLAG_NAME)
- fun getGroupRoot_adapter() {
- assertThat(underTest.entryAdapter.groupRoot).isEqualTo(underTest.entryAdapter)
+ fun isGroupRoot_adapter() {
+ assertThat(underTest.entryAdapter.isGroupRoot).isTrue()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 1f5c672..34dff24 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -542,7 +542,7 @@
@Test
@EnableFlags(NotificationBundleUi.FLAG_NAME)
- public void getGroupRoot_adapter_groupSummary() {
+ public void isGroupRoot_adapter_groupSummary() {
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class);
Notification notification = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
@@ -562,12 +562,12 @@
.build();
entry.setRow(row);
- assertThat(entry.getEntryAdapter().getGroupRoot()).isNull();
+ assertThat(entry.getEntryAdapter().isGroupRoot()).isFalse();
}
@Test
@EnableFlags(NotificationBundleUi.FLAG_NAME)
- public void getGroupRoot_adapter_groupChild() {
+ public void isGroupRoot_adapter_groupChild() {
Notification notification = new Notification.Builder(mContext, "")
.setSmallIcon(R.drawable.ic_person)
.setGroupSummary(true)
@@ -591,7 +591,7 @@
.setParent(groupEntry.build())
.build();
- assertThat(entry.getEntryAdapter().getGroupRoot()).isEqualTo(parent.getEntryAdapter());
+ assertThat(entry.getEntryAdapter().isGroupRoot()).isFalse();
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
index 543f0c7..e40d68e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/DismissibilityCoordinatorTest.kt
@@ -66,7 +66,7 @@
assertTrue(
"Notifs without any flags should be dismissible",
- dismissibilityProvider.isDismissable(entry)
+ dismissibilityProvider.isDismissable(entry.key)
)
}
@@ -96,7 +96,7 @@
assertFalse(
"Non-dismiss Notifs should NOT be dismissible",
- dismissibilityProvider.isDismissable(entry)
+ dismissibilityProvider.isDismissable(entry.key)
)
}
@@ -113,7 +113,7 @@
assertFalse(
"Ongoing Notifs should NOT be dismissible when the device is locked",
- dismissibilityProvider.isDismissable(entry)
+ dismissibilityProvider.isDismissable(entry.key)
)
}
@@ -130,7 +130,7 @@
assertTrue(
"Ongoing Notifs should be dismissible when the device is unlocked",
- dismissibilityProvider.isDismissable(entry)
+ dismissibilityProvider.isDismissable(entry.key)
)
}
@@ -148,7 +148,7 @@
assertFalse(
"Non-dismiss Notifs should NOT be dismissible",
- dismissibilityProvider.isDismissable(entry)
+ dismissibilityProvider.isDismissable(entry.key)
)
}
@@ -174,16 +174,16 @@
assertTrue(
"Notifs without any flags should be dismissible",
- dismissibilityProvider.isDismissable(noFlagEntry)
+ dismissibilityProvider.isDismissable(noFlagEntry.key)
)
assertTrue(
"Ongoing Notifs should be dismissible when the device is unlocked",
- dismissibilityProvider.isDismissable(ongoingEntry)
+ dismissibilityProvider.isDismissable(ongoingEntry.key)
)
assertFalse(
"Non-dismiss Notifs should NOT be dismissible",
- dismissibilityProvider.isDismissable(nonDismissEntry)
+ dismissibilityProvider.isDismissable(nonDismissEntry.key)
)
}
@@ -199,10 +199,13 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+ assertFalse(
+ "Child should be non-dismissible",
+ dismissibilityProvider.isDismissable(entry.key)
+ )
assertFalse(
"Summary should be non-dismissible",
- dismissibilityProvider.isDismissable(summary)
+ dismissibilityProvider.isDismissable(summary.key)
)
}
@@ -219,10 +222,13 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+ assertFalse(
+ "Child should be non-dismissible",
+ dismissibilityProvider.isDismissable(entry.key)
+ )
assertFalse(
"Summary should be non-dismissible",
- dismissibilityProvider.isDismissable(summary)
+ dismissibilityProvider.isDismissable(summary.key)
)
}
@@ -239,8 +245,11 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry))
- assertTrue("Summary should be dismissible", dismissibilityProvider.isDismissable(summary))
+ assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry.key))
+ assertTrue(
+ "Summary should be dismissible",
+ dismissibilityProvider.isDismissable(summary.key)
+ )
}
@Test
@@ -254,7 +263,10 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+ assertFalse(
+ "Child should be non-dismissible",
+ dismissibilityProvider.isDismissable(entry.key)
+ )
}
@Test
@@ -269,7 +281,10 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertFalse("Child should be non-dismissible", dismissibilityProvider.isDismissable(entry))
+ assertFalse(
+ "Child should be non-dismissible",
+ dismissibilityProvider.isDismissable(entry.key)
+ )
}
@Test
@@ -284,7 +299,7 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry))
+ assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry.key))
}
@Test
@@ -299,10 +314,10 @@
onBeforeRenderListListener.onBeforeRenderList(listOf(group))
- assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry))
+ assertTrue("Child should be dismissible", dismissibilityProvider.isDismissable(entry.key))
assertFalse(
"Summary should be non-dismissible",
- dismissibilityProvider.isDismissable(summary)
+ dismissibilityProvider.isDismissable(summary.key)
)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 3dd0982..8e6aedca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.collection.render
import android.os.Build
+import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
@@ -29,9 +30,12 @@
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
@@ -55,56 +59,58 @@
private lateinit var underTest: GroupExpansionManagerImpl
+ private lateinit var testHelper: NotificationTestHelper
private val dumpManager: DumpManager = mock()
private val groupMembershipManager: GroupMembershipManager = mock()
private val pipeline: NotifPipeline = mock()
private lateinit var beforeRenderListListener: OnBeforeRenderListListener
- private val summary1 = notificationSummaryEntry("foo", 1)
- private val summary2 = notificationSummaryEntry("bar", 1)
- private val entries =
- listOf<ListEntry>(
- GroupEntryBuilder()
- .setSummary(summary1)
- .setChildren(
- listOf(
- notificationEntry("foo", 2),
- notificationEntry("foo", 3),
- notificationEntry("foo", 4)
- )
- )
- .build(),
- GroupEntryBuilder()
- .setSummary(summary2)
- .setChildren(
- listOf(
- notificationEntry("bar", 2),
- notificationEntry("bar", 3),
- notificationEntry("bar", 4)
- )
- )
- .build(),
- notificationEntry("baz", 1)
- )
+ private lateinit var summary1: NotificationEntry
+ private lateinit var summary2: NotificationEntry
+ private lateinit var entries: List<ListEntry>
- private fun notificationEntry(pkg: String, id: Int) =
- NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply { row = mock() }
-
- private fun notificationSummaryEntry(pkg: String, id: Int) =
- NotificationEntryBuilder().setPkg(pkg).setId(id).setParent(GroupEntry.ROOT_ENTRY).build()
- .apply { row = mock() }
+ private fun notificationEntry(pkg: String, id: Int, parent: ExpandableNotificationRow?) =
+ NotificationEntryBuilder().setPkg(pkg).setId(id).build().apply {
+ row = testHelper.createRow().apply {
+ setIsChildInGroup(true, parent)
+ }
+ }
@Before
fun setUp() {
+ testHelper = NotificationTestHelper(mContext, mDependency)
+
+ summary1 = testHelper.createRow().entry
+ summary2 = testHelper.createRow().entry
+ entries =
+ listOf<ListEntry>(
+ GroupEntryBuilder()
+ .setSummary(summary1)
+ .setChildren(
+ listOf(
+ notificationEntry("foo", 2, summary1.row),
+ notificationEntry("foo", 3, summary1.row),
+ notificationEntry("foo", 4, summary1.row)
+ )
+ )
+ .build(),
+ GroupEntryBuilder()
+ .setSummary(summary2)
+ .setChildren(
+ listOf(
+ notificationEntry("bar", 2, summary2.row),
+ notificationEntry("bar", 3, summary2.row),
+ notificationEntry("bar", 4, summary2.row)
+ )
+ )
+ .build(),
+ notificationEntry("baz", 1, null)
+ )
+
whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1)
whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2)
- whenever(groupMembershipManager.getGroupRoot(summary1.entryAdapter))
- .thenReturn(summary1.entryAdapter)
- whenever(groupMembershipManager.getGroupRoot(summary2.entryAdapter))
- .thenReturn(summary2.entryAdapter)
-
underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager)
}
@@ -221,4 +227,15 @@
verify(listener).onGroupExpansionChange(summary1.row, false)
verifyNoMoreInteractions(listener)
}
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isGroupExpanded() {
+ underTest.setGroupExpanded(summary1.entryAdapter, true)
+
+ assertThat(underTest.isGroupExpanded(summary1.entryAdapter)).isTrue();
+ assertThat(underTest.isGroupExpanded(
+ (entries[0] as? GroupEntry)?.getChildren()?.get(0)?.entryAdapter))
+ .isTrue();
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
index dcbf44e..2bbf094 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
@@ -170,6 +170,21 @@
@Test
@EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun isChildEntryAdapterInGroup_child() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+ assertThat(underTest.isChildInGroup(entry.entryAdapter)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
fun isGroupRoot_topLevelEntry() {
val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
@@ -203,40 +218,4 @@
assertThat(underTest.isGroupRoot(entry.entryAdapter)).isFalse()
}
-
- @Test
- @EnableFlags(NotificationBundleUi.FLAG_NAME)
- fun getGroupRoot_topLevelEntry() {
- val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
- assertThat(underTest.getGroupRoot(entry.entryAdapter)).isNull()
- }
-
- @Test
- @EnableFlags(NotificationBundleUi.FLAG_NAME)
- fun getGroupRoot_summary() {
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
-
- assertThat(underTest.getGroupRoot(summary.entryAdapter)).isEqualTo(summary.entryAdapter)
- }
-
- @Test
- @EnableFlags(NotificationBundleUi.FLAG_NAME)
- fun getGroupRoot_child() {
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
-
- assertThat(underTest.getGroupRoot(entry.entryAdapter)).isEqualTo(summary.entryAdapter)
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 7603eec..33211d4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -20,6 +20,7 @@
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.RoundableState
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
@@ -56,6 +57,7 @@
private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
private val notificationRow = mock<ExpandableNotificationRow>()
private val notificationEntry = mock<NotificationEntry>()
+ private val notificationEntryAdapter = mock<EntryAdapter>()
private val dumpManager = mock<DumpManager>()
private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
private val notificationShelf = mock<NotificationShelf>()
@@ -109,8 +111,10 @@
Assume.assumeFalse(isTv())
mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
+ whenever(notificationRow.key).thenReturn("key")
whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
whenever(notificationRow.entry).thenReturn(notificationEntry)
+ whenever(notificationRow.entryAdapter).thenReturn(notificationEntryAdapter)
whenever(notificationRow.roundableState)
.thenReturn(RoundableState(notificationRow, notificationRow, 0f))
ambientState.isSmallScreen = true
@@ -452,7 +456,11 @@
@Test
fun resetViewStates_hunsOverlapping_bottomHunClipped() {
val topHun = mockExpandableNotificationRow()
+ whenever(topHun.key).thenReturn("key")
+ whenever(topHun.entryAdapter).thenReturn(notificationEntryAdapter)
val bottomHun = mockExpandableNotificationRow()
+ whenever(bottomHun.key).thenReturn("key")
+ whenever(bottomHun.entryAdapter).thenReturn(notificationEntryAdapter)
whenever(topHun.isHeadsUp).thenReturn(true)
whenever(topHun.isPinned).thenReturn(true)
whenever(bottomHun.isHeadsUp).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt
index bd76268..6a56716 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.kt
@@ -388,7 +388,7 @@
// Pulsing: Enabled
row.isHeadsUp = true
- underTest.updateHeadsUpAndPulsingRoundness(entry)
+ underTest.updateHeadsUpAndPulsingRoundness(row)
val debugString: String = row.roundableState.debugString()
// If Pulsing is enabled, roundness should be set to 1
@@ -397,7 +397,7 @@
// Pulsing: Disabled
row.isHeadsUp = false
- underTest.updateHeadsUpAndPulsingRoundness(entry)
+ underTest.updateHeadsUpAndPulsingRoundness(row)
// If Pulsing is disabled, roundness should be set to 0
assertThat(row.topRoundness.toDouble()).isWithin(0.001).of(0.0)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt
index baea1a1..47967b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt
@@ -54,6 +54,7 @@
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.visualInterruptionDecisionProvider
import com.android.systemui.statusbar.notificationLockscreenUserManager
@@ -293,6 +294,7 @@
@Test
@EnableSceneContainer
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun testExpandSensitiveNotification_onLockScreen_opensShade() =
kosmos.runTest {
// Given we are on the keyguard
@@ -303,11 +305,10 @@
)
// When the user expands a sensitive Notification
- val row = createRow()
- val entry =
- row.entry.apply { setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true) }
+ val row = createRow(createNotificationEntry())
+ row.entry.apply { setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true) }
- underTest.onExpandClicked(entry, mock(), /* nowExpanded= */ true)
+ underTest.onExpandClicked(row.entry, mock(), /* nowExpanded= */ true)
// Then we open the locked shade
verify(kosmos.lockscreenShadeTransitionController)
@@ -317,6 +318,32 @@
@Test
@EnableSceneContainer
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun testExpandSensitiveNotification_onLockScreen_opensShade_entryAdapter() =
+ kosmos.runTest {
+ // Given we are on the keyguard
+ kosmos.sysuiStatusBarStateController.state = StatusBarState.KEYGUARD
+ // And the device is locked
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+
+ // When the user expands a sensitive Notification
+ val entry = createNotificationEntry()
+ val row = createRow(entry)
+ entry.setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true)
+
+ underTest.onExpandClicked(row, row.entryAdapter, /* nowExpanded= */ true)
+
+ // Then we open the locked shade
+ verify(kosmos.lockscreenShadeTransitionController)
+ // Explicit parameters to avoid issues with Kotlin default arguments in Mockito
+ .goToLockedShade(row, true)
+ }
+
+ @Test
+ @EnableSceneContainer
+ @DisableFlags(NotificationBundleUi.FLAG_NAME)
fun testExpandSensitiveNotification_onLockedShade_showsBouncer() =
kosmos.runTest {
// Given we are on the locked shade
@@ -328,7 +355,7 @@
// When the user expands a sensitive Notification
val entry =
- createRow().entry.apply {
+ createRow(createNotificationEntry()).entry.apply {
setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true)
}
underTest.onExpandClicked(entry, mock(), /* nowExpanded= */ true)
@@ -337,6 +364,29 @@
verify(kosmos.activityStarter).dismissKeyguardThenExecute(any(), eq(null), eq(false))
}
+ @Test
+ @EnableSceneContainer
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun testExpandSensitiveNotification_onLockedShade_showsBouncer_entryAdapter() =
+ kosmos.runTest {
+ // Given we are on the locked shade
+ kosmos.sysuiStatusBarStateController.state = StatusBarState.SHADE_LOCKED
+ // And the device is locked
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+
+ // When the user expands a sensitive Notification
+ val entry = createNotificationEntry()
+ val row = createRow(entry)
+ entry.setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true)
+
+ underTest.onExpandClicked(row, row.entryAdapter, /* nowExpanded= */ true)
+
+ // Then we show the bouncer
+ verify(kosmos.activityStarter).dismissKeyguardThenExecute(any(), eq(null), eq(false))
+ }
+
private fun createPresenter(): StatusBarNotificationPresenter {
val initController: InitController = InitController()
return StatusBarNotificationPresenter(
@@ -398,10 +448,13 @@
interruptSuppressor = suppressorCaptor.lastValue
}
- private fun createRow(): ExpandableNotificationRow {
+ private fun createRow(entry: NotificationEntry): ExpandableNotificationRow {
val row: ExpandableNotificationRow = mock()
- val entry: NotificationEntry = createNotificationEntry()
- whenever(row.entry).thenReturn(entry)
+ if (NotificationBundleUi.isEnabled) {
+ whenever(row.entryAdapter).thenReturn(entry.entryAdapter)
+ } else {
+ whenever(row.entry).thenReturn(entry)
+ }
entry.row = row
return row
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
index 6e4dc14..0cbc30d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -34,6 +34,9 @@
/** Font axes that can be modified on this clock */
val axes: List<ClockFontAxis> = listOf(),
+
+ /** List of font presets for this clock. Can be assigned directly. */
+ val axisPresets: List<List<ClockFontAxisSetting>> = listOf(),
)
/** Represents an Axis that can be modified */
diff --git a/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
new file mode 100644
index 0000000..1de8c2b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid
+ android:color="@color/notification_guts_priority_button_bg_fill" />
+
+ <stroke
+ android:width="1.5dp"
+ android:color="@color/notification_guts_priority_button_bg_stroke" />
+
+ <corners android:radius="16dp" />
+</shape>
diff --git a/packages/SystemUI/res/layout/notification_2025_info.xml b/packages/SystemUI/res/layout/notification_2025_info.xml
new file mode 100644
index 0000000..7b69166
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_info.xml
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2025, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- extends LinearLayout -->
+<com.android.systemui.statusbar.notification.row.NotificationInfo
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/notification_guts"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:clipChildren="false"
+ android:clipToPadding="true"
+ android:orientation="vertical"
+ android:paddingStart="@*android:dimen/notification_2025_margin">
+
+ <!-- Package Info -->
+ <LinearLayout
+ android:id="@+id/header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="true">
+ <ImageView
+ android:id="@+id/pkg_icon"
+ android:layout_width="@*android:dimen/notification_2025_icon_circle_size"
+ android:layout_height="@*android:dimen/notification_2025_icon_circle_size"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:layout_marginEnd="@*android:dimen/notification_2025_margin" />
+ <LinearLayout
+ android:id="@+id/names"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:minHeight="@*android:dimen/notification_2025_icon_circle_size">
+ <TextView
+ android:id="@+id/channel_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ style="@style/TextAppearance.NotificationImportanceChannel"/>
+ <TextView
+ android:id="@+id/group_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ android:ellipsize="end"
+ style="@style/TextAppearance.NotificationImportanceChannelGroup"/>
+ <TextView
+ android:id="@+id/pkg_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceApp"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:maxLines="1"/>
+ <TextView
+ android:id="@+id/delegate_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceHeader"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:text="@string/notification_delegate_header"
+ android:maxLines="1" />
+
+ </LinearLayout>
+
+ <!-- feedback for notificationassistantservice -->
+ <ImageButton
+ android:id="@+id/feedback"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_guts_bundle_feedback"
+ android:src="@*android:drawable/ic_feedback"
+ android:paddingTop="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+
+ <!-- Optional link to app. Only appears if the channel is not disabled and the app
+ asked for it -->
+ <ImageButton
+ android:id="@+id/app_settings"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_app_settings"
+ android:src="@drawable/ic_info"
+ android:paddingTop="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+
+ <!-- System notification settings -->
+ <ImageButton
+ android:id="@+id/info"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:contentDescription="@string/notification_more_settings"
+ android:background="@drawable/ripple_drawable"
+ android:src="@drawable/ic_settings"
+ android:padding="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/inline_controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@*android:dimen/notification_2025_margin"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical">
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_text"
+ android:text="@string/notification_unblockable_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_call_text"
+ android:text="@string/notification_unblockable_call_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_multichannel_text"
+ android:text="@string/notification_multichannel_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <LinearLayout
+ android:id="@+id/interruptiveness_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical">
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/automatic"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/automatic_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:src="@drawable/ic_notifications_automatic"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/automatic_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_automatic_title"/>
+ <TextView
+ android:id="@+id/automatic_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_automatic"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/alert"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/alert_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:src="@drawable/ic_notifications_alert"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/alert_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_alert_title"/>
+ <TextView
+ android:id="@+id/alert_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_default"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/silence"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_separation"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/silence_icon"
+ android:src="@drawable/ic_notifications_silence"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/silence_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_toEndOf="@id/silence_icon"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_silence_title"/>
+ <TextView
+ android:id="@+id/silence_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_low"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/bottom_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:minHeight="@dimen/notification_2025_guts_button_size"
+ android:gravity="center_vertical"
+ >
+ <TextView
+ android:id="@+id/turn_off_notifications"
+ android:text="@string/inline_turn_off_notifications"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="32dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="@*android:dimen/notification_2025_margin"
+ android:gravity="center"
+ android:minWidth="@dimen/notification_2025_min_tap_target_size"
+ android:minHeight="@dimen/notification_2025_min_tap_target_size"
+ android:maxWidth="200dp"
+ style="@style/TextAppearance.NotificationInfo.Button"
+ android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+ <TextView
+ android:id="@+id/done"
+ android:text="@string/inline_ok_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="@*android:dimen/notification_2025_margin"
+ android:gravity="center"
+ android:minWidth="@dimen/notification_2025_min_tap_target_size"
+ android:minHeight="@dimen/notification_2025_min_tap_target_size"
+ android:maxWidth="125dp"
+ style="@style/TextAppearance.NotificationInfo.Button"
+ android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
+</com.android.systemui.statusbar.notification.row.NotificationInfo>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b273886..4995858 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1125,4 +1125,7 @@
<!-- Configuration to swipe to open glanceable hub -->
<bool name="config_swipeToOpenGlanceableHub">false</bool>
+
+ <!-- Whether or not to show the UMO on the glanceable hub when media is playing. -->
+ <bool name="config_showUmoOnHub">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 640e1fa..7c370d3 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -390,6 +390,12 @@
<!-- Extra space for guts bundle feedback button -->
<dimen name="notification_guts_bundle_feedback_size">48dp</dimen>
+ <!-- Size of icon buttons in notification info. -->
+ <!-- 24dp for the icon itself + 16dp * 2 for top and bottom padding -->
+ <dimen name="notification_2025_guts_button_size">56dp</dimen>
+
+ <dimen name="notification_2025_min_tap_target_size">48dp</dimen>
+
<dimen name="notification_importance_toggle_size">48dp</dimen>
<dimen name="notification_importance_button_separation">8dp</dimen>
<dimen name="notification_importance_drawable_padding">8dp</dimen>
@@ -402,6 +408,10 @@
<dimen name="notification_importance_button_description_top_margin">12dp</dimen>
<dimen name="rect_button_radius">8dp</dimen>
+ <!-- Padding for importance selection buttons in notification info, 2025 redesign version -->
+ <dimen name="notification_2025_importance_button_padding_vertical">12dp</dimen>
+ <dimen name="notification_2025_importance_button_padding_horizontal">16dp</dimen>
+
<!-- The minimum height for the snackbar shown after the snooze option has been chosen. -->
<dimen name="snooze_snackbar_min_height">56dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 084495f..6ff1240 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3176,8 +3176,8 @@
<string name="controls_media_settings_button">Settings</string>
<!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
<string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
- <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] -->
- <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string>
+ <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
<!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] -->
<string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string>
@@ -4178,4 +4178,7 @@
<string name="qs_edit_mode_reset_dialog_content">
All Quick Settings tiles will reset to the device’s original settings
</string>
+
+ <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
+ <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
index 208adc2..5f7dca8 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
@@ -64,15 +64,14 @@
synchronized(sessions) {
sessions.putAll(
packageInstaller.allSessions
- .filter { !TextUtils.isEmpty(it.appPackageName) }
- .map { session -> session.toModel() }
+ .mapNotNull { session -> session.toModel() }
.associateBy { it.sessionId }
)
updateInstallerSessionsFlow()
}
packageInstaller.registerSessionCallback(
this@PackageInstallerMonitor,
- bgHandler
+ bgHandler,
)
} else {
synchronized(sessions) {
@@ -130,7 +129,7 @@
if (session == null) {
sessions.remove(sessionId)
} else {
- sessions[sessionId] = session.toModel()
+ session.toModel()?.apply { sessions[sessionId] = this }
}
updateInstallerSessionsFlow()
}
@@ -144,7 +143,11 @@
companion object {
const val TAG = "PackageInstallerMonitor"
- private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+ private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession? {
+ if (TextUtils.isEmpty(this.appPackageName)) {
+ return null
+ }
+
return PackageInstallSession(
sessionId = this.sessionId,
packageName = this.appPackageName,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
index 48a6d9d..7765d00 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
@@ -16,7 +16,9 @@
package com.android.systemui.communal
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.CoreStartable
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SHOW_UMO
import com.android.systemui.communal.data.repository.CommunalMediaRepository
import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -24,8 +26,8 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
@SysUISingleton
class CommunalOngoingContentStartable
@@ -36,6 +38,7 @@
private val communalMediaRepository: CommunalMediaRepository,
private val communalSettingsInteractor: CommunalSettingsInteractor,
private val communalSmartspaceRepository: CommunalSmartspaceRepository,
+ @Named(SHOW_UMO) private val showUmoOnHub: Boolean,
) : CoreStartable {
override fun start() {
@@ -46,10 +49,14 @@
bgScope.launch {
communalInteractor.isCommunalEnabled.collect { enabled ->
if (enabled) {
- communalMediaRepository.startListening()
+ if (showUmoOnHub) {
+ communalMediaRepository.startListening()
+ }
communalSmartspaceRepository.startListening()
} else {
- communalMediaRepository.stopListening()
+ if (showUmoOnHub) {
+ communalMediaRepository.stopListening()
+ }
communalSmartspaceRepository.stopListening()
}
}
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 ff74162..bb3be53 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -105,6 +105,7 @@
const val LOGGABLE_PREFIXES = "loggable_prefixes"
const val LAUNCHER_PACKAGE = "launcher_package"
const val SWIPE_TO_HUB = "swipe_to_hub"
+ const val SHOW_UMO = "show_umo"
@Provides
@Communal
@@ -150,5 +151,11 @@
fun provideSwipeToHub(@Main resources: Resources): Boolean {
return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub)
}
+
+ @Provides
+ @Named(SHOW_UMO)
+ fun provideShowUmo(@Main resources: Resources): Boolean {
+ return resources.getBoolean(R.bool.config_showUmoOnHub)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 2650159..15a4722 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,8 +29,6 @@
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
@@ -52,8 +50,6 @@
NotificationMinimalism.token dependsOn NotificationThrottleHun.token
ModesEmptyShadeFix.token dependsOn modesUi
- PromotedNotificationUiAod.token dependsOn PromotedNotificationUi.token
-
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index d3b76a5..a42682b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -36,6 +36,7 @@
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockPreviewConfig
import com.android.systemui.shared.clocks.ClockRegistry
+import kotlinx.coroutines.flow.combine
/** Binder for the small clock view, large clock view. */
object KeyguardPreviewClockViewBinder {
@@ -76,38 +77,39 @@
repeatOnLifecycle(Lifecycle.State.STARTED) {
var lastClock: ClockController? = null
launch("$TAG#viewModel.previewClock") {
- viewModel.previewClock.collect { currentClock ->
- lastClock?.let { clock ->
- (clock.largeClock.layout.views + clock.smallClock.layout.views)
- .forEach { rootView.removeView(it) }
- }
- lastClock = currentClock
- updateClockAppearance(
- currentClock,
- clockPreviewConfig.context.resources,
- )
+ combine(viewModel.previewClock, viewModel.selectedClockSize, ::Pair)
+ .collect { (currentClock, clockSize) ->
+ lastClock?.let { clock ->
+ (clock.largeClock.layout.views + clock.smallClock.layout.views)
+ .forEach { rootView.removeView(it) }
+ }
+ lastClock = currentClock
+ updateClockAppearance(
+ currentClock,
+ clockPreviewConfig.context.resources,
+ )
- if (viewModel.shouldHighlightSelectedAffordance) {
- (currentClock.largeClock.layout.views +
- currentClock.smallClock.layout.views)
- .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
- }
- currentClock.largeClock.layout.views.forEach {
- (it.parent as? ViewGroup)?.removeView(it)
- rootView.addView(it)
- }
+ if (viewModel.shouldHighlightSelectedAffordance) {
+ (currentClock.largeClock.layout.views +
+ currentClock.smallClock.layout.views)
+ .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA }
+ }
+ currentClock.largeClock.layout.views.forEach {
+ (it.parent as? ViewGroup)?.removeView(it)
+ rootView.addView(it)
+ }
- currentClock.smallClock.layout.views.forEach {
- (it.parent as? ViewGroup)?.removeView(it)
- rootView.addView(it)
+ currentClock.smallClock.layout.views.forEach {
+ (it.parent as? ViewGroup)?.removeView(it)
+ rootView.addView(it)
+ }
+ applyPreviewConstraints(
+ clockPreviewConfig,
+ rootView,
+ currentClock,
+ clockSize,
+ )
}
- applyPreviewConstraints(
- clockPreviewConfig,
- rootView,
- currentClock,
- viewModel,
- )
- }
}
.invokeOnCompletion {
// recover seed color especially for Transit clock
@@ -133,7 +135,7 @@
clockPreviewConfig: ClockPreviewConfig,
rootView: ConstraintLayout,
previewClock: ClockController,
- viewModel: KeyguardPreviewClockViewModel,
+ clockSize: ClockSizeSetting?,
) {
val cs = ConstraintSet().apply { clone(rootView) }
@@ -147,16 +149,15 @@
previewClock.largeClock.layout.applyPreviewConstraints(configWithUpdatedLockId, cs)
previewClock.smallClock.layout.applyPreviewConstraints(configWithUpdatedLockId, cs)
- // When selectedClockSize is the initial value, make both clocks invisible to avoid
- // flickering
+ // When selectedClockSize is the initial value, make both clocks invisible to avoid flicker
val largeClockVisibility =
- when (viewModel.selectedClockSize.value) {
+ when (clockSize) {
ClockSizeSetting.DYNAMIC -> VISIBLE
ClockSizeSetting.SMALL -> INVISIBLE
null -> INVISIBLE
}
val smallClockVisibility =
- when (viewModel.selectedClockSize.value) {
+ when (clockSize) {
ClockSizeSetting.DYNAMIC -> INVISIBLE
ClockSizeSetting.SMALL -> VISIBLE
null -> INVISIBLE
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
index c9716be..34f7c4d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
@@ -18,9 +18,6 @@
import android.animation.Animator
import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
import android.text.format.DateUtils
import androidx.annotation.UiThread
import androidx.lifecycle.Observer
@@ -31,11 +28,8 @@
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
-import java.util.Locale
private const val TAG = "SeekBarObserver"
-private const val MIN_IN_SEC = 60
-private const val HOUR_IN_SEC = MIN_IN_SEC * 60
/**
* Observer for changes from SeekBarViewModel.
@@ -133,9 +127,10 @@
}
holder.seekBar.setMax(data.duration)
- val totalTimeDescription = formatTimeContentDescription(data.duration)
+ val totalTimeString =
+ DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
- holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration)
+ holder.scrubbingTotalTimeView.text = totalTimeString
}
data.elapsedTime?.let {
@@ -153,62 +148,20 @@
}
}
- val elapsedTimeDescription = formatTimeContentDescription(it)
+ val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
- holder.scrubbingElapsedTimeView.text = formatTimeLabel(it)
+ holder.scrubbingElapsedTimeView.text = elapsedTimeString
}
holder.seekBar.contentDescription =
holder.seekBar.context.getString(
R.string.controls_media_seekbar_description,
- elapsedTimeDescription,
- totalTimeDescription,
+ elapsedTimeString,
+ totalTimeString
)
}
}
- /** Returns a time string suitable for display, e.g. "12:34" */
- private fun formatTimeLabel(milliseconds: Int): CharSequence {
- return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS)
- }
-
- /**
- * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds"
- *
- * Follows same logic as Chronometer#formatDuration
- */
- private fun formatTimeContentDescription(milliseconds: Int): CharSequence {
- var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS
-
- val hours =
- if (seconds >= HOUR_IN_SEC) {
- seconds / HOUR_IN_SEC
- } else {
- 0
- }
- seconds -= hours * HOUR_IN_SEC
-
- val minutes =
- if (seconds >= MIN_IN_SEC) {
- seconds / MIN_IN_SEC
- } else {
- 0
- }
- seconds -= minutes * MIN_IN_SEC
-
- val measures = arrayListOf<Measure>()
- if (hours > 0) {
- measures.add(Measure(hours, MeasureUnit.HOUR))
- }
- if (minutes > 0) {
- measures.add(Measure(minutes, MeasureUnit.MINUTE))
- }
- measures.add(Measure(seconds, MeasureUnit.SECOND))
-
- return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(*measures.toTypedArray())
- }
-
@VisibleForTesting
open fun buildResetAnimator(targetTime: Int): Animator {
val animator =
@@ -216,7 +169,7 @@
holder.seekBar,
"progress",
holder.seekBar.progress,
- targetTime + RESET_ANIMATION_DURATION_MS,
+ targetTime + RESET_ANIMATION_DURATION_MS
)
animator.setAutoCancel(true)
animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 9d37580..1f2f571 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -648,6 +648,10 @@
final MediaDevice connectedMediaDevice =
needToHandleMutingExpectedDevice ? null
: getCurrentConnectedMediaDevice();
+
+ Set<String> selectedDevicesIds = getSelectedMediaDevice().stream()
+ .map(MediaDevice::getId)
+ .collect(Collectors.toSet());
if (oldMediaItems.isEmpty()) {
if (connectedMediaDevice == null) {
if (DEBUG) {
@@ -656,12 +660,14 @@
return categorizeMediaItemsLocked(
/* connectedMediaDevice */ null,
devices,
+ selectedDevicesIds,
needToHandleMutingExpectedDevice);
} else {
// selected device exist
return categorizeMediaItemsLocked(
connectedMediaDevice,
devices,
+ selectedDevicesIds,
/* needToHandleMutingExpectedDevice */ false);
}
}
@@ -695,9 +701,20 @@
devices.removeAll(targetMediaDevices);
targetMediaDevices.addAll(devices);
}
- List<MediaItem> finalMediaItems = targetMediaDevices.stream()
- .map(MediaItem::createDeviceMediaItem)
- .collect(Collectors.toList());
+ List<MediaItem> finalMediaItems = new ArrayList<>();
+ boolean shouldAddFirstSeenSelectedDevice =
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
+ for (MediaDevice targetMediaDevice : targetMediaDevices) {
+ if (shouldAddFirstSeenSelectedDevice
+ && selectedDevicesIds.contains(targetMediaDevice.getId())) {
+ finalMediaItems.add(MediaItem.createDeviceMediaItem(
+ targetMediaDevice, /* isFirstDeviceInGroup */ true));
+ shouldAddFirstSeenSelectedDevice = false;
+ } else {
+ finalMediaItems.add(MediaItem.createDeviceMediaItem(
+ targetMediaDevice, /* isFirstDeviceInGroup */ false));
+ }
+ }
dividerItems.forEach(finalMediaItems::add);
attachConnectNewDeviceItemIfNeeded(finalMediaItems);
return finalMediaItems;
@@ -724,11 +741,9 @@
@GuardedBy("mMediaDevicesLock")
private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice,
List<MediaDevice> devices,
+ Set<String> selectedDevicesIds,
boolean needToHandleMutingExpectedDevice) {
List<MediaItem> finalMediaItems = new ArrayList<>();
- Set<String> selectedDevicesIds = getSelectedMediaDevice().stream()
- .map(MediaDevice::getId)
- .collect(Collectors.toSet());
if (connectedMediaDevice != null) {
selectedDevicesIds.add(connectedMediaDevice.getId());
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 71eacdf..e0b93fb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -252,8 +252,12 @@
Box(
modifier =
Modifier.graphicsLayer { alpha = viewModel.viewAlpha }
+ .thenIf(notificationScrimClippingParams.isEnabled) {
+ Modifier.notificationScrimClip {
+ notificationScrimClippingParams.params
+ }
+ }
.thenIf(!Flags.notificationShadeBlur()) {
- // Clipping before translation to match QSContainerImpl.onDraw
Modifier.offset {
IntOffset(
x = 0,
@@ -261,11 +265,6 @@
)
}
}
- .thenIf(notificationScrimClippingParams.isEnabled) {
- Modifier.notificationScrimClip {
- notificationScrimClippingParams.params
- }
- }
// Disable touches in the whole composable while the mirror is
// showing. While the mirror is showing, an ancestor of the
// ComposeView is made alpha 0, but touches are still being captured
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index f80b8fb..e48e943 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -99,7 +99,7 @@
}
override fun getDetailsViewModel(): TileDetailsViewModel {
- return internetDetailsViewModelFactory.create { longClick(null) }
+ return internetDetailsViewModelFactory.create()
}
override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
index e8c4274..8ad4e16 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
@@ -28,17 +28,4 @@
* It's safe to run long running computations inside this function.
*/
@WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>)
-
- /**
- * Provides the [TileDetailsViewModel] for constructing the corresponding details view.
- *
- * This property is defined here to reuse the business logic. For example, reusing the user
- * long-click as the go-to-settings callback in the details view.
- * Subclasses can override this property to provide a specific [TileDetailsViewModel]
- * implementation.
- *
- * @return The [TileDetailsViewModel] instance, or null if not implemented.
- */
- val detailsViewModel: TileDetailsViewModel?
- get() = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
index 8c75cf0..7f475f3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
@@ -19,6 +19,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
@@ -70,9 +71,7 @@
* Creates [QSTileViewModelImpl] based on the interactors obtained from [QSTileComponent].
* Reference of that [QSTileComponent] is then stored along the view model.
*/
- fun create(
- tileSpec: TileSpec,
- ): QSTileViewModel {
+ fun create(tileSpec: TileSpec): QSTileViewModel {
val config = qsTileConfigProvider.getConfig(tileSpec.spec)
val component =
customTileComponentBuilder.qsTileConfigModule(QSTileConfigModule(config)).build()
@@ -90,6 +89,7 @@
backgroundDispatcher,
uiBackgroundDispatcher,
component.coroutineScope(),
+ /* tileDetailsViewModel= */ null,
)
}
}
@@ -127,6 +127,7 @@
userActionInteractor: QSTileUserActionInteractor<T>,
tileDataInteractor: QSTileDataInteractor<T>,
mapper: QSTileDataToStateMapper<T>,
+ tileDetailsViewModel: TileDetailsViewModel? = null,
): QSTileViewModelImpl<T> =
QSTileViewModelImpl(
qsTileConfigProvider.getConfig(tileSpec.spec),
@@ -142,6 +143,7 @@
backgroundDispatcher,
uiBackgroundDispatcher,
coroutineScopeFactory.create(),
+ tileDetailsViewModel,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 30bf5b3..9bdec43 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -83,6 +83,7 @@
private val backgroundDispatcher: CoroutineDispatcher,
uiBackgroundDispatcher: CoroutineDispatcher,
private val tileScope: CoroutineScope,
+ override val tileDetailsViewModel: TileDetailsViewModel? = null,
) : QSTileViewModel, Dumpable {
private val users: MutableStateFlow<UserHandle> =
@@ -114,9 +115,6 @@
.flowOn(backgroundDispatcher)
.stateIn(tileScope, SharingStarted.WhileSubscribed(), true)
- override val detailsViewModel: TileDetailsViewModel?
- get() = userActionInteractor().detailsViewModel
-
override fun forceUpdate() {
tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 0ed56f6..6709fd2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -16,9 +16,11 @@
package com.android.systemui.qs.tiles.dialog
+import android.content.Intent
+import android.provider.Settings
import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.statusbar.connectivity.AccessPointController
-import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -27,10 +29,13 @@
constructor(
private val accessPointController: AccessPointController,
val contentManagerFactory: InternetDetailsContentManager.Factory,
- @Assisted private val onLongClick: () -> Unit,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
) : TileDetailsViewModel() {
override fun clickOnSettingsButton() {
- onLongClick()
+ qsTileIntentUserActionHandler.handle(
+ /* expandable= */ null,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
override fun getTitle(): String {
@@ -58,7 +63,7 @@
}
@AssistedFactory
- interface Factory {
- fun create(onLongClick: () -> Unit): InternetDetailsViewModel
+ fun interface Factory {
+ fun create(): InternetDetailsViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index 0adc4131..8d4a24e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -400,6 +400,9 @@
mInternetDialogTitle.setText(internetContent.mInternetDialogTitleString);
mInternetDialogSubTitle.setText(internetContent.mInternetDialogSubTitle);
+ if (!internetContent.mIsWifiEnabled) {
+ setProgressBarVisible(false);
+ }
mAirplaneModeButton.setVisibility(
internetContent.mIsAirplaneModeEnabled ? View.VISIBLE : View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
index 0ed46e7..5f692f2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles.impl.di
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index 8e48fe4..0431e36 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -18,13 +18,10 @@
import android.content.Intent
import android.provider.Settings
-import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
@@ -41,7 +38,6 @@
private val internetDialogManager: InternetDialogManager,
private val accessPointController: AccessPointController,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
- private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory,
) : QSTileUserActionInteractor<InternetTileModel> {
override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
@@ -58,16 +54,12 @@
}
}
is QSTileUserAction.LongClick -> {
- handleLongClick(action.expandable)
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
else -> {}
}
}
-
- override val detailsViewModel: TileDetailsViewModel =
- internetDetailsViewModelFactory.create { handleLongClick(null) }
-
- private fun handleLongClick(expandable: Expandable?) {
- qsTileIntentUserActionHandler.handle(expandable, Intent(Settings.ACTION_WIFI_SETTINGS))
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index e8b9926..eeb8c85 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -39,7 +39,7 @@
val isAvailable: StateFlow<Boolean>
/** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */
- val detailsViewModel: TileDetailsViewModel?
+ val tileDetailsViewModel: TileDetailsViewModel?
get() = null
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 30d1f05..527c542 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -157,7 +157,7 @@
}
override fun getDetailsViewModel(): TileDetailsViewModel? {
- return qsTileViewModel.detailsViewModel
+ return qsTileViewModel.tileDetailsViewModel
}
@Deprecated(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
index 5042f1b..e998a73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ActionClickLogger.kt
@@ -30,26 +30,25 @@
@NotifInteractionLog private val buffer: LogBuffer
) {
fun logInitialClick(
- entry: NotificationEntry?,
+ entry: String?,
index: Integer?,
pendingIntent: PendingIntent
) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry?.key
- str2 = entry?.ranking?.channel?.id
+ str1 = entry
str3 = pendingIntent.toString()
int1 = index?.toInt() ?: Int.MIN_VALUE
}, {
- "ACTION CLICK $str1 (channel=$str2) for pending intent $str3 at index $int1"
+ "ACTION CLICK $str1 for pending intent $str3 at index $int1"
})
}
fun logRemoteInputWasHandled(
- entry: NotificationEntry?,
+ entry: String?,
index: Int?
) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry?.key
+ str1 = entry
int1 = index ?: Int.MIN_VALUE
}, {
" [Action click] Triggered remote input (for $str1) at index $int1"
@@ -57,12 +56,12 @@
}
fun logStartingIntentWithDefaultHandler(
- entry: NotificationEntry?,
+ entry: String?,
pendingIntent: PendingIntent,
index: Int?
) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry?.key
+ str1 = entry
str2 = pendingIntent.toString()
int1 = index ?: Int.MIN_VALUE
}, {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 7dc2ae7..e44701d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -580,7 +580,8 @@
/**
* @see IStatusBar#immersiveModeChanged
*/
- default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
+ default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {}
/**
* @see IStatusBar#moveFocusedTaskToDesktop(int)
@@ -876,11 +877,13 @@
}
@Override
- public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {
synchronized (mLock) {
final SomeArgs args = SomeArgs.obtain();
args.argi1 = rootDisplayAreaId;
args.argi2 = isImmersiveMode ? 1 : 0;
+ args.argi3 = windowType;
mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
}
}
@@ -2030,8 +2033,10 @@
args = (SomeArgs) msg.obj;
int rootDisplayAreaId = args.argi1;
boolean isImmersiveMode = args.argi2 != 0;
+ int windowType = args.argi3;
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode,
+ windowType);
}
break;
case MSG_ENTER_DESKTOP: {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index fed3f6e..97e62d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -23,6 +23,8 @@
import static android.app.StatusBarManager.DISABLE_RECENT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
@@ -208,7 +210,8 @@
}
@Override
- public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {
mHandler.removeMessages(H.SHOW);
if (isImmersiveMode) {
if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
@@ -221,7 +224,9 @@
&& mCanSystemBarsBeShownByUser
&& !mNavBarEmpty
&& !UserManager.isDeviceInDemoMode(mDisplayContext)
- && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+ && (mLockTaskState != LOCK_TASK_MODE_LOCKED)
+ && windowType != TYPE_PRESENTATION
+ && windowType != TYPE_PRIVATE_PRESENTATION) {
final Message msg = mHandler.obtainMessage(
H.SHOW);
msg.arg1 = rootDisplayAreaId;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index 5b5058f..13737dbff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -560,10 +560,10 @@
var entry: NotificationEntry? = null
if (expandView is ExpandableNotificationRow) {
entry = expandView.entry
- entry.setUserExpanded(/* userExpanded= */ true, /* allowChildExpansion= */ true)
+ expandView.setUserExpanded(/* userExpanded= */ true, /* allowChildExpansion= */ true)
// Indicate that the group expansion is changing at this time -- this way the group
// and children backgrounds / divider animations will look correct.
- entry.setGroupExpansionChanging(true)
+ expandView.isGroupExpansionChanging = true
userId = entry.sbn.userId
}
var fullShadeNeedsBouncer =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index c0ee56b..0d34bdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -133,11 +133,14 @@
Integer actionIndex = (Integer)
view.getTag(com.android.internal.R.id.notification_action_index_tag);
+ final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent());
final NotificationEntry entry = getNotificationForParent(view.getParent());
- mLogger.logInitialClick(entry, actionIndex, pendingIntent);
+ mLogger.logInitialClick(
+ row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent);
if (handleRemoteInput(view, pendingIntent)) {
- mLogger.logRemoteInputWasHandled(entry, actionIndex);
+ mLogger.logRemoteInputWasHandled(
+ row != null ? row.getLoggingKey() : null, actionIndex);
return true;
}
@@ -157,7 +160,8 @@
return mCallback.handleRemoteViewClick(view, pendingIntent,
action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
- mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex);
+ mLogger.logStartingIntentWithDefaultHandler(
+ row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex);
boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
return started;
@@ -224,6 +228,16 @@
return null;
}
+ private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) {
+ while (parent != null) {
+ if (parent instanceof ExpandableNotificationRow) {
+ return ((ExpandableNotificationRow) parent);
+ }
+ parent = parent.getParent();
+ }
+ return null;
+ }
+
private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
return true;
@@ -722,7 +736,7 @@
*
* @return on-click handler
*/
- public RemoteViews.InteractionHandler getRemoteViewsOnClickHandler() {
+ public InteractionHandler getRemoteViewsOnClickHandler() {
return mInteractionHandler;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 10090283..48f0245 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -34,6 +34,7 @@
import com.android.systemui.qs.tiles.NfcTile
import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
@@ -162,13 +163,15 @@
factory: QSTileViewModelFactory.Static<AirplaneModeTileModel>,
mapper: AirplaneModeMapper,
stateInteractor: AirplaneModeTileDataInteractor,
- userActionInteractor: AirplaneModeTileUserActionInteractor
+ userActionInteractor: AirplaneModeTileUserActionInteractor,
+ internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
): QSTileViewModel =
factory.create(
TileSpec.create(AIRPLANE_MODE_TILE_SPEC),
userActionInteractor,
stateInteractor,
mapper,
+ internetDetailsViewModelFactory.create(),
)
@Provides
@@ -226,13 +229,15 @@
factory: QSTileViewModelFactory.Static<InternetTileModel>,
mapper: InternetTileMapper,
stateInteractor: InternetTileDataInteractor,
- userActionInteractor: InternetTileUserActionInteractor
+ userActionInteractor: InternetTileUserActionInteractor,
+ internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
): QSTileViewModel =
factory.create(
TileSpec.create(INTERNET_TILE_SPEC),
userActionInteractor,
stateInteractor,
mapper,
+ internetDetailsViewModelFactory.create(),
)
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
index 9bc5231..f52b924 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -74,7 +74,7 @@
else
Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY
entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->
- logger.logAsyncTaskProgress(entry, "getting shortcut icon")
+ logger.logAsyncTaskProgress(entry.logKey, "getting shortcut icon")
messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index 6487d55..ccfb43e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -73,25 +73,25 @@
final ExpandableNotificationRow row = (ExpandableNotificationRow) v;
final NotificationEntry entry = row.getEntry();
- mLogger.logOnClick(entry);
+ mLogger.logOnClick(row.getLoggingKey());
// Check if the notification is displaying the menu, if so slide notification back
if (isMenuVisible(row)) {
- mLogger.logMenuVisible(entry);
+ mLogger.logMenuVisible(row.getLoggingKey());
row.animateResetTranslation();
return;
} else if (row.isChildInGroup() && isMenuVisible(row.getNotificationParent())) {
- mLogger.logParentMenuVisible(entry);
+ mLogger.logParentMenuVisible(row.getLoggingKey());
row.getNotificationParent().animateResetTranslation();
return;
} else if (row.isSummaryWithChildren() && row.areChildrenExpanded()) {
// We never want to open the app directly if the user clicks in between
// the notifications.
- mLogger.logChildrenExpanded(entry);
+ mLogger.logChildrenExpanded(row.getLoggingKey());
return;
} else if (row.areGutsExposed()) {
// ignore click if guts are exposed
- mLogger.logGutsExposed(entry);
+ mLogger.logGutsExposed(row.getLoggingKey());
return;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index cea2b59..812c609 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -25,42 +25,41 @@
class NotificationClickerLogger @Inject constructor(
@NotifInteractionLog private val buffer: LogBuffer
) {
- fun logOnClick(entry: NotificationEntry) {
+ fun logOnClick(entry: String) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry.logKey
- str2 = entry.ranking.channel.id
+ str1 = entry
}, {
- "CLICK $str1 (channel=$str2)"
+ "CLICK $str1"
})
}
- fun logMenuVisible(entry: NotificationEntry) {
+ fun logMenuVisible(entry: String) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry.logKey
+ str1 = entry
}, {
"Ignoring click on $str1; menu is visible"
})
}
- fun logParentMenuVisible(entry: NotificationEntry) {
+ fun logParentMenuVisible(entry: String) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry.logKey
+ str1 = entry
}, {
"Ignoring click on $str1; parent menu is visible"
})
}
- fun logChildrenExpanded(entry: NotificationEntry) {
+ fun logChildrenExpanded(entry: String) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry.logKey
+ str1 = entry
}, {
"Ignoring click on $str1; children are expanded"
})
}
- fun logGutsExposed(entry: NotificationEntry) {
+ fun logGutsExposed(entry: String) {
buffer.log(TAG, LogLevel.DEBUG, {
- str1 = entry.logKey
+ str1 = entry
}, {
"Ignoring click on $str1; guts are exposed"
})
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
index ab40582..243a868 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorController.kt
@@ -74,7 +74,7 @@
}
private val notificationEntry = notification.entry
- private val notificationKey = notificationEntry.sbn.key
+ private val notificationKey = notification.key
override val isLaunching: Boolean = true
@@ -175,7 +175,7 @@
HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(notification, animate)
headsUpManager.removeNotification(
- row.entry.key,
+ row.key,
true /* releaseImmediately */,
animate,
reason,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
index 31bcf2b..acbcab0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -94,7 +94,7 @@
/** Get the notification key, reformatted for logging, for the (optional) row */
public static String logKey(ExpandableNotificationRow row) {
- return row == null ? "null" : logKey(row.getEntry());
+ return row == null ? "null" : row.getLoggingKey();
}
/** Removes newlines from the notification key to prettify apps that have these in the tag */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 35a2828..c79cae7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.os.Build;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -34,6 +35,10 @@
import java.util.List;
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
/**
* Class to represent notifications bundled by classification.
*/
@@ -41,6 +46,9 @@
private final BundleEntryAdapter mEntryAdapter;
+ // TODO(b/394483200): move NotificationEntry's implementation to PipelineEntry?
+ private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(false);
+
// TODO (b/389839319): implement the row
private ExpandableNotificationRow mRow;
@@ -97,20 +105,26 @@
return true;
}
+ @NonNull
@Override
public String getKey() {
return mKey;
}
@Override
+ @Nullable
public ExpandableNotificationRow getRow() {
return mRow;
}
- @Nullable
@Override
- public EntryAdapter getGroupRoot() {
- return this;
+ public boolean isGroupRoot() {
+ return true;
+ }
+
+ @Override
+ public StateFlow<Boolean> isSensitive() {
+ return BundleEntry.this.mSensitive;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index 6431cac..109ebe6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -24,6 +24,8 @@
import com.android.systemui.statusbar.notification.icon.IconPack;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import kotlinx.coroutines.flow.StateFlow;
+
/**
* Adapter interface for UI to get relevant info.
*/
@@ -51,15 +53,10 @@
ExpandableNotificationRow getRow();
/**
- * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
- * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
- * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
- * or a child of a group summary, the summary NotificationEntry will be returned, even if that
- * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
- * group or bundle grouping, null will be returned.
+ * Whether this entry is the root of its collapsable 'group' - either a BundleEntry or a
+ * notification group summary
*/
- @Nullable
- EntryAdapter getGroupRoot();
+ boolean isGroupRoot();
/**
* @return whether the row can be removed with the 'Clear All' action
@@ -107,4 +104,9 @@
* Retrieves the pack of icons associated with this entry
*/
IconPack getIcons();
+
+ /**
+ * Returns whether the content of this entry is sensitive
+ */
+ StateFlow<Boolean> isSensitive();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 826329d..22de83e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -620,7 +620,7 @@
}
private boolean isDismissable(NotificationEntry entry) {
- return mDismissibilityProvider.isDismissable(entry);
+ return mDismissibilityProvider.isDismissable(entry.getKey());
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index e5b72d4..fb2a66c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -297,20 +297,20 @@
return NotificationEntry.this.getRow();
}
- @Nullable
@Override
- public EntryAdapter getGroupRoot() {
- // TODO (b/395857098): for backwards compatibility this will return null if called
- // on a group summary that's not in a bundles, but it should return itself.
+ public boolean isGroupRoot() {
if (isTopLevelEntry() || getParent() == null) {
- return null;
+ return false;
}
if (NotificationEntry.this.getParent() instanceof GroupEntry parentGroupEntry) {
- if (parentGroupEntry.getSummary() != null) {
- return parentGroupEntry.getSummary().mEntryAdapter;
- }
+ return parentGroupEntry.getSummary() == NotificationEntry.this;
}
- return null;
+ return false;
+ }
+
+ @Override
+ public StateFlow<Boolean> isSensitive() {
+ return NotificationEntry.this.isSensitive();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt
index 53f2366..75bf38f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProvider.kt
@@ -20,6 +20,6 @@
/** Keeps track of the dismissibility of Notifications currently handed over to the view layer. */
interface NotificationDismissibilityProvider {
- /** @return true if the given {NotificationEntry} can currently be dismissed by the user */
- fun isDismissable(entry: NotificationEntry): Boolean
+ /** @return true if the given entry's key can currently be dismissed by the user */
+ fun isDismissable(entryKey: String): Boolean
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
index 9326d33..740442a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationDismissibilityProviderImpl.kt
@@ -39,8 +39,8 @@
var nonDismissableEntryKeys = setOf<String>()
private set
- override fun isDismissable(entry: NotificationEntry): Boolean {
- return entry.key !in nonDismissableEntryKeys
+ override fun isDismissable(entryKey: String): Boolean {
+ return entryKey !in nonDismissableEntryKeys
}
@Synchronized
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index b179a69..c5ae875 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -29,6 +29,7 @@
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.io.PrintWriter;
@@ -162,41 +163,38 @@
@Override
public boolean isGroupExpanded(EntryAdapter entry) {
NotificationBundleUi.assertInNewMode();
- return mExpandedCollections.contains(mGroupMembershipManager.getGroupRoot(entry));
+ ExpandableNotificationRow parent = entry.getRow().getNotificationParent();
+ return mExpandedCollections.contains(entry)
+ || (parent != null && mExpandedCollections.contains(parent.getEntryAdapter()));
}
@Override
- public void setGroupExpanded(EntryAdapter entry, boolean expanded) {
+ public void setGroupExpanded(EntryAdapter groupRoot, boolean expanded) {
NotificationBundleUi.assertInNewMode();
- EntryAdapter groupParent = mGroupMembershipManager.getGroupRoot(entry);
- if (!entry.isAttached()) {
+ if (!groupRoot.isAttached()) {
if (expanded) {
Log.wtf(TAG, "Cannot expand group that is not attached");
- } else {
- // The entry is no longer attached, but we still want to make sure we don't have
- // a stale expansion state.
- groupParent = entry;
}
}
boolean changed;
if (expanded) {
- changed = mExpandedCollections.add(groupParent);
+ changed = mExpandedCollections.add(groupRoot);
} else {
- changed = mExpandedCollections.remove(groupParent);
+ changed = mExpandedCollections.remove(groupRoot);
}
// Only notify listeners if something changed.
if (changed) {
- sendOnGroupExpandedChange(entry, expanded);
+ sendOnGroupExpandedChange(groupRoot, expanded);
}
}
@Override
- public boolean toggleGroupExpansion(EntryAdapter entry) {
+ public boolean toggleGroupExpansion(EntryAdapter groupRoot) {
NotificationBundleUi.assertInNewMode();
- setGroupExpanded(entry, !isGroupExpanded(entry));
- return isGroupExpanded(entry);
+ setGroupExpanded(groupRoot, !isGroupExpanded(groupRoot));
+ return isGroupExpanded(groupRoot);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
index 3edbfaf..86aa4a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManager.java
@@ -51,17 +51,6 @@
NotificationEntry getGroupSummary(@NonNull NotificationEntry entry);
/**
- * Gets the EntryAdapter that is the nearest root of the collection of rows the given entry
- * belongs to. If the given entry is a BundleEntry or an isolated child of a BundleEntry, the
- * BundleEntry will be returned. If the given notification is a group summary NotificationEntry,
- * or a child of a group summary, the summary NotificationEntry will be returned, even if that
- * summary belongs to a BundleEntry. If the entry is a notification that does not belong to any
- * group or bundle grouping, null will be returned.
- */
- @Nullable
- EntryAdapter getGroupRoot(@NonNull EntryAdapter entry);
-
- /**
* @return whether a given notification is a child in a group
*/
boolean isChildInGroup(@NonNull NotificationEntry entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
index a1a23e3..aec0d70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
@@ -60,7 +60,7 @@
@Override
public boolean isGroupRoot(@NonNull EntryAdapter entry) {
NotificationBundleUi.assertInNewMode();
- return entry == entry.getGroupRoot();
+ return entry.isGroupRoot();
}
@Nullable
@@ -76,13 +76,6 @@
return null;
}
- @Nullable
- @Override
- public EntryAdapter getGroupRoot(@NonNull EntryAdapter entry) {
- NotificationBundleUi.assertInNewMode();
- return entry.getGroupRoot();
- }
-
@Override
public boolean isChildInGroup(@NonNull NotificationEntry entry) {
NotificationBundleUi.assertInLegacyMode();
@@ -94,7 +87,7 @@
public boolean isChildInGroup(@NonNull EntryAdapter entry) {
NotificationBundleUi.assertInNewMode();
// An entry is a child if it's not a group root or top level entry, but it is attached.
- return entry.isAttached() && entry != getGroupRoot(entry) && !entry.isTopLevelEntry();
+ return entry.isAttached() && !entry.isGroupRoot() && !entry.isTopLevelEntry();
}
@Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
index 5f7acea..9728fcf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
@@ -19,7 +19,9 @@
import android.graphics.Region
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import dagger.Binds
import dagger.Module
import java.io.PrintWriter
@@ -153,6 +155,12 @@
fun setAnimationStateHandler(handler: AnimationStateHandler)
/**
+ * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
+ * it's collapsed again.
+ */
+ fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean)
+
+ /**
* Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
* it's collapsed again.
*/
@@ -310,6 +318,8 @@
override fun setAnimationStateHandler(handler: AnimationStateHandler) {}
+ override fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean) {}
+
override fun setExpanded(entry: NotificationEntry, expanded: Boolean) {}
override fun setGutsShown(entry: NotificationEntry, gutsShown: Boolean) {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index acb27a4..7c5f3b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -878,10 +878,8 @@
ExpandableNotificationRow topRow = topEntry.getRow();
if (topEntry.rowIsChildInGroup()) {
if (NotificationBundleUi.isEnabled()) {
- final EntryAdapter adapter = mGroupMembershipManager.getGroupRoot(
- topRow.getEntryAdapter());
- if (adapter != null) {
- topRow = adapter.getRow();
+ if (topRow.getNotificationParent() != null) {
+ topRow = topRow.getNotificationParent();
}
} else {
final NotificationEntry groupSummary =
@@ -1093,7 +1091,23 @@
* Set an entry to be expanded and therefore stick in the heads up area if it's pinned
* until it's collapsed again.
*/
+ @Override
+ public void setExpanded(@NonNull String entryKey, @NonNull ExpandableNotificationRow row,
+ boolean expanded) {
+ NotificationBundleUi.assertInNewMode();
+ HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey);
+ if (headsUpEntry != null && row.getPinnedStatus().isPinned()) {
+ headsUpEntry.setExpanded(expanded);
+ }
+ }
+
+ /**
+ * Set an entry to be expanded and therefore stick in the heads up area if it's pinned
+ * until it's collapsed again.
+ */
+ @Override
public void setExpanded(@NonNull NotificationEntry entry, boolean expanded) {
+ NotificationBundleUi.assertInLegacyMode();
HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
if (headsUpEntry != null && entry.isRowPinned()) {
headsUpEntry.setExpanded(expanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
index 47e725c..95f07c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpTouchHelper.java
@@ -141,7 +141,7 @@
if (mPickedChild != null && mTouchingHeadsUpView) {
// We may swallow this click if the heads up just came in.
if (mHeadsUpManager.shouldSwallowClick(
- mPickedChild.getEntry().getSbn().getKey())) {
+ mPickedChild.getKey())) {
endMotion();
return true;
}
@@ -209,7 +209,7 @@
if (mPickedChild != null && mTouchingHeadsUpView) {
// We may swallow this click if the heads up just came in.
if (mHeadsUpManager.shouldSwallowClick(
- mPickedChild.getEntry().getSbn().getKey())) {
+ mPickedChild.getKey())) {
endMotion();
setTrackingHeadsUp(false);
return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java
index 40da232..e1cbdac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpUtil.java
@@ -76,12 +76,7 @@
}
final ExpandableNotificationRow row = (ExpandableNotificationRow) view;
- final NotificationEntry entry = row.getEntry();
- if (entry == null) {
- return "(null entry)";
- }
-
- final String key = entry.getKey();
+ final String key = row.getKey();
if (key == null) {
return "(null key)";
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
index 0e1f66f..48095cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationRoundnessLogger.kt
@@ -41,7 +41,7 @@
TAG_ROUNDNESS,
INFO,
{
- str1 = (view as? ExpandableNotificationRow)?.entry?.key
+ str1 = (view as? ExpandableNotificationRow)?.key
bool1 = isFirstInSection
bool2 = isLastInSection
bool3 = topChanged
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index 6bfc9f0..4bd6053 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -21,7 +21,6 @@
import android.app.NotificationChannel
import android.app.NotificationChannel.DEFAULT_CHANNEL_ID
import android.app.NotificationChannelGroup
-import android.app.NotificationManager.IMPORTANCE_NONE
import android.app.NotificationManager.Importance
import android.content.Context
import android.graphics.Color
@@ -40,7 +39,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.res.R
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
import javax.inject.Inject
private const val TAG = "ChannelDialogController"
@@ -59,9 +58,9 @@
*/
@SysUISingleton
class ChannelEditorDialogController @Inject constructor(
- @ShadeDisplayAware private val context: Context,
+ private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
private val noMan: INotificationManager,
- private val dialogBuilder: ChannelEditorDialog.Builder
+ private val dialogBuilder: ChannelEditorDialog.Builder,
) {
private var prepared = false
@@ -272,7 +271,7 @@
}
private fun initDialog() {
- dialogBuilder.setContext(context)
+ dialogBuilder.setContext(shadeDialogContextInteractor.context)
dialog = dialogBuilder.build()
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 108c060..6134d1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -270,6 +270,7 @@
private NotificationContentView[] mLayouts;
private ExpandableNotificationRowLogger mLogger;
private String mLoggingKey;
+ private String mKey;
private NotificationGuts mGuts;
private NotificationEntry mEntry;
private EntryAdapter mEntryAdapter;
@@ -404,17 +405,25 @@
: mGroupMembershipManager.isGroupSummary(mEntry);
if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && isGroupRoot) {
mGroupExpansionChanging = true;
- final boolean wasExpanded = NotificationBundleUi.isEnabled()
- ? mGroupExpansionManager.isGroupExpanded(mEntryAdapter)
- : mGroupExpansionManager.isGroupExpanded(mEntry);
- boolean nowExpanded = NotificationBundleUi.isEnabled()
- ? mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter)
- : mGroupExpansionManager.toggleGroupExpansion(mEntry);
- mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
- if (shouldLogExpandClickMetric) {
- mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
+ if (NotificationBundleUi.isEnabled()) {
+ final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntryAdapter);
+ boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntryAdapter);
+ mOnExpandClickListener.onExpandClicked(this, mEntryAdapter, nowExpanded);
+ if (shouldLogExpandClickMetric) {
+ mMetricsLogger.action(
+ MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
+ }
+ onExpansionChanged(true /* userAction */, wasExpanded);
+ } else {
+ final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry);
+ boolean nowExpanded = mGroupExpansionManager.toggleGroupExpansion(mEntry);
+ mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
+ if (shouldLogExpandClickMetric) {
+ mMetricsLogger.action(
+ MetricsEvent.ACTION_NOTIFICATION_GROUP_EXPANDER, nowExpanded);
+ }
+ onExpansionChanged(true /* userAction */, wasExpanded);
}
- onExpansionChanged(true /* userAction */, wasExpanded);
} else if (mEnableNonGroupedNotificationExpand) {
if (v.isAccessibilityFocused()) {
mPrivateLayout.setFocusOnVisibilityChange();
@@ -435,7 +444,11 @@
}
notifyHeightChanged(/* needsAnimation= */ true);
- mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
+ if (NotificationBundleUi.isEnabled()) {
+ mOnExpandClickListener.onExpandClicked(this, mEntryAdapter, nowExpanded);
+ } else {
+ mOnExpandClickListener.onExpandClicked(mEntry, v, nowExpanded);
+ }
if (shouldLogExpandClickMetric) {
mMetricsLogger.action(MetricsEvent.ACTION_NOTIFICATION_EXPANDER, nowExpanded);
}
@@ -535,6 +548,14 @@
return mLoggingKey;
}
+ public String getKey() {
+ if (NotificationBundleUi.isEnabled()) {
+ return mKey;
+ } else {
+ return mEntry.getKey();
+ }
+ }
+
/**
* Sets animations running in the layouts of this row, including public, private, and children.
*
@@ -1224,7 +1245,7 @@
*/
public void collectVisibleLocations(Map<String, Integer> locationsMap) {
if (getVisibility() == View.VISIBLE) {
- locationsMap.put(getEntry().getKey(), getViewState().location);
+ locationsMap.put(getKey(), getViewState().location);
if (mChildrenContainer != null) {
List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren();
for (int i = 0; i < children.size(); i++) {
@@ -2136,6 +2157,7 @@
mMenuRow.setAppName(mAppName);
}
mLogger = logger;
+ mKey = notificationKey;
mLoggingKey = logKey(notificationKey);
mBypassController = bypassController;
mGroupMembershipManager = groupMembershipManager;
@@ -2946,7 +2968,9 @@
&& !mChildrenContainer.showingAsLowPriority()) {
final boolean wasExpanded = isGroupExpanded();
if (NotificationBundleUi.isEnabled()) {
- mGroupExpansionManager.setGroupExpanded(mEntryAdapter, userExpanded);
+ if (mEntryAdapter.isGroupRoot()) {
+ mGroupExpansionManager.setGroupExpanded(mEntryAdapter, userExpanded);
+ }
} else {
mGroupExpansionManager.setGroupExpanded(mEntry, userExpanded);
}
@@ -3416,7 +3440,7 @@
}
private boolean canEntryBeDismissed() {
- return mDismissibilityProvider.isDismissable(mEntry);
+ return mDismissibilityProvider.isDismissable(getKey());
}
/**
@@ -3440,9 +3464,9 @@
public void makeActionsVisibile() {
setUserExpanded(true, true);
if (isChildInGroup()) {
- if (NotificationBundleUi.isEnabled()) {
- mGroupExpansionManager.setGroupExpanded(mEntryAdapter, true);
- } else {
+ if (!NotificationBundleUi.isEnabled()) {
+ // this is only called if row.getParent() instanceof NotificationStackScrollLayout,
+ // so there is never a group to expand
mGroupExpansionManager.setGroupExpanded(mEntry, true);
}
}
@@ -4023,6 +4047,9 @@
public interface OnExpandClickListener {
void onExpandClicked(NotificationEntry clickedEntry, View clickedView, boolean nowExpanded);
+
+ void onExpandClicked(ExpandableNotificationRow row, EntryAdapter clickedEntry,
+ boolean nowExpanded);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 4b2b168..7444679 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -56,6 +56,7 @@
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
+import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
@@ -141,11 +142,11 @@
// We don't want to reinflate anything for removed notifications. Otherwise views might
// be readded to the stack, leading to leaks. This may happen with low-priority groups
// where the removal of already removed children can lead to a reinflation.
- mLogger.logNotBindingRowWasRemoved(entry);
+ mLogger.logNotBindingRowWasRemoved(row.getLoggingKey());
return;
}
- mLogger.logBinding(entry, contentToBind);
+ mLogger.logBinding(row.getLoggingKey(), contentToBind);
StatusBarNotification sbn = entry.getSbn();
@@ -283,7 +284,7 @@
@NonNull ExpandableNotificationRow row) {
final boolean abortedTask = entry.abortTask();
if (abortedTask) {
- mLogger.logCancelBindAbortedTask(entry);
+ mLogger.logCancelBindAbortedTask(row.getLoggingKey());
}
return abortedTask;
}
@@ -293,7 +294,7 @@
@NonNull NotificationEntry entry,
@NonNull ExpandableNotificationRow row,
@InflationFlag int contentToUnbind) {
- mLogger.logUnbinding(entry, contentToUnbind);
+ mLogger.logUnbinding(row.getLoggingKey(), contentToUnbind);
int curFlag = 1;
while (contentToUnbind != 0) {
if ((contentToUnbind & curFlag) != 0) {
@@ -410,18 +411,19 @@
&& result.newExpandedView != null;
boolean inflateHeadsUp = (reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0
&& result.newHeadsUpView != null;
+ String logKey = NotificationUtils.logKey(entry);
if (inflateContracted || inflateExpanded || inflateHeadsUp) {
- logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state");
+ logger.logAsyncTaskProgress(logKey, "inflating contracted smart reply state");
result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry);
}
if (inflateExpanded) {
- logger.logAsyncTaskProgress(entry, "inflating expanded smart reply state");
+ logger.logAsyncTaskProgress(logKey, "inflating expanded smart reply state");
result.expandedInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
context, packageContext, entry, previousSmartReplyState,
result.inflatedSmartReplyState);
}
if (inflateHeadsUp) {
- logger.logAsyncTaskProgress(entry, "inflating heads up smart reply state");
+ logger.logAsyncTaskProgress(logKey, "inflating heads up smart reply state");
result.headsUpInflatedSmartReplies = inflater.inflateSmartReplyViewHolder(
context, packageContext, entry, previousSmartReplyState,
result.inflatedSmartReplyState);
@@ -438,23 +440,21 @@
NotificationRowContentBinderLogger logger) {
return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> {
InflationProgress result = new InflationProgress();
- final NotificationEntry entryForLogging = row.getEntry();
-
// create an image inflater
result.mRowImageInflater = RowImageInflater.newInstance(row.mImageModelIndex);
if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
- logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view");
+ logger.logAsyncTaskProgress(row.getLoggingKey(), "creating contracted remote view");
result.newContentView = createContentView(builder, bindParams.isMinimized);
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) {
- logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view");
+ logger.logAsyncTaskProgress(row.getLoggingKey(), "creating expanded remote view");
result.newExpandedView = createExpandedView(builder, bindParams.isMinimized);
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) {
- logger.logAsyncTaskProgress(entryForLogging, "creating heads up remote view");
+ logger.logAsyncTaskProgress(row.getLoggingKey(), "creating heads up remote view");
final boolean isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle();
if (isHeadsUpCompact) {
result.newHeadsUpView = builder.createCompactHeadsUpContentView();
@@ -464,7 +464,7 @@
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
- logger.logAsyncTaskProgress(entryForLogging, "creating public remote view");
+ logger.logAsyncTaskProgress(row.getLoggingKey(), "creating public remote view");
if (LockscreenOtpRedaction.isEnabled()
&& bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
result.newPublicView = createSensitiveContentMessageNotification(
@@ -477,13 +477,13 @@
if (AsyncGroupHeaderViewInflation.isEnabled()) {
if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) {
- logger.logAsyncTaskProgress(entryForLogging,
+ logger.logAsyncTaskProgress(row.getLoggingKey(),
"creating group summary remote view");
result.mNewGroupHeaderView = builder.makeNotificationGroupHeader();
}
if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) {
- logger.logAsyncTaskProgress(entryForLogging,
+ logger.logAsyncTaskProgress(row.getLoggingKey(),
"creating low-priority group summary remote view");
result.mNewMinimizedGroupHeaderView =
builder.makeLowPriorityContentView(true /* useRegularSubtext */);
@@ -577,6 +577,7 @@
@Nullable InflationCallback callback,
NotificationRowContentBinderLogger logger) {
Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
+ String logKey = NotificationUtils.logKey(entry);
NotificationContentView privateLayout = row.getPrivateLayout();
NotificationContentView publicLayout = row.getPublicLayout();
@@ -590,7 +591,7 @@
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
- logger.logAsyncTaskProgress(entry, "contracted view applied");
+ logger.logAsyncTaskProgress(logKey, "contracted view applied");
result.inflatedContentView = v;
}
@@ -599,7 +600,7 @@
return result.newContentView;
}
};
- logger.logAsyncTaskProgress(entry, "applying contracted view");
+ logger.logAsyncTaskProgress(logKey, "applying contracted view");
applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
reInflateFlags, flag,
remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
@@ -618,7 +619,7 @@
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
- logger.logAsyncTaskProgress(entry, "expanded view applied");
+ logger.logAsyncTaskProgress(logKey, "expanded view applied");
result.inflatedExpandedView = v;
}
@@ -627,7 +628,7 @@
return result.newExpandedView;
}
};
- logger.logAsyncTaskProgress(entry, "applying expanded view");
+ logger.logAsyncTaskProgress(logKey, "applying expanded view");
applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result,
reInflateFlags,
flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
@@ -646,7 +647,7 @@
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
- logger.logAsyncTaskProgress(entry, "heads up view applied");
+ logger.logAsyncTaskProgress(logKey, "heads up view applied");
result.inflatedHeadsUpView = v;
}
@@ -655,7 +656,7 @@
return result.newHeadsUpView;
}
};
- logger.logAsyncTaskProgress(entry, "applying heads up view");
+ logger.logAsyncTaskProgress(logKey, "applying heads up view");
applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
result, reInflateFlags,
flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler,
@@ -673,7 +674,7 @@
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
- logger.logAsyncTaskProgress(entry, "public view applied");
+ logger.logAsyncTaskProgress(logKey, "public view applied");
result.inflatedPublicView = v;
}
@@ -682,7 +683,7 @@
return result.newPublicView;
}
};
- logger.logAsyncTaskProgress(entry, "applying public view");
+ logger.logAsyncTaskProgress(logKey, "applying public view");
applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
result, reInflateFlags, flag,
remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback,
@@ -702,7 +703,7 @@
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
- logger.logAsyncTaskProgress(entry, "group header view applied");
+ logger.logAsyncTaskProgress(logKey, "group header view applied");
result.mInflatedGroupHeaderView = (NotificationHeaderView) v;
}
@@ -711,7 +712,7 @@
return result.mNewGroupHeaderView;
}
};
- logger.logAsyncTaskProgress(entry, "applying group header view");
+ logger.logAsyncTaskProgress(logKey, "applying group header view");
applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
result, reInflateFlags,
/* inflationId = */ FLAG_GROUP_SUMMARY_HEADER,
@@ -731,7 +732,7 @@
ApplyCallback applyCallback = new ApplyCallback() {
@Override
public void setResultView(View v) {
- logger.logAsyncTaskProgress(entry,
+ logger.logAsyncTaskProgress(logKey,
"low-priority group header view applied");
result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v;
}
@@ -741,7 +742,7 @@
return result.mNewMinimizedGroupHeaderView;
}
};
- logger.logAsyncTaskProgress(entry, "applying low priority group header view");
+ logger.logAsyncTaskProgress(logKey, "applying low priority group header view");
applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized,
result, reInflateFlags,
/* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
@@ -760,7 +761,7 @@
CancellationSignal cancellationSignal = new CancellationSignal();
cancellationSignal.setOnCancelListener(
() -> {
- logger.logAsyncTaskProgress(entry, "apply cancelled");
+ logger.logAsyncTaskProgress(logKey, "apply cancelled");
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
runningInflations.values().forEach(CancellationSignal::cancel);
});
@@ -807,7 +808,7 @@
existingWrapper.onReinflated();
}
} catch (Exception e) {
- handleInflationError(runningInflations, e, row.getEntry(), callback, logger,
+ handleInflationError(runningInflations, e, row, callback, logger,
"applying view synchronously");
// Add a running inflation to make sure we don't trigger callbacks.
// Safe to do because only happens in tests.
@@ -829,7 +830,7 @@
String invalidReason = isValidView(v, entry, row.getResources());
if (invalidReason != null) {
handleInflationError(runningInflations, new InflationException(invalidReason),
- row.getEntry(), callback, logger, "applied invalid view");
+ row, callback, logger, "applied invalid view");
runningInflations.remove(inflationId);
return;
}
@@ -866,7 +867,7 @@
onViewApplied(newView);
} catch (Exception anotherException) {
runningInflations.remove(inflationId);
- handleInflationError(runningInflations, e, row.getEntry(),
+ handleInflationError(runningInflations, e, row,
callback, logger, "applying view");
}
}
@@ -962,13 +963,13 @@
private static void handleInflationError(
HashMap<Integer, CancellationSignal> runningInflations, Exception e,
- NotificationEntry notification, @Nullable InflationCallback callback,
+ ExpandableNotificationRow row, @Nullable InflationCallback callback,
NotificationRowContentBinderLogger logger, String logContext) {
Assert.isMainThread();
- logger.logAsyncTaskException(notification, logContext, e);
+ logger.logAsyncTaskException(row.getLoggingKey(), logContext, e);
runningInflations.values().forEach(CancellationSignal::cancel);
if (callback != null) {
- callback.handleInflationException(notification, e);
+ callback.handleInflationException(row.getEntry(), e);
}
}
@@ -989,7 +990,7 @@
}
NotificationContentView privateLayout = row.getPrivateLayout();
NotificationContentView publicLayout = row.getPublicLayout();
- logger.logAsyncTaskProgress(entry, "finishing");
+ logger.logAsyncTaskProgress(NotificationUtils.logKey(entry), "finishing");
// Put the new image index on the row
row.mImageModelIndex = result.mRowImageInflater.getNewImageIndex();
@@ -1284,7 +1285,8 @@
return doInBackgroundInternal();
} catch (Exception e) {
mError = e;
- mLogger.logAsyncTaskException(mEntry, "inflating", e);
+ mLogger.logAsyncTaskException(
+ NotificationUtils.logKey(mEntry), "inflating", e);
return null;
}
});
@@ -1311,12 +1313,12 @@
InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
recoveredBuilder, mBindParams, mContext, packageContext, mRow,
mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger);
-
- mLogger.logAsyncTaskProgress(mEntry,
+ String logKey = NotificationUtils.logKey(mEntry);
+ mLogger.logAsyncTaskProgress(logKey,
"getting existing smart reply state (on wrong thread!)");
InflatedSmartReplyState previousSmartReplyState =
mRow.getExistingSmartReplyState();
- mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
+ mLogger.logAsyncTaskProgress(logKey, "inflating smart reply views");
InflationProgress result = inflateSmartReplyViews(
/* result = */ inflationProgress,
mReInflateFlags,
@@ -1379,26 +1381,26 @@
}
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- mLogger.logAsyncTaskProgress(mEntry, "extracting promoted notification content");
+ mLogger.logAsyncTaskProgress(logKey, "extracting promoted notification content");
final ImageModelProvider imageModelProvider =
result.mRowImageInflater.useForContentModel();
final PromotedNotificationContentModel promotedContent =
mPromotedNotificationContentExtractor.extractContent(mEntry,
recoveredBuilder, imageModelProvider);
- mLogger.logAsyncTaskProgress(mEntry, "extracted promoted notification content: "
+ mLogger.logAsyncTaskProgress(logKey, "extracted promoted notification content: "
+ promotedContent);
result.mPromotedContent = promotedContent;
}
- mLogger.logAsyncTaskProgress(mEntry, "loading RON images");
+ mLogger.logAsyncTaskProgress(logKey, "loading RON images");
inflationProgress.mRowImageInflater.loadImagesSynchronously(packageContext);
- mLogger.logAsyncTaskProgress(mEntry,
+ mLogger.logAsyncTaskProgress(logKey,
"getting row image resolver (on wrong thread!)");
final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
// wait for image resolver to finish preloading
- mLogger.logAsyncTaskProgress(mEntry, "waiting for preloaded images");
+ mLogger.logAsyncTaskProgress(logKey, "waiting for preloaded images");
imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS);
return result;
@@ -1449,13 +1451,14 @@
@Override
public void abort() {
- mLogger.logAsyncTaskProgress(mEntry, "cancelling inflate");
+ String logKey = NotificationUtils.logKey(mEntry);
+ mLogger.logAsyncTaskProgress(logKey, "cancelling inflate");
cancel(true /* mayInterruptIfRunning */);
if (mCancellationSignal != null) {
- mLogger.logAsyncTaskProgress(mEntry, "cancelling apply");
+ mLogger.logAsyncTaskProgress(logKey, "cancelling apply");
mCancellationSignal.cancel();
}
- mLogger.logAsyncTaskProgress(mEntry, "aborted");
+ mLogger.logAsyncTaskProgress(logKey, "aborted");
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index ab382df..e89a76f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.row;
-import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
+import static android.app.Flags.notificationsRedesignTemplates;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
@@ -706,8 +706,11 @@
static NotificationMenuItem createInfoItem(Context context) {
Resources res = context.getResources();
String infoDescription = res.getString(R.string.notification_menu_gear_description);
+ int layoutId = notificationsRedesignTemplates()
+ ? R.layout.notification_2025_info
+ : R.layout.notification_info;
NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
- R.layout.notification_info, null, false);
+ layoutId, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
R.drawable.ic_settings);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index b9a3594..c930dd8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -51,6 +51,7 @@
import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
@@ -125,10 +126,10 @@
// We don't want to reinflate anything for removed notifications. Otherwise views might
// be readded to the stack, leading to leaks. This may happen with low-priority groups
// where the removal of already removed children can lead to a reinflation.
- logger.logNotBindingRowWasRemoved(entry)
+ logger.logNotBindingRowWasRemoved(row.loggingKey)
return
}
- logger.logBinding(entry, contentToBind)
+ logger.logBinding(row.loggingKey, contentToBind)
val sbn: StatusBarNotification = entry.sbn
// To check if the notification has inline image and preload inline image if necessary.
@@ -245,7 +246,7 @@
override fun cancelBind(entry: NotificationEntry, row: ExpandableNotificationRow): Boolean {
val abortedTask: Boolean = entry.abortTask()
if (abortedTask) {
- logger.logCancelBindAbortedTask(entry)
+ logger.logCancelBindAbortedTask(row.loggingKey)
}
return abortedTask
}
@@ -256,7 +257,7 @@
row: ExpandableNotificationRow,
@InflationFlag contentToUnbind: Int,
) {
- logger.logUnbinding(entry, contentToUnbind)
+ logger.logUnbinding(row.loggingKey, contentToUnbind)
var curFlag = 1
var contentLeftToUnbind = contentToUnbind
while (contentLeftToUnbind != 0) {
@@ -419,7 +420,7 @@
try {
return@trace Result.success(doInBackgroundInternal())
} catch (e: Exception) {
- logger.logAsyncTaskException(entry, "inflating", e)
+ logger.logAsyncTaskException(entry.logKey, "inflating", e)
return@trace Result.failure(e)
}
}
@@ -451,11 +452,11 @@
logger = logger,
)
logger.logAsyncTaskProgress(
- entry,
+ row.loggingKey,
"getting existing smart reply state (on wrong thread!)",
)
val previousSmartReplyState: InflatedSmartReplyState? = row.existingSmartReplyState
- logger.logAsyncTaskProgress(entry, "inflating smart reply views")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating smart reply views")
inflateSmartReplyViews(
/* result = */ inflationProgress,
reInflateFlags,
@@ -467,7 +468,7 @@
logger,
)
if (AsyncHybridViewInflation.isEnabled) {
- logger.logAsyncTaskProgress(entry, "inflating single line view")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating single line view")
inflationProgress.inflatedSingleLineView =
inflationProgress.contentModel.singleLineViewModel?.let {
SingleLineViewInflater.inflatePrivateSingleLineView(
@@ -481,7 +482,7 @@
}
if (LockscreenOtpRedaction.isEnabled) {
- logger.logAsyncTaskProgress(entry, "inflating public single line view")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating public single line view")
inflationProgress.inflatedPublicSingleLineView =
inflationProgress.contentModel.publicSingleLineViewModel?.let { viewModel ->
SingleLineViewInflater.inflatePublicSingleLineView(
@@ -494,13 +495,13 @@
}
}
- logger.logAsyncTaskProgress(entry, "loading RON images")
+ logger.logAsyncTaskProgress(entry.logKey, "loading RON images")
inflationProgress.rowImageInflater.loadImagesSynchronously(packageContext)
- logger.logAsyncTaskProgress(entry, "getting row image resolver (on wrong thread!)")
+ logger.logAsyncTaskProgress(entry.logKey, "getting row image resolver (on wrong thread!)")
val imageResolver = row.imageResolver
// wait for image resolver to finish preloading
- logger.logAsyncTaskProgress(entry, "waiting for preloaded images")
+ logger.logAsyncTaskProgress(entry.logKey, "waiting for preloaded images")
imageResolver.waitForPreloadedImages(IMG_PRELOAD_TIMEOUT_MS)
return inflationProgress
}
@@ -547,13 +548,13 @@
}
override fun abort() {
- logger.logAsyncTaskProgress(entry, "cancelling inflate")
+ logger.logAsyncTaskProgress(entry.logKey, "cancelling inflate")
cancel(/* mayInterruptIfRunning= */ true)
if (cancellationSignal != null) {
- logger.logAsyncTaskProgress(entry, "cancelling apply")
+ logger.logAsyncTaskProgress(entry.logKey, "cancelling apply")
cancellationSignal!!.cancel()
}
- logger.logAsyncTaskProgress(entry, "aborted")
+ logger.logAsyncTaskProgress(entry.logKey, "aborted")
}
override fun handleInflationException(e: Exception) {
@@ -641,11 +642,11 @@
(reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 &&
result.remoteViews.headsUp != null)
if (inflateContracted || inflateExpanded || inflateHeadsUp) {
- logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating contracted smart reply state")
result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry)
}
if (inflateExpanded) {
- logger.logAsyncTaskProgress(entry, "inflating expanded smart reply state")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating expanded smart reply state")
result.expandedInflatedSmartReplies =
inflater.inflateSmartReplyViewHolder(
context,
@@ -656,7 +657,7 @@
)
}
if (inflateHeadsUp) {
- logger.logAsyncTaskProgress(entry, "inflating heads up smart reply state")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating heads up smart reply state")
result.headsUpInflatedSmartReplies =
inflater.inflateSmartReplyViewHolder(
context,
@@ -687,13 +688,16 @@
val promotedContent =
if (PromotedNotificationContentModel.featureFlagEnabled()) {
- logger.logAsyncTaskProgress(entry, "extracting promoted notification content")
+ logger.logAsyncTaskProgress(
+ entry.logKey,
+ "extracting promoted notification content"
+ )
val imageModelProvider = rowImageInflater.useForContentModel()
promotedNotificationContentExtractor
.extractContent(entry, builder, imageModelProvider)
.also {
logger.logAsyncTaskProgress(
- entry,
+ entry.logKey,
"extracted promoted notification content: $it",
)
}
@@ -726,7 +730,7 @@
AsyncHybridViewInflation.isEnabled &&
reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
) {
- logger.logAsyncTaskProgress(entry, "inflating single line view model")
+ logger.logAsyncTaskProgress(entry.logKey, "inflating single line view model")
SingleLineViewInflater.inflateSingleLineViewModel(
notification = entry.sbn.notification,
messagingStyle = messagingStyle,
@@ -743,7 +747,10 @@
LockscreenOtpRedaction.isEnabled &&
reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE != 0
) {
- logger.logAsyncTaskProgress(entry, "inflating public single line view model")
+ logger.logAsyncTaskProgress(
+ entry.logKey,
+ "inflating public single line view model"
+ )
if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
SingleLineViewInflater.inflateSingleLineViewModel(
notification = entry.sbn.notification,
@@ -834,11 +841,10 @@
logger: NotificationRowContentBinderLogger,
): NewRemoteViews {
return TraceUtils.trace("NotificationContentInflater.createRemoteViews") {
- val entryForLogging: NotificationEntry = row.entry
val contracted =
if (reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
logger.logAsyncTaskProgress(
- entryForLogging,
+ row.loggingKey,
"creating contracted remote view",
)
createContentView(builder, bindParams.isMinimized)
@@ -846,7 +852,7 @@
val expanded =
if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
logger.logAsyncTaskProgress(
- entryForLogging,
+ row.loggingKey,
"creating expanded remote view",
)
createExpandedView(builder, bindParams.isMinimized)
@@ -854,7 +860,7 @@
val headsUp =
if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
logger.logAsyncTaskProgress(
- entryForLogging,
+ row.loggingKey,
"creating heads up remote view",
)
val isHeadsUpCompact = headsUpStyleProvider.shouldApplyCompactStyle()
@@ -866,7 +872,10 @@
} else null
val public =
if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) {
- logger.logAsyncTaskProgress(entryForLogging, "creating public remote view")
+ logger.logAsyncTaskProgress(
+ row.loggingKey,
+ "creating public remote view"
+ )
if (
LockscreenOtpRedaction.isEnabled &&
bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT
@@ -888,7 +897,7 @@
reInflateFlags and FLAG_GROUP_SUMMARY_HEADER != 0
) {
logger.logAsyncTaskProgress(
- entryForLogging,
+ row.loggingKey,
"creating group summary remote view",
)
builder.makeNotificationGroupHeader()
@@ -899,7 +908,7 @@
reInflateFlags and FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER != 0
) {
logger.logAsyncTaskProgress(
- entryForLogging,
+ row.loggingKey,
"creating low-priority group summary remote view",
)
builder.makeLowPriorityContentView(true /* useRegularSubtext */)
@@ -971,14 +980,14 @@
val applyCallback: ApplyCallback =
object : ApplyCallback() {
override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "contracted view applied")
+ logger.logAsyncTaskProgress(entry.logKey, "contracted view applied")
result.inflatedContentView = v
}
override val remoteView: RemoteViews
get() = result.remoteViews.contracted
}
- logger.logAsyncTaskProgress(entry, "applying contracted view")
+ logger.logAsyncTaskProgress(entry.logKey, "applying contracted view")
applyRemoteView(
inflationExecutor = inflationExecutor,
inflateSynchronously = inflateSynchronously,
@@ -1010,14 +1019,14 @@
val applyCallback: ApplyCallback =
object : ApplyCallback() {
override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "expanded view applied")
+ logger.logAsyncTaskProgress(entry.logKey, "expanded view applied")
result.inflatedExpandedView = v
}
override val remoteView: RemoteViews
get() = result.remoteViews.expanded
}
- logger.logAsyncTaskProgress(entry, "applying expanded view")
+ logger.logAsyncTaskProgress(entry.logKey, "applying expanded view")
applyRemoteView(
inflationExecutor = inflationExecutor,
inflateSynchronously = inflateSynchronously,
@@ -1049,14 +1058,14 @@
val applyCallback: ApplyCallback =
object : ApplyCallback() {
override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "heads up view applied")
+ logger.logAsyncTaskProgress(entry.logKey, "heads up view applied")
result.inflatedHeadsUpView = v
}
override val remoteView: RemoteViews
get() = result.remoteViews.headsUp
}
- logger.logAsyncTaskProgress(entry, "applying heads up view")
+ logger.logAsyncTaskProgress(entry.logKey, "applying heads up view")
applyRemoteView(
inflationExecutor = inflationExecutor,
inflateSynchronously = inflateSynchronously,
@@ -1088,14 +1097,14 @@
val applyCallback: ApplyCallback =
object : ApplyCallback() {
override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "public view applied")
+ logger.logAsyncTaskProgress(entry.logKey, "public view applied")
result.inflatedPublicView = v
}
override val remoteView: RemoteViews
get() = result.remoteViews.public!!
}
- logger.logAsyncTaskProgress(entry, "applying public view")
+ logger.logAsyncTaskProgress(entry.logKey, "applying public view")
applyRemoteView(
inflationExecutor = inflationExecutor,
inflateSynchronously = inflateSynchronously,
@@ -1130,14 +1139,17 @@
val applyCallback: ApplyCallback =
object : ApplyCallback() {
override fun setResultView(v: View) {
- logger.logAsyncTaskProgress(entry, "group header view applied")
+ logger.logAsyncTaskProgress(
+ entry.logKey,
+ "group header view applied"
+ )
result.inflatedGroupHeaderView = v as NotificationHeaderView?
}
override val remoteView: RemoteViews
get() = result.remoteViews.normalGroupHeader!!
}
- logger.logAsyncTaskProgress(entry, "applying group header view")
+ logger.logAsyncTaskProgress(entry.logKey, "applying group header view")
applyRemoteView(
inflationExecutor = inflationExecutor,
inflateSynchronously = inflateSynchronously,
@@ -1173,7 +1185,7 @@
object : ApplyCallback() {
override fun setResultView(v: View) {
logger.logAsyncTaskProgress(
- entry,
+ entry.logKey,
"low-priority group header view applied",
)
result.inflatedMinimizedGroupHeaderView =
@@ -1183,7 +1195,10 @@
override val remoteView: RemoteViews
get() = result.remoteViews.minimizedGroupHeader!!
}
- logger.logAsyncTaskProgress(entry, "applying low priority group header view")
+ logger.logAsyncTaskProgress(
+ entry.logKey,
+ "applying low priority group header view"
+ )
applyRemoteView(
inflationExecutor = inflationExecutor,
inflateSynchronously = inflateSynchronously,
@@ -1221,7 +1236,7 @@
)
val cancellationSignal = CancellationSignal()
cancellationSignal.setOnCancelListener {
- logger.logAsyncTaskProgress(entry, "apply cancelled")
+ logger.logAsyncTaskProgress(entry.logKey, "apply cancelled")
Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row))
runningInflations.values.forEach(
Consumer { obj: CancellationSignal -> obj.cancel() }
@@ -1278,7 +1293,7 @@
handleInflationError(
runningInflations,
e,
- row.entry,
+ row,
callback,
logger,
"applying view synchronously",
@@ -1303,7 +1318,7 @@
handleInflationError(
runningInflations,
InflationException(invalidReason),
- row.entry,
+ row,
callback,
logger,
"applied invalid view",
@@ -1362,7 +1377,7 @@
handleInflationError(
runningInflations,
e,
- row.entry,
+ row,
callback,
logger,
"applying view",
@@ -1465,15 +1480,15 @@
private fun handleInflationError(
runningInflations: HashMap<Int, CancellationSignal>,
e: Exception,
- notification: NotificationEntry,
+ notification: ExpandableNotificationRow?,
callback: InflationCallback?,
logger: NotificationRowContentBinderLogger,
logContext: String,
) {
Assert.isMainThread()
- logger.logAsyncTaskException(notification, logContext, e)
+ logger.logAsyncTaskException(notification?.loggingKey, logContext, e)
runningInflations.values.forEach(Consumer { obj: CancellationSignal -> obj.cancel() })
- callback?.handleInflationException(notification, e)
+ callback?.handleInflationException(notification?.entry, e)
}
/**
@@ -1496,7 +1511,7 @@
if (runningInflations.isNotEmpty()) {
return false
}
- logger.logAsyncTaskProgress(entry, "finishing")
+ logger.logAsyncTaskProgress(row.loggingKey, "finishing")
// Put the new image index on the row
row.mImageModelIndex = result.rowImageInflater.getNewImageIndex()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
index a32e1d7..e94b2dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
@@ -35,66 +35,66 @@
class NotificationRowContentBinderLogger
@Inject
constructor(@NotifInflationLog private val buffer: LogBuffer) {
- fun logNotBindingRowWasRemoved(entry: NotificationEntry) {
+ fun logNotBindingRowWasRemoved(entry: String) {
buffer.log(
TAG,
LogLevel.INFO,
- { str1 = entry.logKey },
+ { str1 = entry },
{ "not inflating $str1: row was removed" }
)
}
- fun logBinding(entry: NotificationEntry, @InflationFlag flag: Int) {
+ fun logBinding(entry: String, @InflationFlag flag: Int) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
- str1 = entry.logKey
+ str1 = entry
int1 = flag
},
{ "binding views ${flagToString(int1)} for $str1" }
)
}
- fun logCancelBindAbortedTask(entry: NotificationEntry) {
+ fun logCancelBindAbortedTask(entry: String) {
buffer.log(
TAG,
LogLevel.INFO,
- { str1 = entry.logKey },
+ { str1 = entry },
{ "aborted task to cancel binding $str1" }
)
}
- fun logUnbinding(entry: NotificationEntry, @InflationFlag flag: Int) {
+ fun logUnbinding(entry: String, @InflationFlag flag: Int) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
- str1 = entry.logKey
+ str1 = entry
int1 = flag
},
{ "unbinding views ${flagToString(int1)} for $str1" }
)
}
- fun logAsyncTaskProgress(entry: NotificationEntry, progress: String) {
+ fun logAsyncTaskProgress(entry: String?, progress: String) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
- str1 = entry.logKey
+ str1 = entry
str2 = progress
},
{ "async task for $str1: $str2" }
)
}
- fun logAsyncTaskException(entry: NotificationEntry, logContext: String, exception: Throwable) {
+ fun logAsyncTaskException(entry: String?, logContext: String, exception: Throwable) {
buffer.log(
TAG,
LogLevel.DEBUG,
{
- str1 = entry.logKey
+ str1 = entry
str2 = logContext
str3 = exception.stackTraceToString()
},
@@ -103,7 +103,7 @@
}
fun logInflateSingleLine(
- entry: NotificationEntry,
+ entry: String?,
@InflationFlag inflationFlags: Int,
isConversation: Boolean
) {
@@ -111,7 +111,7 @@
TAG,
LogLevel.DEBUG,
{
- str1 = entry.logKey
+ str1 = entry
int1 = inflationFlags
bool1 = isConversation
},
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
index ea73b4b..e0c1692 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -417,8 +417,8 @@
): HybridNotificationView? {
if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null
- logger.logInflateSingleLine(entry, reinflateFlags, isConversation)
- logger.logAsyncTaskProgress(entry, "inflating single-line content view")
+ logger.logInflateSingleLine(entry.logKey, reinflateFlags, isConversation)
+ logger.logAsyncTaskProgress(entry.logKey, "inflating single-line content view")
var view: HybridNotificationView? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index da98858..9bd5a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -291,7 +291,7 @@
* currently being swiped. From the center outwards, the multipliers apply to the neighbors
* of the swiped view.
*/
- private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f)
+ private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.04f, 0.12f, 0.5f, 0.12f, 0.04f)
const val MAGNETIC_REDUCTION = 0.65f
@@ -299,7 +299,7 @@
private const val DETACH_STIFFNESS = 800f
private const val DETACH_DAMPING_RATIO = 0.95f
private const val SNAP_BACK_STIFFNESS = 550f
- private const val SNAP_BACK_DAMPING_RATIO = 0.52f
+ private const val SNAP_BACK_DAMPING_RATIO = 0.6f
// Maximum value of corner roundness that gets applied during the pre-detach dragging
private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 1d18535..048958e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -1458,7 +1458,7 @@
if (AsyncHybridViewInflation.isEnabled()) {
minExpandHeight += mMinSingleLineHeight;
} else {
- Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey()
+ Log.e(TAG, "getMinHeight: child " + child.getKey()
+ " single line view is null", new Exception());
}
}
@@ -1688,8 +1688,8 @@
public void addTransientView(View view, int index) {
if (mLogger != null && view instanceof ExpandableNotificationRow) {
mLogger.addTransientRow(
- ((ExpandableNotificationRow) view).getEntry(),
- getContainingNotification().getEntry(),
+ ((ExpandableNotificationRow) view).getLoggingKey(),
+ getContainingNotification().getLoggingKey(),
index
);
}
@@ -1700,8 +1700,8 @@
public void removeTransientView(View view) {
if (mLogger != null && view instanceof ExpandableNotificationRow) {
mLogger.removeTransientRow(
- ((ExpandableNotificationRow) view).getEntry(),
- getContainingNotification().getEntry()
+ ((ExpandableNotificationRow) view).getLoggingKey(),
+ getContainingNotification().getLoggingKey()
);
}
super.removeTransientView(view);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
index 4986b63..d8da412 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerLogger.kt
@@ -27,16 +27,16 @@
@Inject
constructor(@NotificationRenderLog private val notificationRenderBuffer: LogBuffer) {
fun addTransientRow(
- childEntry: NotificationEntry,
- containerEntry: NotificationEntry,
+ childEntry: String,
+ containerEntry: String,
index: Int
) {
notificationRenderBuffer.log(
TAG,
LogLevel.INFO,
{
- str1 = childEntry.logKey
- str2 = containerEntry.logKey
+ str1 = childEntry
+ str2 = containerEntry
int1 = index
},
{ "addTransientRow: childKey: $str1 -- containerKey: $str2 -- index: $int1" }
@@ -44,15 +44,15 @@
}
fun removeTransientRow(
- childEntry: NotificationEntry,
- containerEntry: NotificationEntry,
+ childEntry: String,
+ containerEntry: String,
) {
notificationRenderBuffer.log(
TAG,
LogLevel.INFO,
{
- str1 = childEntry.logKey
- str2 = containerEntry.logKey
+ str1 = childEntry
+ str2 = containerEntry
},
{ "removeTransientRow: childKey: $str1 -- containerKey: $str2" }
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index 043d64e..3d8fe01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -278,13 +278,13 @@
val fs =
when (val first = s.firstVisibleChild) {
null -> "(null)"
- is ExpandableNotificationRow -> first.entry.key
+ is ExpandableNotificationRow -> first.loggingKey
else -> Integer.toHexString(System.identityHashCode(first))
}
val ls =
when (val last = s.lastVisibleChild) {
null -> "(null)"
- is ExpandableNotificationRow -> last.entry.key
+ is ExpandableNotificationRow -> last.loggingKey
else -> Integer.toHexString(System.identityHashCode(last))
}
Log.d(TAG, "updateSections: f=$fs s=$i")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index fdb0e73..6313258 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -789,17 +789,17 @@
private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr,
boolean expected, boolean actual) {
if (mLogger == null) return;
- mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual);
+ mLogger.hunSkippedForUnexpectedState(enr.getLoggingKey(), expected, actual);
}
private void logHunAnimationSkipped(ExpandableNotificationRow enr, String reason) {
if (mLogger == null) return;
- mLogger.hunAnimationSkipped(enr.getEntry(), reason);
+ mLogger.hunAnimationSkipped(enr.getLoggingKey(), reason);
}
private void logHunAnimationEventAdded(ExpandableNotificationRow enr, int type) {
if (mLogger == null) return;
- mLogger.hunAnimationEventAdded(enr.getEntry(), type);
+ mLogger.hunAnimationEventAdded(enr.getLoggingKey(), type);
}
private void onDrawDebug(Canvas canvas) {
@@ -1810,16 +1810,22 @@
private ExpandableNotificationRow getTopHeadsUpRow() {
ExpandableNotificationRow row = mTopHeadsUpRow;
- if (row.isChildInGroup()) {
- final NotificationEntry groupSummary =
- mGroupMembershipManager.getGroupSummary(row.getEntry());
- if (groupSummary != null) {
- row = groupSummary.getRow();
+ if (NotificationBundleUi.isEnabled()) {
+ if (mGroupMembershipManager.isChildInGroup(row.getEntryAdapter())
+ && row.isChildInGroup()) {
+ row = row.getNotificationParent();
+ }
+ } else {
+ if (row.isChildInGroup()) {
+ final NotificationEntry groupSummary =
+ mGroupMembershipManager.getGroupSummary(row.getEntry());
+ if (groupSummary != null) {
+ row = groupSummary.getRow();
+ }
}
}
return row;
}
-
/**
* @return the position from where the appear transition ends when expanding.
* Measured in absolute height.
@@ -1966,10 +1972,19 @@
&& touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
if (slidingChild instanceof ExpandableNotificationRow row) {
NotificationEntry entry = row.getEntry();
+ boolean isEntrySummaryForTopHun;
+ if (NotificationBundleUi.isEnabled()) {
+ isEntrySummaryForTopHun = Objects.equals(
+ ((ExpandableNotificationRow) slidingChild).getNotificationParent(),
+ mTopHeadsUpRow);
+ } else {
+ isEntrySummaryForTopHun = mTopHeadsUpRow != null &&
+ mGroupMembershipManager.getGroupSummary(mTopHeadsUpRow.getEntry())
+ == entry;
+ }
if (!mIsExpanded && row.isHeadsUp() && row.isPinned()
&& mTopHeadsUpRow != row
- && mGroupMembershipManager.getGroupSummary(mTopHeadsUpRow.getEntry())
- != entry) {
+ && !isEntrySummaryForTopHun) {
continue;
}
return row.getViewAtPosition(touchY - childTop);
@@ -2902,17 +2917,17 @@
if (child instanceof ExpandableNotificationRow) {
if (container instanceof NotificationChildrenContainer) {
mLogger.addTransientChildNotificationToChildContainer(
- ((ExpandableNotificationRow) child).getEntry(),
+ ((ExpandableNotificationRow) child).getLoggingKey(),
((NotificationChildrenContainer) container)
- .getContainingNotification().getEntry()
+ .getContainingNotification().getLoggingKey()
);
} else if (container instanceof NotificationStackScrollLayout) {
mLogger.addTransientChildNotificationToNssl(
- ((ExpandableNotificationRow) child).getEntry()
+ ((ExpandableNotificationRow) child).getLoggingKey()
);
} else {
mLogger.addTransientChildNotificationToViewGroup(
- ((ExpandableNotificationRow) child).getEntry(),
+ ((ExpandableNotificationRow) child).getLoggingKey(),
container
);
}
@@ -2922,7 +2937,7 @@
@Override
public void addTransientView(View view, int index) {
if (mLogger != null && view instanceof ExpandableNotificationRow) {
- mLogger.addTransientRow(((ExpandableNotificationRow) view).getEntry(), index);
+ mLogger.addTransientRow(((ExpandableNotificationRow) view).getLoggingKey(), index);
}
super.addTransientView(view, index);
}
@@ -2930,7 +2945,7 @@
@Override
public void removeTransientView(View view) {
if (mLogger != null && view instanceof ExpandableNotificationRow) {
- mLogger.removeTransientRow(((ExpandableNotificationRow) view).getEntry());
+ mLogger.removeTransientRow(((ExpandableNotificationRow) view).getLoggingKey());
}
super.removeTransientView(view);
}
@@ -2981,7 +2996,7 @@
String key = "";
if (mDebugRemoveAnimation) {
if (child instanceof ExpandableNotificationRow) {
- key = ((ExpandableNotificationRow) child).getEntry().getKey();
+ key = ((ExpandableNotificationRow) child).getKey();
}
Log.d(TAG, "generateRemoveAnimation " + key);
}
@@ -3403,7 +3418,7 @@
+ " isHeadsUp=" + isHeadsUp
+ " type=" + type
+ " onBottom=" + onBottom
- + " row=" + row.getEntry().getKey());
+ + " row=" + row.getKey());
}
logHunAnimationEventAdded(row, type);
}
@@ -3478,7 +3493,7 @@
if (mDebugRemoveAnimation) {
String key = "";
if (child instanceof ExpandableNotificationRow) {
- key = ((ExpandableNotificationRow) child).getEntry().getKey();
+ key = ((ExpandableNotificationRow) child).getKey();
}
Log.d(TAG, "created Remove Event - SwipedOut: " + childWasSwipedOut + " " + key);
}
@@ -4366,7 +4381,7 @@
if (mLogger == null) {
return;
}
- mLogger.transientNotificationRowTraversalCleaned(transientView.getEntry(), reason);
+ mLogger.transientNotificationRowTraversalCleaned(transientView.getLoggingKey(), reason);
}
void onPanelTrackingStarted() {
@@ -5060,7 +5075,7 @@
+ " addAnimation=" + addAnimation
+ (row.getEntry() == null ? " entry NULL "
: " isSeenInShade=" + row.getEntry().isSeenInShade()
- + " row=" + row.getEntry().getKey())
+ + " row=" + row.getKey())
+ " mIsExpanded=" + mIsExpanded
+ " isHeadsUp=" + isHeadsUp);
}
@@ -6766,7 +6781,7 @@
NotificationChildrenContainer childrenContainer = row.getChildrenContainer();
if (childrenContainer == null) {
Log.wtf(TAG, "Tried to update group header alignment for something that's "
- + "not a group; key = " + row.getEntry().getKey());
+ + "not a group; key = " + row.getKey());
return;
}
NotificationHeaderView header = childrenContainer.getGroupHeader();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 01ef90a..5c96470 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -553,7 +553,7 @@
if (view instanceof ExpandableNotificationRow row) {
if (row.isHeadsUp()) {
mHeadsUpManager.addSwipedOutNotification(
- row.getEntry().getSbn().getKey());
+ row.getKey());
}
row.performDismiss(false /* fromAccessibility */);
}
@@ -598,7 +598,7 @@
&& (parent.areGutsExposed()
|| mSwipeHelper.getExposedMenuView() == parent
|| (parent.getAttachedChildren().size() == 1
- && mDismissibilityProvider.isDismissable(parent.getEntry())))) {
+ && mDismissibilityProvider.isDismissable(parent.getKey())))) {
// In this case the group is expanded and showing the menu for the
// group, further interaction should apply to the group, not any
// child notifications so we use the parent of the child. We also do the
@@ -642,7 +642,7 @@
&& row.getEntry().getSbn().getNotification().fullScreenIntent
== null) {
mHeadsUpManager.removeNotification(
- row.getEntry().getSbn().getKey(),
+ row.getKey(),
/* removeImmediately= */ true,
/* reason= */ "onChildSnappedBack"
);
@@ -1955,12 +1955,11 @@
@Override
public void bindRow(ExpandableNotificationRow row) {
row.setHeadsUpAnimatingAwayListener(animatingAway -> {
- NotificationEntry entry = row.getEntry();
- mHeadsUpAppearanceController.updateHeader(entry);
- mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(entry);
+ mHeadsUpAppearanceController.updateHeader(row);
+ mHeadsUpAppearanceController.updateHeadsUpAndPulsingRoundness(row);
if (GroupHunAnimationFix.isEnabled() && !animatingAway) {
// invalidate list to make sure the row is sorted to the correct section
- mHeadsUpManager.onEntryAnimatingAwayEnded(entry);
+ mHeadsUpManager.onEntryAnimatingAwayEnded(row.getEntry());
}
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 30658710..871b81d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -23,15 +23,15 @@
@NotificationRenderLog private val notificationRenderBuffer: LogBuffer,
@ShadeLog private val shadeLogBuffer: LogBuffer,
) {
- fun hunAnimationSkipped(entry: NotificationEntry, reason: String) {
+ fun hunAnimationSkipped(entry: String, reason: String) {
buffer.log(TAG, INFO, {
- str1 = entry.logKey
+ str1 = entry
str2 = reason
}, {
"heads up animation skipped: key: $str1 reason: $str2"
})
}
- fun hunAnimationEventAdded(entry: NotificationEntry, type: Int) {
+ fun hunAnimationEventAdded(entry: String, type: Int) {
val reason: String
reason = if (type == ANIMATION_TYPE_HEADS_UP_DISAPPEAR) {
"HEADS_UP_DISAPPEAR"
@@ -47,16 +47,16 @@
type.toString()
}
buffer.log(TAG, INFO, {
- str1 = entry.logKey
+ str1 = entry
str2 = reason
}, {
"heads up animation added: $str1 with type $str2"
})
}
- fun hunSkippedForUnexpectedState(entry: NotificationEntry, expected: Boolean, actual: Boolean) {
+ fun hunSkippedForUnexpectedState(entry: String, expected: Boolean, actual: Boolean) {
buffer.log(TAG, INFO, {
- str1 = entry.logKey
+ str1 = entry
bool1 = expected
bool2 = actual
}, {
@@ -84,9 +84,9 @@
})
}
- fun transientNotificationRowTraversalCleaned(entry: NotificationEntry, reason: String) {
+ fun transientNotificationRowTraversalCleaned(entry: String, reason: String) {
notificationRenderBuffer.log(TAG, INFO, {
- str1 = entry.logKey
+ str1 = entry
str2 = reason
}, {
"transientNotificationRowTraversalCleaned: key: $str1 reason: $str2"
@@ -94,12 +94,12 @@
}
fun addTransientChildNotificationToChildContainer(
- childEntry: NotificationEntry,
- containerEntry: NotificationEntry,
+ childEntry: String,
+ containerEntry: String,
) {
notificationRenderBuffer.log(TAG, INFO, {
- str1 = childEntry.logKey
- str2 = containerEntry.logKey
+ str1 = childEntry
+ str2 = containerEntry
}, {
"addTransientChildToContainer from onViewRemovedInternal: childKey: $str1 " +
"-- containerKey: $str2"
@@ -107,21 +107,21 @@
}
fun addTransientChildNotificationToNssl(
- childEntry: NotificationEntry,
+ childEntry: String,
) {
notificationRenderBuffer.log(TAG, INFO, {
- str1 = childEntry.logKey
+ str1 = childEntry
}, {
"addTransientRowToNssl from onViewRemovedInternal: childKey: $str1"
})
}
fun addTransientChildNotificationToViewGroup(
- childEntry: NotificationEntry,
+ childEntry: String,
container: ViewGroup
) {
notificationRenderBuffer.log(TAG, ERROR, {
- str1 = childEntry.logKey
+ str1 = childEntry
str2 = container.toString()
}, {
"addTransientRowTo unhandled ViewGroup from onViewRemovedInternal: childKey: $str1 " +
@@ -130,14 +130,14 @@
}
fun addTransientRow(
- childEntry: NotificationEntry,
+ childEntry: String,
index: Int
) {
notificationRenderBuffer.log(
TAG,
INFO,
{
- str1 = childEntry.logKey
+ str1 = childEntry
int1 = index
},
{ "addTransientRow to NSSL: childKey: $str1 -- index: $int1" }
@@ -145,13 +145,13 @@
}
fun removeTransientRow(
- childEntry: NotificationEntry,
+ childEntry: String,
) {
notificationRenderBuffer.log(
TAG,
INFO,
{
- str1 = childEntry.logKey
+ str1 = childEntry
},
{ "removeTransientRow from NSSL: childKey: $str1" }
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 08692be..88d3ad8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -275,11 +275,7 @@
public static void debugLogView(View view, String s) {
String viewString = "";
if (view instanceof ExpandableNotificationRow row) {
- if (row.getEntry() == null) {
- viewString = "ExpandableNotificationRow has null NotificationEntry";
- } else {
- viewString = row.getEntry().getSbn().getId() + "";
- }
+ viewString = row.getKey();
} else if (view == null) {
viewString = "View is null";
} else if (view instanceof SectionHeaderView) {
@@ -413,10 +409,8 @@
*/
public boolean isCyclingOut(ExpandableNotificationRow row, AmbientState ambientState) {
if (!NotificationHeadsUpCycling.isEnabled()) return false;
- if (row.getEntry() == null) return false;
- if (row.getEntry().getKey() == null) return false;
String cyclingOutKey = ambientState.getAvalanchePreviousHunKey();
- return row.getEntry().getKey().equals(cyclingOutKey);
+ return row.getKey().equals(cyclingOutKey);
}
/**
@@ -424,10 +418,8 @@
*/
public boolean isCyclingIn(ExpandableNotificationRow row, AmbientState ambientState) {
if (!NotificationHeadsUpCycling.isEnabled()) return false;
- if (row.getEntry() == null) return false;
- if (row.getEntry().getKey() == null) return false;
String cyclingInKey = ambientState.getAvalancheShowingHunKey();
- return row.getEntry().getKey().equals(cyclingInKey);
+ return row.getKey().equals(cyclingInKey);
}
/** Updates the dimmed and hiding sensitive states of the children. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
index 2b05223..4da418e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java
@@ -422,7 +422,7 @@
if (changingView instanceof ExpandableNotificationRow && mLogger != null) {
loggable = true;
isHeadsUp = ((ExpandableNotificationRow) changingView).isHeadsUp();
- key = ((ExpandableNotificationRow) changingView).getEntry().getKey();
+ key = ((ExpandableNotificationRow) changingView).getKey();
}
if (event.animationType ==
NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 548ab83..7f4ac37 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -215,13 +215,13 @@
@Override
public void onHeadsUpPinned(NotificationEntry entry) {
updatePinnedStatus();
- updateHeader(entry);
- updateHeadsUpAndPulsingRoundness(entry);
+ updateHeader(entry.getRow());
+ updateHeadsUpAndPulsingRoundness(entry.getRow());
}
@Override
public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) {
- updateHeadsUpAndPulsingRoundness(entry);
+ updateHeadsUpAndPulsingRoundness(entry.getRow());
mPhoneStatusBarTransitions.onHeadsUpStateChanged(isHeadsUp);
}
@@ -416,8 +416,8 @@
@Override
public void onHeadsUpUnPinned(NotificationEntry entry) {
updatePinnedStatus();
- updateHeader(entry);
- updateHeadsUpAndPulsingRoundness(entry);
+ updateHeader(entry.getRow());
+ updateHeadsUpAndPulsingRoundness(entry.getRow());
}
public void setAppearFraction(float expandedHeight, float appearFraction) {
@@ -448,9 +448,8 @@
ExpandableNotificationRow previousTracked = mTrackedChild;
mTrackedChild = trackedChild;
if (previousTracked != null) {
- NotificationEntry entry = previousTracked.getEntry();
- updateHeader(entry);
- updateHeadsUpAndPulsingRoundness(entry);
+ updateHeader(previousTracked);
+ updateHeadsUpAndPulsingRoundness(previousTracked);
}
}
@@ -460,13 +459,12 @@
private void updateHeadsUpHeaders() {
mHeadsUpManager.getAllEntries().forEach(entry -> {
- updateHeader(entry);
- updateHeadsUpAndPulsingRoundness(entry);
+ updateHeader(entry.getRow());
+ updateHeadsUpAndPulsingRoundness(entry.getRow());
});
}
- public void updateHeader(NotificationEntry entry) {
- ExpandableNotificationRow row = entry.getRow();
+ public void updateHeader(ExpandableNotificationRow row) {
float headerVisibleAmount = 1.0f;
// To fix the invisible HUN group header issue
if (!AsyncGroupHeaderViewInflation.isEnabled()) {
@@ -480,10 +478,9 @@
/**
* Update the HeadsUp and the Pulsing roundness based on current state
- * @param entry target notification
+ * @param row target notification row
*/
- public void updateHeadsUpAndPulsingRoundness(NotificationEntry entry) {
- ExpandableNotificationRow row = entry.getRow();
+ public void updateHeadsUpAndPulsingRoundness(ExpandableNotificationRow row) {
boolean isTrackedChild = row == mTrackedChild;
if (row.isPinned() || row.isHeadsUpAnimatingAway() || isTrackedChild) {
float roundness = MathUtils.saturate(1f - mAppearFraction);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
index 686efb7..edaf400 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LSShadeTransitionLogger.kt
@@ -34,9 +34,8 @@
private val displayMetrics: DisplayMetrics
) {
fun logUnSuccessfulDragDown(startingChild: View?) {
- val entry = (startingChild as? ExpandableNotificationRow)?.entry
buffer.log(TAG, LogLevel.INFO, {
- str1 = entry?.key ?: "no entry"
+ str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
}, {
"Tried to drag down but can't drag down on $str1"
})
@@ -49,27 +48,24 @@
}
fun logDragDownStarted(startingChild: ExpandableView?) {
- val entry = (startingChild as? ExpandableNotificationRow)?.entry
buffer.log(TAG, LogLevel.INFO, {
- str1 = entry?.key ?: "no entry"
+ str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
}, {
"The drag down has started on $str1"
})
}
fun logDraggedDownLockDownShade(startingChild: View?) {
- val entry = (startingChild as? ExpandableNotificationRow)?.entry
buffer.log(TAG, LogLevel.INFO, {
- str1 = entry?.key ?: "no entry"
+ str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
}, {
"Dragged down in locked down shade on $str1"
})
}
fun logDraggedDown(startingChild: View?, dragLengthY: Int) {
- val entry = (startingChild as? ExpandableNotificationRow)?.entry
buffer.log(TAG, LogLevel.INFO, {
- str1 = entry?.key ?: "no entry"
+ str1 = (startingChild as? ExpandableNotificationRow)?.loggingKey ?: "no entry"
}, {
"Drag down succeeded on $str1"
})
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index ba41fd4..df1680a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -670,7 +670,7 @@
}
private void removeHunAfterClick(ExpandableNotificationRow row) {
- String key = row.getEntry().getSbn().getKey();
+ String key = row.getKey();
if (mHeadsUpManager != null && mHeadsUpManager.isHeadsUpEntry(key)) {
// Release the HUN notification to the shade.
if (mPresenter.isPresenterFullyCollapsed()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 4d1d64e..74b1c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -58,6 +58,7 @@
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
import com.android.systemui.statusbar.notification.domain.interactor.NotificationAlertsInteractor;
@@ -262,6 +263,23 @@
}
}
+ @Override
+ public void onExpandClicked(ExpandableNotificationRow row, EntryAdapter clickedEntry,
+ boolean nowExpanded) {
+ mHeadsUpManager.setExpanded(clickedEntry.getKey(), row, nowExpanded);
+ mPowerInteractor.wakeUpIfDozing("NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
+ if (nowExpanded) {
+ if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
+ mShadeTransitionController.goToLockedShade(row, /* needsQSAnimation = */ true);
+ } else if (clickedEntry.isSensitive().getValue() && isInLockedDownShade()) {
+ mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
+ // launch the bouncer if the device is locked
+ mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */
+ , null /* cancelRunnable */, false /* afterKeyguardGone */);
+ }
+ }
+ }
+
/** @return true if the Shade is shown over the Lockscreen, and the device is locked */
private boolean isInLockedDownShade() {
if (SceneContainerFlag.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index a0e3fbd..8b0f7c4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -29,18 +29,13 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.SliderState
-import androidx.compose.material3.VerticalSlider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
@@ -49,16 +44,17 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.compose.ui.graphics.painter.DrawablePainter
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
-import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
import javax.inject.Inject
-import kotlin.math.round
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
@@ -90,7 +86,7 @@
}
}
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun VolumeDialogSlider(
viewModel: VolumeDialogSliderViewModel,
@@ -108,59 +104,8 @@
)
val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null)
val sliderStateModel = collectedSliderStateModel ?: return
-
- val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt()
-
val interactionSource = remember { MutableInteractionSource() }
- val hapticsViewModel: SliderHapticsViewModel? =
- hapticsViewModelFactory?.let {
- rememberViewModel(traceName = "SliderHapticsViewModel") {
- it.create(
- interactionSource,
- sliderStateModel.valueRange,
- Orientation.Vertical,
- VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
- sliderStateModel.valueRange
- ),
- VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
- )
- }
- }
- val sliderState =
- remember(steps, sliderStateModel.valueRange) {
- SliderState(
- value = sliderStateModel.value,
- valueRange = sliderStateModel.valueRange,
- steps = steps,
- )
- .also { sliderState ->
- sliderState.onValueChangeFinished = {
- viewModel.onSliderChangeFinished(sliderState.value)
- hapticsViewModel?.onValueChangeEnded()
- }
- sliderState.onValueChange = { newValue ->
- sliderState.value = newValue
- hapticsViewModel?.addVelocityDataPoint(newValue)
- overscrollViewModel.setSlider(
- value = sliderState.value,
- min = sliderState.valueRange.start,
- max = sliderState.valueRange.endInclusive,
- )
- viewModel.setStreamVolume(newValue, true)
- }
- }
- }
-
- var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) }
- LaunchedEffect(sliderStateModel.value) {
- val value = sliderStateModel.value
- sliderState.value = value
- if (value != lastDiscreteStep) {
- lastDiscreteStep = value
- hapticsViewModel?.onValueChange(value)
- }
- }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
when (it) {
@@ -171,24 +116,33 @@
}
}
- VerticalSlider(
- state = sliderState,
- enabled = !sliderStateModel.isDisabled,
- reverseDirection = true,
+ Slider(
+ value = sliderStateModel.value,
+ valueRange = sliderStateModel.valueRange,
+ onValueChanged = { value ->
+ overscrollViewModel.setSlider(
+ value = value,
+ min = sliderStateModel.valueRange.start,
+ max = sliderStateModel.valueRange.endInclusive,
+ )
+ viewModel.setStreamVolume(value, true)
+ },
+ onValueChangeFinished = { viewModel.onSliderChangeFinished(it) },
+ isEnabled = !sliderStateModel.isDisabled,
+ isReverseDirection = true,
+ isVertical = true,
colors = colors,
interactionSource = interactionSource,
- modifier =
- modifier.pointerInput(Unit) {
- coroutineScope {
- val currentContext = currentCoroutineContext()
- awaitPointerEventScope {
- while (currentContext.isActive) {
- viewModel.onTouchEvent(awaitPointerEvent())
- }
- }
- }
- },
- track = {
+ haptics =
+ hapticsViewModelFactory?.let {
+ Haptics.Enabled(
+ hapticsViewModelFactory = it,
+ hapticFilter = SliderHapticFeedbackFilter(),
+ orientation = Orientation.Vertical,
+ )
+ } ?: Haptics.Disabled,
+ stepDistance = 1f,
+ track = { sliderState ->
VolumeDialogSliderTrack(
sliderState,
colors = colors,
@@ -201,6 +155,19 @@
},
)
},
+ accessibilityParams =
+ AccessibilityParams(label = "", currentStateDescription = "", disabledMessage = ""),
+ modifier =
+ modifier.pointerInput(Unit) {
+ coroutineScope {
+ val currentContext = currentCoroutineContext()
+ awaitPointerEventScope {
+ while (currentContext.isActive) {
+ viewModel.onTouchEvent(awaitPointerEvent())
+ }
+ }
+ }
+ },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
index 3efb2b4..3d98eba 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
@@ -116,8 +116,8 @@
override val isEnabled: Boolean
get() = true
- override val a11yStep: Int
- get() = 1
+ override val a11yStep: Float
+ get() = 1f
override val disabledMessage: String?
get() = null
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index f9d776b..9d32285 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -165,7 +165,7 @@
label = label,
disabledMessage = disabledMessage,
isEnabled = isEnabled,
- a11yStep = volumeRange.step,
+ a11yStep = volumeRange.step.toFloat(),
a11yClickDescription =
if (isAffectedByMute) {
context.getString(
@@ -307,7 +307,7 @@
override val label: String,
override val disabledMessage: String?,
override val isEnabled: Boolean,
- override val a11yStep: Int,
+ override val a11yStep: Float,
override val a11yClickDescription: String?,
override val a11yStateDescription: String?,
override val isMutable: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index d74a433..a6c8091 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -86,7 +86,7 @@
icon = Icon.Resource(R.drawable.ic_cast, null),
label = context.getString(R.string.media_device_cast),
isEnabled = true,
- a11yStep = 1,
+ a11yStep = 1f,
)
}
@@ -96,7 +96,7 @@
override val icon: Icon,
override val label: String,
override val isEnabled: Boolean,
- override val a11yStep: Int,
+ override val a11yStep: Float,
) : SliderState {
override val hapticFilter: SliderHapticFeedbackFilter
get() = SliderHapticFeedbackFilter()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index f135371..4bc237b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,7 +36,7 @@
* A11y slider controls works by adjusting one step up or down. The default slider step isn't
* enough to trigger rounding to the correct value.
*/
- val a11yStep: Int
+ val a11yStep: Float
val a11yClickDescription: String?
val a11yStateDescription: String?
val disabledMessage: String?
@@ -49,7 +49,7 @@
override val icon: Icon? = null
override val label: String = ""
override val disabledMessage: String? = null
- override val a11yStep: Int = 0
+ override val a11yStep: Float = 0f
override val a11yClickDescription: String? = null
override val a11yStateDescription: String? = null
override val isEnabled: Boolean = true
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
new file mode 100644
index 0000000..d3562e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+
+package com.android.systemui.volume.ui.slider
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.material3.VerticalSlider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
+import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import kotlin.math.round
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private val defaultSpring =
+ SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)
+private val defaultTrack: @Composable (SliderState) -> Unit =
+ @Composable { SliderDefaults.Track(it) }
+
+@Composable
+fun Slider(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ onValueChanged: (Float) -> Unit,
+ onValueChangeFinished: ((Float) -> Unit)?,
+ stepDistance: Float,
+ isEnabled: Boolean,
+ accessibilityParams: AccessibilityParams,
+ modifier: Modifier = Modifier,
+ colors: SliderColors = SliderDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ haptics: Haptics = Haptics.Disabled,
+ isVertical: Boolean = false,
+ isReverseDirection: Boolean = false,
+ track: (@Composable (SliderState) -> Unit)? = null,
+) {
+ require(stepDistance > 0) { "stepDistance must be positive" }
+ val coroutineScope = rememberCoroutineScope()
+ val snappedValue = snapValue(value, valueRange, stepDistance)
+ val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)
+
+ val animatable = remember { Animatable(snappedValue) }
+ var animationJob: Job? by remember { mutableStateOf(null) }
+ val sliderState =
+ remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }
+ val valueChange: (Float) -> Unit = { newValue ->
+ hapticsViewModel?.onValueChange(newValue)
+ val snappedNewValue = snapValue(newValue, valueRange, stepDistance)
+ if (animatable.targetValue != snappedNewValue) {
+ onValueChanged(snappedNewValue)
+ animationJob?.cancel()
+ animationJob =
+ coroutineScope.launch {
+ animatable.animateTo(
+ targetValue = snappedNewValue,
+ animationSpec = defaultSpring,
+ )
+ }
+ }
+ }
+ val semantics =
+ accessibilityParams.createSemantics(
+ animatable.targetValue,
+ valueRange,
+ valueChange,
+ isEnabled,
+ stepDistance,
+ )
+
+ LaunchedEffect(snappedValue) {
+ if (!animatable.isRunning && animatable.targetValue != snappedValue) {
+ animationJob?.cancel()
+ animationJob =
+ coroutineScope.launch {
+ animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring)
+ }
+ }
+ }
+
+ sliderState.onValueChangeFinished = {
+ hapticsViewModel?.onValueChangeEnded()
+ onValueChangeFinished?.invoke(animatable.targetValue)
+ }
+ sliderState.onValueChange = valueChange
+ sliderState.value = animatable.value
+
+ if (isVertical) {
+ VerticalSlider(
+ state = sliderState,
+ enabled = isEnabled,
+ reverseDirection = isReverseDirection,
+ interactionSource = interactionSource,
+ colors = colors,
+ track = track ?: defaultTrack,
+ modifier = modifier.clearAndSetSemantics(semantics),
+ )
+ } else {
+ Slider(
+ state = sliderState,
+ enabled = isEnabled,
+ interactionSource = interactionSource,
+ colors = colors,
+ track = track ?: defaultTrack,
+ modifier = modifier.clearAndSetSemantics(semantics),
+ )
+ }
+}
+
+private fun snapValue(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ stepDistance: Float,
+): Float {
+ if (stepDistance == 0f) {
+ return value
+ }
+ val coercedValue = value.coerceIn(valueRange)
+ return Math.round(coercedValue / stepDistance) * stepDistance
+}
+
+@Composable
+private fun AccessibilityParams.createSemantics(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ onValueChanged: (Float) -> Unit,
+ isEnabled: Boolean,
+ stepDistance: Float,
+): SemanticsPropertyReceiver.() -> Unit {
+ val semanticsContentDescription =
+ disabledMessage
+ ?.takeIf { !isEnabled }
+ ?.let { message ->
+ stringResource(R.string.volume_slider_disabled_message_template, label, message)
+ } ?: label
+ return {
+ contentDescription = semanticsContentDescription
+ if (isEnabled) {
+ currentStateDescription?.let { stateDescription = it }
+ progressBarRangeInfo = ProgressBarRangeInfo(value, valueRange)
+ } else {
+ disabled()
+ }
+ setProgress { targetValue ->
+ val targetDirection =
+ when {
+ targetValue > value -> 1
+ targetValue < value -> -1
+ else -> 0
+ }
+
+ val newValue =
+ (value + targetDirection * stepDistance).coerceIn(
+ valueRange.start,
+ valueRange.endInclusive,
+ )
+ onValueChanged(newValue)
+ true
+ }
+ }
+}
+
+@Composable
+private fun Haptics.createViewModel(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ interactionSource: MutableInteractionSource,
+): SliderHapticsViewModel? {
+ return when (this) {
+ is Haptics.Disabled -> null
+ is Haptics.Enabled -> {
+ hapticsViewModelFactory.let {
+ rememberViewModel(traceName = "SliderHapticsViewModel") {
+ it.create(
+ interactionSource,
+ valueRange,
+ orientation,
+ VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
+ valueRange,
+ hapticFilter,
+ ),
+ VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
+ )
+ }
+ .also { hapticsViewModel ->
+ var lastDiscreteStep by remember { mutableFloatStateOf(value) }
+ LaunchedEffect(value) {
+ snapshotFlow { value }
+ .map { round(it) }
+ .filter { it != lastDiscreteStep }
+ .distinctUntilChanged()
+ .collect { discreteStep ->
+ lastDiscreteStep = discreteStep
+ hapticsViewModel.onValueChange(discreteStep)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+data class AccessibilityParams(
+ val label: String,
+ val currentStateDescription: String?,
+ val disabledMessage: String?,
+)
+
+sealed interface Haptics {
+ data object Disabled : Haptics
+
+ data class Enabled(
+ val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+ val hapticFilter: SliderHapticFeedbackFilter,
+ val orientation: Orientation,
+ ) : Haptics
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 88c2697..5c26dac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -1573,6 +1573,25 @@
assertThat(items.get(1).isFirstDeviceInGroup()).isFalse();
}
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
+ @Test
+ public void deviceListUpdateWithDifferentDevices_firstSelectedDeviceIsFirstDeviceInGroup() {
+ when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
+ doReturn(mMediaDevices)
+ .when(mLocalMediaManager)
+ .getSelectedMediaDevice();
+ mMediaSwitchingController.start(mCb);
+ reset(mCb);
+ mMediaSwitchingController.getMediaItemList().clear();
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+ mMediaDevices.clear();
+ mMediaDevices.add(mMediaDevice2);
+ mMediaSwitchingController.onDeviceListUpdate(mMediaDevices);
+
+ List<MediaItem> items = mMediaSwitchingController.getMediaItemList();
+ assertThat(items.get(0).isFirstDeviceInGroup()).isTrue();
+ }
+
private int getNumberOfConnectDeviceButtons() {
int numberOfConnectDeviceButtons = 0;
for (MediaItem item : mMediaSwitchingController.getMediaItemList()) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
index 3d0a8f6..ebbe023 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
@@ -878,4 +878,18 @@
mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE);
mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE);
}
+
+ @Test
+ public void updateDialog_wifiIsDisabled_turnOffProgressBar() {
+ when(mInternetDetailsContentController.isWifiEnabled()).thenReturn(false);
+ mInternetDialogDelegateLegacy.mIsProgressBarVisible = true;
+
+ mInternetDialogDelegateLegacy.updateDialog(false);
+
+ mBgExecutor.runAllReady();
+ mInternetDialogDelegateLegacy.mDataInternetContent.observe(
+ mInternetDialogDelegateLegacy.mLifecycleOwner, i -> {
+ assertThat(mInternetDialogDelegateLegacy.mIsProgressBarVisible).isFalse();
+ });
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index c5b19ab..0b2fea5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -31,13 +31,13 @@
import android.view.View
import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.runner.RunWith
import org.junit.Test
import org.mockito.Answers
-import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito
@@ -66,11 +66,14 @@
@Mock
private lateinit var dialog: ChannelEditorDialog
+ private val shadeDialogContextInteractor = FakeShadeDialogContextInteractor(mContext)
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
`when`(dialogBuilder.build()).thenReturn(dialog)
- controller = ChannelEditorDialogController(mContext, mockNoMan, dialogBuilder)
+ controller =
+ ChannelEditorDialogController(shadeDialogContextInteractor, mockNoMan, dialogBuilder)
channel1 = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT)
channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_NONE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 24d8d1c..acfa94a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -611,7 +611,8 @@
public void testCanDismiss_immediately() throws Exception {
ExpandableNotificationRow row =
mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
- when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+ when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(
+ row.getEntry().getKey()))
.thenReturn(true);
row.performDismiss(false);
verify(mNotificationTestHelper.getOnUserInteractionCallback())
@@ -623,7 +624,8 @@
public void testCanDismiss() throws Exception {
ExpandableNotificationRow row =
mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
- when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+ when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(
+ row.getEntry().getKey()))
.thenReturn(true);
row.performDismiss(false);
TestableLooper.get(this).processAllMessages();
@@ -635,7 +637,8 @@
public void testCannotDismiss() throws Exception {
ExpandableNotificationRow row =
mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
- when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(row.getEntry()))
+ when(mNotificationTestHelper.getDismissibilityProvider().isDismissable(
+ row.getEntry().getKey()))
.thenReturn(false);
row.performDismiss(false);
verify(mNotificationTestHelper.getOnUserInteractionCallback(), never())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
similarity index 96%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 6066a38..9e914ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -65,10 +65,10 @@
mobileConnectionsRepository.fake.run {
setMobileConnectionRepositoryMap(
mapOf(
- SUB_1_ID to CONNECTION_1,
- SUB_2_ID to CONNECTION_2,
- SUB_3_ID to CONNECTION_3,
- SUB_4_ID to CONNECTION_4,
+ SUB_1_ID to FakeMobileConnectionRepository(SUB_1_ID, mock()),
+ SUB_2_ID to FakeMobileConnectionRepository(SUB_2_ID, mock()),
+ SUB_3_ID to FakeMobileConnectionRepository(SUB_3_ID, mock()),
+ SUB_4_ID to FakeMobileConnectionRepository(SUB_4_ID, mock()),
)
)
setActiveMobileDataSubscriptionId(SUB_1_ID)
@@ -496,7 +496,10 @@
@Test
fun activeDataConnection_turnedOn() =
kosmos.runTest {
- CONNECTION_1.setDataEnabled(true)
+ (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+ as FakeMobileConnectionRepository)
+ .dataEnabled
+ .value = true
val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
@@ -506,10 +509,17 @@
@Test
fun activeDataConnection_turnedOff() =
kosmos.runTest {
- CONNECTION_1.setDataEnabled(true)
+ (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+ as FakeMobileConnectionRepository)
+ .dataEnabled
+ .value = true
+
val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
- CONNECTION_1.setDataEnabled(false)
+ (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+ as FakeMobileConnectionRepository)
+ .dataEnabled
+ .value = false
assertThat(latest).isFalse()
}
@@ -921,20 +931,18 @@
@EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME)
fun isStackable_checksForTerrestrialConnections() =
kosmos.runTest {
- val exclusivelyNonTerrestrialSub =
- SubscriptionModel(
- isExclusivelyNonTerrestrial = true,
- subscriptionId = 5,
- carrierName = "Carrier 5",
- profileClass = PROFILE_CLASS_UNSET,
- )
-
val latest by collectLastValue(underTest.isStackable)
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
+ setNumberOfLevelsForSubId(SUB_1_ID, 5)
+ setNumberOfLevelsForSubId(SUB_2_ID, 5)
assertThat(latest).isTrue()
- connectionsRepository.setSubscriptions(listOf(SUB_1, exclusivelyNonTerrestrialSub))
+ (fakeMobileConnectionsRepository.getRepoForSubId(SUB_1_ID)
+ as FakeMobileConnectionRepository)
+ .isNonTerrestrial
+ .value = true
+
assertThat(latest).isFalse()
}
@@ -1006,7 +1014,6 @@
carrierName = "Carrier $SUB_1_ID",
profileClass = PROFILE_CLASS_UNSET,
)
- private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, mock())
private const val SUB_2_ID = 2
private val SUB_2 =
@@ -1015,7 +1022,6 @@
carrierName = "Carrier $SUB_2_ID",
profileClass = PROFILE_CLASS_UNSET,
)
- private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, mock())
private const val SUB_3_ID = 3
private val SUB_3_OPP =
@@ -1026,7 +1032,6 @@
carrierName = "Carrier $SUB_3_ID",
profileClass = PROFILE_CLASS_UNSET,
)
- private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, mock())
private const val SUB_4_ID = 4
private val SUB_4_OPP =
@@ -1037,6 +1042,5 @@
carrierName = "Carrier $SUB_4_ID",
profileClass = PROFILE_CLASS_UNSET,
)
- private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, mock())
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
index bc1c60c..c058490 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
@@ -33,7 +33,4 @@
override suspend fun handleInput(input: QSTileInput<T>) {
mutex.withLock { mutableInputs.add(input) }
}
-
- override var detailsViewModel: TileDetailsViewModel? =
- FakeTileDetailsViewModel("FakeQSTileUserActionInteractor")
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index 3ebef02..b616766 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -23,13 +23,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.os.Bundle;
import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
import android.platform.test.annotations.internal.InnerRunner;
import android.util.Log;
-import androidx.test.platform.app.InstrumentationRegistry;
-
import com.android.ravenwood.common.RavenwoodCommonUtils;
import org.junit.rules.TestRule;
@@ -285,11 +282,6 @@
private boolean onBefore(Description description, Scope scope, Order order) {
Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
- if (scope == Scope.Instance && order == Order.Outer) {
- // Start of a test method.
- mState.enterTestMethod(description);
- }
-
final var classDescription = getDescription();
// Class-level annotations are checked by the runner already, so we only check
@@ -299,6 +291,12 @@
return false;
}
}
+
+ if (scope == Scope.Instance && order == Order.Outer) {
+ // Start of a test method.
+ mState.enterTestMethod(description);
+ }
+
return true;
}
@@ -314,8 +312,7 @@
if (scope == Scope.Instance && order == Order.Outer) {
// End of a test method.
- mState.exitTestMethod();
-
+ mState.exitTestMethod(description);
}
// If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
index 70bc52b..705186e 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -81,12 +81,15 @@
RavenwoodRuntimeEnvironmentController.exitTestClass();
}
+ /** Called when a test method is about to start */
public void enterTestMethod(Description description) {
mMethodDescription = description;
- RavenwoodRuntimeEnvironmentController.initForMethod();
+ RavenwoodRuntimeEnvironmentController.enterTestMethod(description);
}
- public void exitTestMethod() {
+ /** Called when a test method finishes */
+ public void exitTestMethod(Description description) {
+ RavenwoodRuntimeEnvironmentController.exitTestMethod(description);
mMethodDescription = null;
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index f205d23..d935626 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -51,6 +51,7 @@
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.Message;
import android.os.Process_ravenwood;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
@@ -74,6 +75,7 @@
import com.android.server.LocalServices;
import com.android.server.compat.PlatformCompat;
+import org.junit.AssumptionViolatedException;
import org.junit.internal.management.ManagementFactory;
import org.junit.runner.Description;
@@ -81,6 +83,7 @@
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -93,6 +96,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
/**
* Responsible for initializing and the environment.
@@ -107,32 +111,60 @@
@SuppressWarnings("UnusedVariable")
private static final PrintStream sStdErr = System.err;
- private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+ private static final String MAIN_THREAD_NAME = "Ravenwood:Main";
+ private static final String TESTS_THREAD_NAME = "Ravenwood:Test";
+
private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;
+ static volatile Thread sTestThread;
+ static volatile Thread sMainThread;
+
/**
* When enabled, attempt to dump all thread stacks just before we hit the
* overall Tradefed timeout, to aid in debugging deadlocks.
+ *
+ * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it.
+ * It'll merely print stacktraces.
*/
private static final boolean ENABLE_TIMEOUT_STACKS =
- "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+ !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
- private static final int TIMEOUT_MILLIS = 9_000;
+ private static final boolean TOLERATE_LOOPER_ASSERTS =
+ !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));
+
+ static final int DEFAULT_TIMEOUT_SECONDS = 10;
+ private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
+
+ static int getTimeoutSeconds() {
+ var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS");
+ if (e == null || e.isEmpty()) {
+ return DEFAULT_TIMEOUT_SECONDS;
+ }
+ return Integer.parseInt(e);
+ }
+
private static final ScheduledExecutorService sTimeoutExecutor =
- Executors.newScheduledThreadPool(1);
+ Executors.newScheduledThreadPool(1, (Runnable r) -> {
+ Thread t = Executors.defaultThreadFactory().newThread(r);
+ t.setName("Ravenwood:TimeoutMonitor");
+ t.setDaemon(true);
+ return t;
+ });
- private static ScheduledFuture<?> sPendingTimeout;
+ private static volatile ScheduledFuture<?> sPendingTimeout;
/**
* When enabled, attempt to detect uncaught exceptions from background threads.
*/
private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
- "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+ !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+
+ private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true;
/**
* When set, an unhandled exception was discovered (typically on a background thread), and we
@@ -141,12 +173,6 @@
private static final AtomicReference<Throwable> sPendingUncaughtException =
new AtomicReference<>();
- private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
- (thread, throwable) -> {
- // Remember the first exception we discover
- sPendingUncaughtException.compareAndSet(null, throwable);
- };
-
// TODO: expose packCallingIdentity function in libbinder and use it directly
// See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
private static long packBinderIdentityToken(
@@ -187,6 +213,8 @@
* Initialize the global environment.
*/
public static void globalInitOnce() {
+ sTestThread = Thread.currentThread();
+ Thread.currentThread().setName(TESTS_THREAD_NAME);
synchronized (sInitializationLock) {
if (!sInitialized) {
// globalInitOnce() is called from class initializer, which cause
@@ -194,6 +222,7 @@
sInitialized = true;
// This is the first call.
+ final long start = System.currentTimeMillis();
try {
globalInitInner();
} catch (Throwable th) {
@@ -202,6 +231,9 @@
sExceptionFromGlobalInit = th;
SneakyThrow.sneakyThrow(th);
}
+ final long end = System.currentTimeMillis();
+ // TODO Show user/system time too
+ Log.e(TAG, "globalInit() took " + (end - start) + "ms");
} else {
// Subsequent calls. If the first call threw, just throw the same error, to prevent
// the test from running.
@@ -220,7 +252,8 @@
RavenwoodCommonUtils.log(TAG, "globalInitInner()");
if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+ Thread.setDefaultUncaughtExceptionHandler(
+ RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions);
}
// Some process-wide initialization:
@@ -304,6 +337,7 @@
ActivityManager.init$ravenwood(SYSTEM.getIdentifier());
final var main = new HandlerThread(MAIN_THREAD_NAME);
+ sMainThread = main;
main.start();
Looper.setMainLooperForTest(main.getLooper());
@@ -350,9 +384,20 @@
var systemServerContext =
new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);
- sInstrumentation = new Instrumentation();
- sInstrumentation.basicInit(instContext, targetContext, null);
- InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY);
+ var instArgs = Bundle.EMPTY;
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ try {
+ // TODO We should get the instrumentation class name from the build file or
+ // somewhere.
+ var InstClass = Class.forName("android.app.Instrumentation");
+ sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance();
+ sInstrumentation.basicInit(instContext, targetContext, null);
+ sInstrumentation.onCreate(instArgs);
+ } catch (Exception e) {
+ SneakyThrow.sneakyThrow(e);
+ }
+ });
+ InstrumentationRegistry.registerInstance(sInstrumentation, instArgs);
RavenwoodSystemServer.init(systemServerContext);
@@ -399,22 +444,46 @@
SystemProperties.clearChangeCallbacksForTest();
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout = sTimeoutExecutor.schedule(
- RavenwoodRuntimeEnvironmentController::dumpStacks,
- TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
- }
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(false);
- }
+ maybeThrowPendingUncaughtException();
}
/**
- * Partially reset and initialize before each test method invocation
+ * Called when a test method is about to be started.
*/
- public static void initForMethod() {
+ public static void enterTestMethod(Description description) {
// TODO(b/375272444): this is a hacky workaround to ensure binder identity
Binder.restoreCallingIdentity(sCallingIdentity);
+
+ scheduleTimeout();
+ }
+
+ /**
+ * Called when a test method finished.
+ */
+ public static void exitTestMethod(Description description) {
+ cancelTimeout();
+ maybeThrowPendingUncaughtException();
+ }
+
+ private static void scheduleTimeout() {
+ if (!ENABLE_TIMEOUT_STACKS) {
+ return;
+ }
+ cancelTimeout();
+
+ sPendingTimeout = sTimeoutExecutor.schedule(
+ RavenwoodRuntimeEnvironmentController::onTestTimedOut,
+ TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ private static void cancelTimeout() {
+ if (!ENABLE_TIMEOUT_STACKS) {
+ return;
+ }
+ var pt = sPendingTimeout;
+ if (pt != null) {
+ pt.cancel(false);
+ }
}
private static void initializeCompatIds() {
@@ -473,15 +542,36 @@
}
/**
+ * Return if an exception is benign and okay to continue running the main looper even
+ * if we detect it.
+ */
+ private static boolean isThrowableBenign(Throwable th) {
+ return th instanceof AssertionError || th instanceof AssumptionViolatedException;
+ }
+
+ static void dispatchMessage(Message msg) {
+ try {
+ msg.getTarget().dispatchMessage(msg);
+ } catch (Throwable th) {
+ var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
+ Thread.currentThread());
+ sStdErr.println(desc);
+ if (TOLERATE_LOOPER_ASSERTS && isThrowableBenign(th)) {
+ sStdErr.printf("*** Continuing the test because it's %s ***\n",
+ th.getClass().getSimpleName());
+ var e = new Exception(desc, th);
+ sPendingUncaughtException.compareAndSet(null, e);
+ return;
+ }
+ throw th;
+ }
+ }
+
+ /**
* A callback when a test class finishes its execution, mostly only for debugging.
*/
public static void exitTestClass() {
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout.cancel(false);
- }
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(true);
- }
+ maybeThrowPendingUncaughtException();
}
public static void logTestRunner(String label, Description description) {
@@ -491,35 +581,70 @@
+ "(" + description.getTestClass().getName() + ")");
}
- private static void dumpStacks() {
- final PrintStream out = System.err;
- out.println("-----BEGIN ALL THREAD STACKS-----");
- final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
- for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
- out.println();
- Thread t = stack.getKey();
- out.println(t.toString() + " ID=" + t.getId());
- for (StackTraceElement e : stack.getValue()) {
- out.println("\tat " + e);
- }
+ private static void maybeThrowPendingUncaughtException() {
+ final Throwable pending = sPendingUncaughtException.getAndSet(null);
+ if (pending != null) {
+ throw new IllegalStateException("Found an uncaught exception", pending);
}
- out.println("-----END ALL THREAD STACKS-----");
}
/**
- * If there's a pending uncaught exception, consume and throw it now. Typically used to
- * report an exception on a background thread as a failure for the currently running test.
+ * Prints the stack trace from all threads.
*/
- private static void maybeThrowPendingUncaughtException(boolean duringReset) {
- final Throwable pending = sPendingUncaughtException.getAndSet(null);
- if (pending != null) {
- if (duringReset) {
- throw new IllegalStateException(
- "Found an uncaught exception during this test", pending);
- } else {
- throw new IllegalStateException(
- "Found an uncaught exception before this test started", pending);
+ private static void onTestTimedOut() {
+ sStdErr.println("********* SLOW TEST DETECTED ********");
+ dumpStacks(null, null);
+ }
+
+ private static final Object sDumpStackLock = new Object();
+
+ /**
+ * Prints the stack trace from all threads.
+ */
+ private static void dumpStacks(
+ @Nullable Thread exceptionThread, @Nullable Throwable throwable) {
+ cancelTimeout();
+ synchronized (sDumpStackLock) {
+ final PrintStream out = sStdErr;
+ out.println("-----BEGIN ALL THREAD STACKS-----");
+
+ var stacks = Thread.getAllStackTraces();
+ var threads = stacks.keySet().stream().sorted(
+ Comparator.comparingLong(Thread::getId)).collect(Collectors.toList());
+
+ // Put the test and the main thread at the top.
+ var testThread = sTestThread;
+ var mainThread = sMainThread;
+ if (mainThread != null) {
+ threads.remove(mainThread);
+ threads.add(0, mainThread);
}
+ if (testThread != null) {
+ threads.remove(testThread);
+ threads.add(0, testThread);
+ }
+ // Put the exception thread at the top.
+ // Also inject the stacktrace from the exception.
+ if (exceptionThread != null) {
+ threads.remove(exceptionThread);
+ threads.add(0, exceptionThread);
+ stacks.put(exceptionThread, throwable.getStackTrace());
+ }
+ for (var th : threads) {
+ out.println();
+
+ out.print("Thread");
+ if (th == exceptionThread) {
+ out.print(" [** EXCEPTION THREAD **]");
+ }
+ out.print(": " + th.getName() + " / " + th);
+ out.println();
+
+ for (StackTraceElement e : stacks.get(th)) {
+ out.println("\tat " + e);
+ }
+ }
+ out.println("-----END ALL THREAD STACKS-----");
}
}
@@ -545,13 +670,17 @@
() -> Class.forName("org.mockito.Matchers"));
}
- // TODO: use the real UiAutomation class instead of a mock
- private static UiAutomation createMockUiAutomation() {
- sAdoptedPermissions = Collections.emptySet();
- var mock = mock(UiAutomation.class, inv -> {
+ static <T> T makeDefaultThrowMock(Class<T> clazz) {
+ return mock(clazz, inv -> {
HostTestUtils.onThrowMethodCalled();
return null;
});
+ }
+
+ // TODO: use the real UiAutomation class instead of a mock
+ private static UiAutomation createMockUiAutomation() {
+ sAdoptedPermissions = Collections.emptySet();
+ var mock = makeDefaultThrowMock(UiAutomation.class);
doAnswer(inv -> {
sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
return null;
@@ -586,6 +715,23 @@
}
}
+ private static void reportUncaughtExceptions(Thread th, Throwable e) {
+ sStdErr.printf("Uncaught exception detected: %s: %s\n",
+ th, RavenwoodCommonUtils.getStackTraceString(e));
+
+ doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION);
+ }
+
+ private static void doBugreport(
+ @Nullable Thread exceptionThread, @Nullable Throwable throwable,
+ boolean killSelf) {
+ // TODO: Print more information
+ dumpStacks(exceptionThread, throwable);
+ if (killSelf) {
+ System.exit(13);
+ }
+ }
+
private static void dumpJavaProperties() {
Log.v(TAG, "JVM properties:");
dumpMap(System.getProperties());
@@ -601,7 +747,6 @@
Log.v(TAG, " " + key + "=" + map.get(key));
}
}
-
private static void dumpOtherInfo() {
Log.v(TAG, "Other key information:");
var jloc = Locale.getDefault();
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 70c161c1..819d93a 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -26,6 +26,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -45,6 +46,9 @@
/** The default values. */
static final Map<String, String> sDefaultValues = new HashMap<>();
+ static final Set<String> sReadableKeys = new HashSet<>();
+ static final Set<String> sWritableKeys = new HashSet<>();
+
private static final String[] PARTITIONS = {
"bootimage",
"odm",
@@ -88,9 +92,24 @@
ravenwoodProps.forEach((key, origValue) -> {
final String value;
- // If a value starts with "$$$", then this is a reference to the device-side value.
if (origValue.startsWith("$$$")) {
+ // If a value starts with "$$$", then:
+ // - If it's "$$$r", the key is allowed to read.
+ // - If it's "$$$w", the key is allowed to write.
+ // - Otherwise, it's a reference to the device-side value.
+ // In case of $$$r and $$$w, if the key ends with a '.', then it'll be treaded
+ // as a prefix match.
var deviceKey = origValue.substring(3);
+ if ("r".equals(deviceKey)) {
+ sReadableKeys.add(key);
+ Log.v(TAG, key + " (readable)");
+ return;
+ } else if ("w".equals(deviceKey)) {
+ sWritableKeys.add(key);
+ Log.v(TAG, key + " (writable)");
+ return;
+ }
+
var deviceValue = deviceProps.get(deviceKey);
if (deviceValue == null) {
throw new RuntimeException("Failed to initialize system properties. Key '"
@@ -131,50 +150,38 @@
sDefaultValues.forEach(RavenwoodRuntimeNative::setSystemProperty);
}
+ private static boolean checkAllowedInner(String key, Set<String> allowed) {
+ if (allowed.contains(key)) {
+ return true;
+ }
+
+ // Also search for a prefix match.
+ for (var k : allowed) {
+ if (k.endsWith(".") && key.startsWith(k)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean checkAllowed(String key, Set<String> allowed) {
+ return checkAllowedInner(key, allowed) || checkAllowedInner(getKeyRoot(key), allowed);
+ }
+
private static boolean isKeyReadable(String key) {
- // All writable keys are also readable
- if (isKeyWritable(key)) return true;
-
- final String root = getKeyRoot(key);
-
- // This set is carefully curated to help identify situations where a test may
- // accidentally depend on a default value of an obscure property whose owner hasn't
- // decided how Ravenwood should behave.
- if (root.startsWith("boot.")) return true;
- if (root.startsWith("build.")) return true;
- if (root.startsWith("product.")) return true;
- if (root.startsWith("soc.")) return true;
- if (root.startsWith("system.")) return true;
-
// All core values should be readable
- if (sDefaultValues.containsKey(key)) return true;
-
- // Hardcoded allowlist
- return switch (key) {
- case "gsm.version.baseband",
- "no.such.thing",
- "qemu.sf.lcd_density",
- "ro.bootloader",
- "ro.hardware",
- "ro.hw_timeout_multiplier",
- "ro.odm.build.media_performance_class",
- "ro.sf.lcd_density",
- "ro.treble.enabled",
- "ro.vndk.version",
- "ro.icu.data.path" -> true;
- default -> false;
- };
+ if (sDefaultValues.containsKey(key)) {
+ return true;
+ }
+ if (checkAllowed(key, sReadableKeys)) {
+ return true;
+ }
+ // All writable keys are also readable
+ return isKeyWritable(key);
}
private static boolean isKeyWritable(String key) {
- final String root = getKeyRoot(key);
-
- if (root.startsWith("debug.")) return true;
-
- // For PropertyInvalidatedCache
- if (root.startsWith("cache_key.")) return true;
-
- return false;
+ return checkAllowed(key, sWritableKeys);
}
static boolean isKeyAccessible(String key, boolean write) {
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 19c1bff..3e2c405 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -15,7 +15,20 @@
*/
package android.platform.test.ravenwood;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod.reflectMethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+
import com.android.ravenwood.common.RavenwoodCommonUtils;
+import com.android.ravenwood.common.SneakyThrow;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
/**
* Utilities for writing (bivalent) ravenwood tests.
@@ -47,4 +60,129 @@
public static void loadJniLibrary(String libname) {
RavenwoodCommonUtils.loadJniLibrary(libname);
}
+
+ private class MainHandlerHolder {
+ static Handler sMainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Returns the main thread handler.
+ */
+ public static Handler getMainHandler() {
+ return MainHandlerHolder.sMainHandler;
+ }
+
+ /**
+ * Run a Callable on Handler and wait for it to complete.
+ */
+ @Nullable
+ public static <T> T runOnHandlerSync(@NonNull Handler h, @NonNull Callable<T> c) {
+ var result = new AtomicReference<T>();
+ var thrown = new AtomicReference<Throwable>();
+ var latch = new CountDownLatch(1);
+ h.post(() -> {
+ try {
+ result.set(c.call());
+ } catch (Throwable th) {
+ thrown.set(th);
+ }
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting on the Runnable", e);
+ }
+ var th = thrown.get();
+ if (th != null) {
+ SneakyThrow.sneakyThrow(th);
+ }
+ return result.get();
+ }
+
+
+ /**
+ * Run a Runnable on Handler and wait for it to complete.
+ */
+ @Nullable
+ public static void runOnHandlerSync(@NonNull Handler h, @NonNull Runnable r) {
+ runOnHandlerSync(h, () -> {
+ r.run();
+ return null;
+ });
+ }
+
+ /**
+ * Run a Callable on main thread and wait for it to complete.
+ */
+ @Nullable
+ public static <T> T runOnMainThreadSync(@NonNull Callable<T> c) {
+ return runOnHandlerSync(getMainHandler(), c);
+ }
+
+ /**
+ * Run a Runnable on main thread and wait for it to complete.
+ */
+ @Nullable
+ public static void runOnMainThreadSync(@NonNull Runnable r) {
+ runOnHandlerSync(getMainHandler(), r);
+ }
+
+ public static class MockitoHelper {
+ private MockitoHelper() {
+ }
+
+ /**
+ * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
+ * method on. (Maybe we should do it in Ravenizer.)
+ */
+ public static void verifyZeroInteractions(Object... mocks) {
+ if (RavenwoodRule.isOnRavenwood()) {
+ // Mockito 4 or later
+ reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
+ .callStatic(new Object[]{mocks});
+ } else {
+ // Mockito 2
+ reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
+ .callStatic(new Object[]{mocks});
+ }
+ }
+ }
+
+
+ /**
+ * Wrap the given {@link Supplier} to become memoized.
+ *
+ * The underlying {@link Supplier} will only be invoked once, and that result will be cached
+ * and returned for any future requests.
+ */
+ static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) {
+ return new Supplier<>() {
+ private T mInstance;
+
+ @Override
+ public T get() {
+ synchronized (this) {
+ if (mInstance == null) {
+ mInstance = create();
+ }
+ return mInstance;
+ }
+ }
+
+ private T create() {
+ try {
+ return supplier.get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ }
+
+ /** Used by {@link #memoize(ThrowingSupplier)} */
+ public interface ThrowingSupplier<T> {
+ /** */
+ T get() throws Exception;
+ }
}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index a967a3f..893b354 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -26,10 +26,12 @@
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
+import java.util.Objects;
import java.util.function.Supplier;
public class RavenwoodCommonUtils {
@@ -329,4 +331,70 @@
public static <T> T withDefault(@Nullable T value, @Nullable T def) {
return value != null ? value : def;
}
+
+ /**
+ * Utility for calling a method with reflections. Used to call a method by name.
+ * Note, this intentionally does _not_ support non-public methods, as we generally
+ * shouldn't violate java visibility in ravenwood.
+ *
+ * @param <TTHIS> class owning the method.
+ */
+ public static class ReflectedMethod<TTHIS> {
+ private final Class<TTHIS> mThisClass;
+ private final Method mMethod;
+
+ private ReflectedMethod(Class<TTHIS> thisClass, Method method) {
+ mThisClass = thisClass;
+ mMethod = method;
+ }
+
+ /** Factory method. */
+ @SuppressWarnings("unchecked")
+ public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+ @NonNull Class<TTHIS> clazz, @NonNull String methodName,
+ @NonNull Class<?>... argTypes) {
+ try {
+ return new ReflectedMethod(clazz, clazz.getMethod(methodName, argTypes));
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Factory method. */
+ @SuppressWarnings("unchecked")
+ public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+ @NonNull String className, @NonNull String methodName,
+ @NonNull Class<?>... argTypes) {
+ try {
+ return reflectMethod((Class<TTHIS>) Class.forName(className), methodName, argTypes);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Call the instance method */
+ @SuppressWarnings("unchecked")
+ public <RET> RET call(@NonNull TTHIS thisObject, @NonNull Object... args) {
+ try {
+ return (RET) mMethod.invoke(Objects.requireNonNull(thisObject), args);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Call the static method */
+ @SuppressWarnings("unchecked")
+ public <RET> RET callStatic(@NonNull Object... args) {
+ try {
+ return (RET) mMethod.invoke(null, args);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /** Handy method to create an array */
+ public static <T> T[] arr(@NonNull T... objects) {
+ return objects;
+ }
}
diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
index 7ab9cda..855a4ff 100644
--- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
@@ -21,7 +21,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.RuntimeInit;
import com.android.ravenwood.RavenwoodRuntimeNative;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
@@ -164,7 +163,7 @@
* Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
* that we don't end up in a recursive loop.
*/
- private static PrintStream getRealOut() {
+ public static PrintStream getRealOut() {
if (RuntimeInit.sOut$ravenwood != null) {
return RuntimeInit.sOut$ravenwood;
} else {
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index eaadac6..50cfd3b 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -57,4 +57,12 @@
public int getTargetSdkVersion() {
return RavenwoodRuntimeState.sTargetSdkLevel;
}
+
+ /** Ignored on ravenwood. */
+ public void registerNativeAllocation(long bytes) {
+ }
+
+ /** Ignored on ravenwood. */
+ public void registerNativeFree(long bytes) {
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index cf1a513..985e00e 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -97,6 +97,9 @@
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
+ if (mFreeFunction == 0) {
+ return () -> {}; // do nothing
+ }
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}
diff --git a/ravenwood/scripts/add-annotations.sh b/ravenwood/scripts/add-annotations.sh
index 3e86037..8c394f5 100755
--- a/ravenwood/scripts/add-annotations.sh
+++ b/ravenwood/scripts/add-annotations.sh
@@ -35,7 +35,7 @@
# We add this line to each methods found.
# Note, if we used a single @, that'd be handled as an at file. Use
# the double-at instead.
-annotation="@@android.platform.test.annotations.DisabledOnRavenwood"
+annotation="@@android.platform.test.annotations.DisabledOnRavenwood(reason = \"bulk-disabled by script\")"
while getopts "t:" opt; do
case "$opt" in
t)
diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
index 9dd7cc6..182a7cf 100644
--- a/ravenwood/tests/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -33,3 +33,34 @@
},
auto_gen_config: true,
}
+
+// Same as RavenwoodCoreTest, but it excludes tests using platform-parametric-runner-lib,
+// because that modules has too many dependencies and slow to build incrementally.
+android_ravenwood_test {
+ name: "RavenwoodCoreTest-light",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ // This library should be removed by Ravenizer
+ "mockito-target-minus-junit4",
+ ],
+ libs: [
+ // We access internal private classes
+ "ravenwood-junit-impl",
+ ],
+ srcs: [
+ "test/**/*.java",
+ "test/**/*.kt",
+ ],
+
+ exclude_srcs: [
+ "test/com/android/ravenwoodtest/runnercallbacktests/*",
+ ],
+ ravenizer: {
+ strip_mockito: true,
+ },
+ auto_gen_config: true,
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
new file mode 100644
index 0000000..68387d7
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.ravenwood.RavenwoodUtils;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public class RavenwoodMainThreadTest {
+ private static final boolean RUN_UNSAFE_TESTS =
+ "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS"));
+
+ @Test
+ public void testRunOnMainThread() {
+ AtomicReference<Thread> thr = new AtomicReference<>();
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ thr.set(Thread.currentThread());
+ });
+ var th = thr.get();
+ assertThat(th).isNotNull();
+ assertThat(th).isNotEqualTo(Thread.currentThread());
+ }
+
+ /**
+ * Sleep a long time on the main thread. This test would then "pass", but Ravenwood
+ * should show the stack traces.
+ *
+ * This is "unsafe" because this test is slow.
+ */
+ @Test
+ public void testUnsafeMainThreadHang() {
+ assumeTrue(RUN_UNSAFE_TESTS);
+
+ // The test should time out.
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ try {
+ Thread.sleep(30_000);
+ } catch (InterruptedException e) {
+ fail("Interrupted");
+ }
+ });
+ }
+
+ /**
+ * AssertionError on the main thread would be swallowed and reported "normally".
+ * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills
+ * the process)
+ *
+ * This is "unsafe" only because this feature can be disabled via the env var.
+ */
+ @Test
+ public void testUnsafeAssertFailureOnMainThread() {
+ assumeTrue(RUN_UNSAFE_TESTS);
+
+ assertThrows(AssertionError.class, () -> {
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ fail();
+ });
+ });
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
new file mode 100644
index 0000000..421fb50
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link ReflectedMethod}.
+ */
+public class RavenwoodReflectorTest {
+ /** test target */
+ public class Target {
+ private final int mVar;
+
+ /** test target */
+ public Target(int var) {
+ mVar = var;
+ }
+
+ /** test target */
+ public int foo(int x) {
+ return x + mVar;
+ }
+
+ /** test target */
+ public static int bar(int x) {
+ return x + 1;
+ }
+ }
+
+ /** Test for a non-static method call */
+ @Test
+ public void testNonStatic() {
+ var obj = new Target(5);
+
+ var m = ReflectedMethod.reflectMethod(Target.class, "foo", int.class);
+ assertThat((int) m.call(obj, 2)).isEqualTo(7);
+ }
+
+ /** Test for a static method call */
+ @Test
+ public void testStatic() {
+ var m = ReflectedMethod.reflectMethod(Target.class, "bar", int.class);
+ assertThat((int) m.callStatic(1)).isEqualTo(2);
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
new file mode 100644
index 0000000..454f5a9
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.os.SystemProperties;
+
+import org.junit.Test;
+
+public class RavenwoodSystemPropertiesTest {
+ @Test
+ public void testRead() {
+ assertThat(SystemProperties.get("ro.board.first_api_level")).isEqualTo("1");
+ }
+
+ @Test
+ public void testWrite() {
+ SystemProperties.set("debug.xxx", "5");
+ assertThat(SystemProperties.get("debug.xxx")).isEqualTo("5");
+ }
+
+ private static void assertException(String expectedMessage, Runnable r) {
+ try {
+ r.run();
+ fail("Excepted exception with message '" + expectedMessage + "' but wasn't thrown");
+ } catch (RuntimeException e) {
+ if (e.getMessage().contains(expectedMessage)) {
+ return;
+ }
+ fail("Excepted exception with message '" + expectedMessage + "' but was '"
+ + e.getMessage() + "'");
+ }
+ }
+
+
+ @Test
+ public void testReadDisallowed() {
+ assertException("Read access to system property 'nonexisitent' denied", () -> {
+ SystemProperties.get("nonexisitent");
+ });
+ }
+
+ @Test
+ public void testWriteDisallowed() {
+ assertException("failed to set system property \"ro.board.first_api_level\" ", () -> {
+ SystemProperties.set("ro.board.first_api_level", "2");
+ });
+ }
+}
diff --git a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index 30abaa2..b1a40f0 100644
--- a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
+++ b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -16,28 +16,27 @@
package com.android.ravenwoodtest;
import android.platform.test.annotations.IgnoreUnderRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
-import org.junit.Rule;
+import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class RavenwoodMinimumTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
- .setProcessApp()
- .build();
-
@Test
public void testSimple() {
Assert.assertTrue(android.os.Process.isApplicationUid(android.os.Process.myUid()));
}
@Test
+ public void testAssumeNot() {
+ Assume.assumeFalse(android.os.Process.isApplicationUid(android.os.Process.myUid()));
+ }
+
+ @Test
@IgnoreUnderRavenwood
public void testIgnored() {
throw new RuntimeException("Shouldn't be executed under ravenwood");
diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop
index 37c50f1..512b459 100644
--- a/ravenwood/texts/ravenwood-build.prop
+++ b/ravenwood/texts/ravenwood-build.prop
@@ -8,9 +8,41 @@
ro.soc.model=Ravenwood
ro.debuggable=1
-# For the graphics stack
-ro.hwui.max_texture_allocation_size=104857600
persist.sys.locale=en-US
+ro.product.locale=en-US
+
+ro.hwui.max_texture_allocation_size=104857600
+
+# Allowlist control:
+# This set is carefully curated to help identify situations where a test may
+# accidentally depend on a default value of an obscure property whose owner hasn't
+# decided how Ravenwood should behave.
+
+boot.=$$$r
+build.=$$$r
+product.=$$$r
+soc.=$$$r
+system.=$$$r
+wm.debug.=$$$r
+wm.extensions.=$$$r
+
+gsm.version.baseband=$$$r
+no.such.thing=$$$r
+qemu.sf.lcd_density=$$$r
+ro.bootloader=$$$r
+ro.hardware=$$$r
+ro.hw_timeout_multiplier=$$$r
+ro.odm.build.media_performance_class=$$$r
+ro.sf.lcd_density=$$$r
+ro.treble.enabled=$$$r
+ro.vndk.version=$$$r
+ro.icu.data.path=$$$r
+
+# Writable keys
+debug.=$$$w
+
+# For PropertyInvalidatedCache
+cache_key.=$$$w
# The ones starting with "ro.product" or "ro.build" will be copied to all "partitions" too.
# See RavenwoodSystemProperties.
diff --git a/ravenwood/texts/ravenwood-services-jarjar-rules.txt b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
index 8fdd340..64a0e25 100644
--- a/ravenwood/texts/ravenwood-services-jarjar-rules.txt
+++ b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
@@ -5,7 +5,7 @@
# Rename all other service internals so that tests can continue to statically
# link services code when owners aren't ready to support on Ravenwood
-rule com.android.server.** repackaged.@0
+rule com.android.server.** repackaged.services.@0
# TODO: support AIDL generated Parcelables via hoststubgen
-rule android.hardware.power.stats.** repackaged.@0
+rule android.hardware.power.stats.** repackaged.services.@0
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index bd34f33..c182c26 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -149,7 +149,6 @@
OperationStorage mOperationStorage;
List<PackageInfo> mPackages;
- PackageInfo mCurrentPackage;
boolean mUpdateSchedule;
CountDownLatch mLatch;
FullBackupJob mJob; // if a scheduled job needs to be finished afterwards
@@ -207,10 +206,9 @@
for (String pkg : whichPackages) {
try {
PackageManager pm = backupManagerService.getPackageManager();
- PackageInfo info = pm.getPackageInfoAsUser(pkg,
+ PackageInfo packageInfo = pm.getPackageInfoAsUser(pkg,
PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
- mCurrentPackage = info;
- if (!mBackupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) {
+ if (!mBackupEligibilityRules.appIsEligibleForBackup(packageInfo.applicationInfo)) {
// Cull any packages that have indicated that backups are not permitted,
// that run as system-domain uids but do not define their own backup agents,
// as well as any explicit mention of the 'special' shared-storage agent
@@ -220,13 +218,13 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
- } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) {
+ } else if (!mBackupEligibilityRules.appGetsFullBackup(packageInfo)) {
// Cull any packages that are found in the queue but now aren't supposed
// to get full-data backup operations.
if (DEBUG) {
@@ -235,13 +233,13 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
- } else if (mBackupEligibilityRules.appIsStopped(info.applicationInfo)) {
+ } else if (mBackupEligibilityRules.appIsStopped(packageInfo.applicationInfo)) {
// Cull any packages in the 'stopped' state: they've either just been
// installed or have explicitly been force-stopped by the user. In both
// cases we do not want to launch them for backup.
@@ -250,21 +248,21 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
}
- mPackages.add(info);
+ mPackages.add(packageInfo);
} catch (NameNotFoundException e) {
Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
- mCurrentPackage,
+ /* pkg= */ null,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
}
}
@@ -352,10 +350,11 @@
} else {
monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
}
- mBackupManagerMonitorEventSender
- .monitorEvent(monitoringEvent, null,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ mBackupManagerMonitorEventSender.monitorEvent(
+ monitoringEvent,
+ /* pkg= */ null,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+ /* extras= */ null);
mUpdateSchedule = false;
backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
return;
@@ -367,8 +366,9 @@
backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
+ /* pkg= */ null,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+ /* extras= */ null);
return;
}
@@ -461,9 +461,10 @@
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
- mCurrentPackage,
+ currentPackage,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+ BackupManagerMonitorEventSender.putMonitoringExtra(
+ /* extras= */ null,
BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
preflightResult));
backupPackageStatus = (int) preflightResult;
@@ -496,9 +497,9 @@
+ ": " + totalRead + " of " + quota);
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
- mCurrentPackage,
+ currentPackage,
BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
+ /* extras= */ null);
mBackupRunner.sendQuotaExceeded(totalRead, quota);
}
}
@@ -645,9 +646,9 @@
Slog.w(TAG, "Exception trying full transport backup", e);
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
- mCurrentPackage,
+ /* pkg= */ null,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+ BackupManagerMonitorEventSender.putMonitoringExtra(/* extras= */ null,
BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
Log.getStackTraceString(e)));
@@ -966,9 +967,6 @@
}
}
-
- // BackupRestoreTask interface: specifically, timeout detection
-
@Override
public void execute() { /* intentionally empty */ }
@@ -981,7 +979,9 @@
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+ mTarget,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+ /* extras= */ null);
mIsCancelled = true;
// Cancel tasks spun off by this task.
mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
index c4519b1..33668a6 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
@@ -71,6 +71,7 @@
mMonitor = monitor;
}
+ @Nullable
public IBackupManagerMonitor getMonitor() {
return mMonitor;
}
@@ -87,9 +88,9 @@
*/
public void monitorEvent(
int id,
- PackageInfo pkg,
+ @Nullable PackageInfo pkg,
int category,
- Bundle extras) {
+ @Nullable Bundle extras) {
try {
Bundle bundle = new Bundle();
bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b6fe0ad..e46bbe2 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -160,6 +160,7 @@
import com.android.server.pm.Installer;
import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
+import com.android.server.storage.ImmutableVolumeInfo;
import com.android.server.storage.StorageSessionController;
import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
import com.android.server.storage.WatchedVolumeInfo;
@@ -777,7 +778,7 @@
break;
}
case H_VOLUME_UNMOUNT: {
- final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
+ final ImmutableVolumeInfo vol = (ImmutableVolumeInfo) msg.obj;
unmount(vol);
break;
}
@@ -898,8 +899,14 @@
for (int i = 0; i < size; i++) {
final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (vol.getMountUserId() == userId) {
+ // Capture the volume before we set mount user id to null,
+ // so that StorageSessionController remove the session from
+ // the correct user (old mount user id)
+ final ImmutableVolumeInfo volToUnmount
+ = vol.getClonedImmutableVolumeInfo();
vol.setMountUserId(UserHandle.USER_NULL);
- mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
+ mHandler.obtainMessage(H_VOLUME_UNMOUNT, volToUnmount)
+ .sendToTarget();
}
}
}
@@ -1295,7 +1302,12 @@
}
private void maybeRemountVolumes(int userId) {
- List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>();
+ // We need to keep 2 lists
+ // 1. List of volumes before we set the mount user Id so that
+ // StorageSessionController is able to remove the session from the correct user (old one)
+ // 2. List of volumes to mount which should have the up to date info
+ List<ImmutableVolumeInfo> volumesToUnmount = new ArrayList<>();
+ List<WatchedVolumeInfo> volumesToMount = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
final WatchedVolumeInfo vol = mVolumes.valueAt(i);
@@ -1303,16 +1315,19 @@
&& vol.getMountUserId() != mCurrentUserId) {
// If there's a visible secondary volume mounted,
// we need to update the currentUserId and remount
+ // But capture the volume with the old user id first to use it in unmounting
+ volumesToUnmount.add(vol.getClonedImmutableVolumeInfo());
vol.setMountUserId(mCurrentUserId);
- volumesToRemount.add(vol);
+ volumesToMount.add(vol);
}
}
}
- for (WatchedVolumeInfo vol : volumesToRemount) {
- Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol);
- mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
- mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+ for (int i = 0; i < volumesToMount.size(); i++) {
+ Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: "
+ + volumesToUnmount.get(i));
+ mHandler.obtainMessage(H_VOLUME_UNMOUNT, volumesToUnmount.get(i)).sendToTarget();
+ mHandler.obtainMessage(H_VOLUME_MOUNT, volumesToMount.get(i)).sendToTarget();
}
}
@@ -2430,10 +2445,10 @@
super.unmount_enforcePermission();
final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
- unmount(vol);
+ unmount(vol.getClonedImmutableVolumeInfo());
}
- private void unmount(WatchedVolumeInfo vol) {
+ private void unmount(ImmutableVolumeInfo vol) {
try {
try {
if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
@@ -2444,7 +2459,7 @@
}
extendWatchdogTimeout("#unmount might be slow");
mVold.unmount(vol.getId());
- mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo());
+ mStorageSessionController.onVolumeUnmount(vol);
} catch (Exception e) {
Slog.wtf(TAG, e);
}
diff --git a/services/core/java/com/android/server/SystemTimeZone.java b/services/core/java/com/android/server/SystemTimeZone.java
index dd07081..c8810f6 100644
--- a/services/core/java/com/android/server/SystemTimeZone.java
+++ b/services/core/java/com/android/server/SystemTimeZone.java
@@ -133,6 +133,7 @@
boolean timeZoneChanged = false;
synchronized (SystemTimeZone.class) {
String currentTimeZoneId = getTimeZoneId();
+ @TimeZoneConfidence int currentConfidence = getTimeZoneConfidence();
if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) {
SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId);
if (DEBUG) {
@@ -145,6 +146,8 @@
String logMsg = "Time zone or confidence set: "
+ " (new) timeZoneId=" + timeZoneId
+ ", (new) confidence=" + confidence
+ + ", (old) timeZoneId=" + currentTimeZoneId
+ + ", (old) confidence=" + currentConfidence
+ ", logInfo=" + logInfo;
addDebugLogEntry(logMsg);
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd7a0ac..b75b7ddf 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2816,13 +2816,11 @@
if (!checkNotifyPermission("notifyEmergencyNumberList()")) {
return;
}
- if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- if (!mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TELEPHONY_CALLING)) {
- // TelephonyManager.getEmergencyNumberList() throws an exception if
- // FEATURE_TELEPHONY_CALLING is not defined.
- return;
- }
+ if (!mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_CALLING)) {
+ // TelephonyManager.getEmergencyNumberList() throws an exception if
+ // FEATURE_TELEPHONY_CALLING is not defined.
+ return;
}
synchronized (mRecords) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8b701f0..b0b34d0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19471,7 +19471,7 @@
/**
* @hide
*/
- @EnforcePermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+ @EnforcePermission(INTERACT_ACROSS_USERS_FULL)
public IBinder refreshIntentCreatorToken(Intent intent) {
refreshIntentCreatorToken_enforcePermission();
IBinder binder = intent.getCreatorToken();
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index 36035bd..78beb18 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -832,7 +832,9 @@
// If this receiver is going to be skipped, skip it now itself and don't even enqueue
// it.
- final String skipReason = mSkipPolicy.shouldSkipMessage(r, receiver);
+ final String skipReason = Flags.avoidNoteOpAtEnqueue()
+ ? mSkipPolicy.shouldSkipAtEnqueueMessage(r, receiver)
+ : mSkipPolicy.shouldSkipMessage(r, receiver);
if (skipReason != null) {
setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED,
"skipped by policy at enqueue: " + skipReason);
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index d2af84c..b0d5994 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -71,10 +71,20 @@
* {@code null} if it can proceed.
*/
public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) {
+ return shouldSkipMessage(r, target, false /* preflight */);
+ }
+
+ public @Nullable String shouldSkipAtEnqueueMessage(@NonNull BroadcastRecord r,
+ @NonNull Object target) {
+ return shouldSkipMessage(r, target, true /* preflight */);
+ }
+
+ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target,
+ boolean preflight) {
if (target instanceof BroadcastFilter) {
- return shouldSkipMessage(r, (BroadcastFilter) target);
+ return shouldSkipMessage(r, (BroadcastFilter) target, preflight);
} else {
- return shouldSkipMessage(r, (ResolveInfo) target);
+ return shouldSkipMessage(r, (ResolveInfo) target, preflight);
}
}
@@ -86,7 +96,7 @@
* {@code null} if it can proceed.
*/
private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
- @NonNull ResolveInfo info) {
+ @NonNull ResolveInfo info, boolean preflight) {
final BroadcastOptions brOptions = r.options;
final ComponentName component = new ComponentName(
info.activityInfo.applicationInfo.packageName,
@@ -134,15 +144,23 @@
+ " requires " + info.activityInfo.permission;
}
} else if (info.activityInfo.permission != null) {
- final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission);
- if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode,
- r.callingUid, r.callerPackage, r.callerFeatureId,
- "Broadcast delivered to " + info.activityInfo.name)
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: broadcasting "
- + broadcastDescription(r, component)
- + " requires appop " + AppOpsManager.permissionToOp(
- info.activityInfo.permission);
+ final String op = AppOpsManager.permissionToOp(info.activityInfo.permission);
+ if (op != null) {
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId,
+ "Broadcast delivered to " + info.activityInfo.name);
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: broadcasting "
+ + broadcastDescription(r, component)
+ + " requires appop " + AppOpsManager.permissionToOp(
+ info.activityInfo.permission);
+ }
}
}
@@ -250,8 +268,8 @@
perm = PackageManager.PERMISSION_DENIED;
}
- int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
- if (appOp != AppOpsManager.OP_NONE) {
+ final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+ if (appOp != null) {
// When there is an app op associated with the permission,
// skip when both the permission and the app op are
// granted.
@@ -259,7 +277,7 @@
mService.getAppOpsManager().checkOpNoThrow(appOp,
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName)
- == AppOpsManager.MODE_ALLOWED)) {
+ == AppOpsManager.MODE_ALLOWED)) {
return "Skipping delivery to " + info.activityInfo.packageName
+ " due to excluded permission " + excludedPermission;
}
@@ -292,9 +310,10 @@
createAttributionSourcesForResolveInfo(info);
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
- perm = hasPermissionForDataDelivery(
+ perm = hasPermission(
requiredPermission,
"Broadcast delivered to " + info.activityInfo.name,
+ preflight,
attributionSources)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
@@ -308,10 +327,14 @@
}
}
}
- if (r.appOp != AppOpsManager.OP_NONE) {
- if (!noteOpForManifestReceiver(r.appOp, r, info, component)) {
+ if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+ final String op = AppOpsManager.opToPublicName(r.appOp);
+ final boolean appOpAllowed = preflight
+ ? checkOpForManifestReceiver(r.appOp, op, r, info, component)
+ : noteOpForManifestReceiver(r.appOp, op, r, info, component);
+ if (!appOpAllowed) {
return "Skipping delivery to " + info.activityInfo.packageName
- + " due to required appop " + r.appOp;
+ + " due to required appop " + AppOpsManager.opToName(r.appOp);
}
}
@@ -338,7 +361,7 @@
* {@code null} if it can proceed.
*/
private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
- @NonNull BroadcastFilter filter) {
+ @NonNull BroadcastFilter filter, boolean preflight) {
if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) {
return "Compat change filtered: broadcasting " + r.intent.toString()
+ " to uid " + filter.owningUid + " due to compat change "
@@ -372,18 +395,25 @@
+ " requires " + filter.requiredPermission
+ " due to registered receiver " + filter;
} else {
- final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
- if (opCode != AppOpsManager.OP_NONE
- && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
- r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: broadcasting "
- + r.intent.toString()
- + " from " + r.callerPackage + " (pid="
- + r.callingPid + ", uid=" + r.callingUid + ")"
- + " requires appop " + AppOpsManager.permissionToOp(
- filter.requiredPermission)
- + " due to registered receiver " + filter;
+ final String op = AppOpsManager.permissionToOp(filter.requiredPermission);
+ if (op != null) {
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op, r.callingUid,
+ r.callerPackage, r.callerFeatureId,
+ "Broadcast sent to protected receiver");
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: broadcasting "
+ + r.intent
+ + " from " + r.callerPackage + " (pid="
+ + r.callingPid + ", uid=" + r.callingUid + ")"
+ + " requires appop " + op
+ + " due to registered receiver " + filter;
+ }
}
}
}
@@ -433,9 +463,10 @@
.build();
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
- final int perm = hasPermissionForDataDelivery(
+ final int perm = hasPermission(
requiredPermission,
"Broadcast delivered to registered receiver " + filter.receiverId,
+ preflight,
attributionSource)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
@@ -471,8 +502,8 @@
final int perm = checkComponentPermission(excludedPermission,
filter.receiverList.pid, filter.receiverList.uid, -1, true);
- int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
- if (appOp != AppOpsManager.OP_NONE) {
+ final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+ if (appOp != null) {
// When there is an app op associated with the permission,
// skip when both the permission and the app op are
// granted.
@@ -480,14 +511,13 @@
mService.getAppOpsManager().checkOpNoThrow(appOp,
filter.receiverList.uid,
filter.packageName)
- == AppOpsManager.MODE_ALLOWED)) {
+ == AppOpsManager.MODE_ALLOWED)) {
return "Appop Denial: receiving "
- + r.intent.toString()
+ + r.intent
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
- + " excludes appop " + AppOpsManager.permissionToOp(
- excludedPermission)
+ + " excludes appop " + appOp
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")";
}
@@ -496,7 +526,7 @@
// skip when permission is granted.
if (perm == PackageManager.PERMISSION_GRANTED) {
return "Permission Denial: receiving "
- + r.intent.toString()
+ + r.intent
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
@@ -523,19 +553,27 @@
}
// If the broadcast also requires an app op check that as well.
- if (r.appOp != AppOpsManager.OP_NONE
- && mService.getAppOpsManager().noteOpNoThrow(r.appOp,
- filter.receiverList.uid, filter.packageName, filter.featureId,
- "Broadcast delivered to registered receiver " + filter.receiverId)
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: receiving "
- + r.intent.toString()
- + " to " + filter.receiverList.app
- + " (pid=" + filter.receiverList.pid
- + ", uid=" + filter.receiverList.uid + ")"
- + " requires appop " + AppOpsManager.opToName(r.appOp)
- + " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")";
+ if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+ final String op = AppOpsManager.opToPublicName(r.appOp);
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ filter.receiverList.uid, filter.packageName, filter.featureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op,
+ filter.receiverList.uid, filter.packageName, filter.featureId,
+ "Broadcast delivered to registered receiver " + filter.receiverId);
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: receiving "
+ + r.intent
+ + " to " + filter.receiverList.app
+ + " (pid=" + filter.receiverList.pid
+ + ", uid=" + filter.receiverList.uid + ")"
+ + " requires appop " + AppOpsManager.opToName(r.appOp)
+ + " due to sender " + r.callerPackage
+ + " (uid " + r.callingUid + ")";
+ }
}
// Ensure that broadcasts are only sent to other apps if they are explicitly marked as
@@ -572,14 +610,14 @@
+ ", uid=" + r.callingUid + ") to " + component.flattenToShortString();
}
- private boolean noteOpForManifestReceiver(int appOp, BroadcastRecord r, ResolveInfo info,
- ComponentName component) {
+ private boolean noteOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component) {
if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
- return noteOpForManifestReceiverInner(appOp, r, info, component, null);
+ return noteOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
} else {
// Attribution tags provided, noteOp each tag
for (String tag : info.activityInfo.attributionTags) {
- if (!noteOpForManifestReceiverInner(appOp, r, info, component, tag)) {
+ if (!noteOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
return false;
}
}
@@ -587,8 +625,8 @@
}
}
- private boolean noteOpForManifestReceiverInner(int appOp, BroadcastRecord r, ResolveInfo info,
- ComponentName component, String tag) {
+ private boolean noteOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component, String tag) {
if (mService.getAppOpsManager().noteOpNoThrow(appOp,
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName,
@@ -598,7 +636,37 @@
Slog.w(TAG, "Appop Denial: receiving "
+ r.intent + " to "
+ component.flattenToShortString()
- + " requires appop " + AppOpsManager.opToName(appOp)
+ + " requires appop " + AppOpsManager.opToName(opCode)
+ + " due to sender " + r.callerPackage
+ + " (uid " + r.callingUid + ")");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component) {
+ if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
+ return checkOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
+ } else {
+ // Attribution tags provided, noteOp each tag
+ for (String tag : info.activityInfo.attributionTags) {
+ if (!checkOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private boolean checkOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component, String tag) {
+ if (mService.getAppOpsManager().checkOpNoThrow(appOp, info.activityInfo.applicationInfo.uid,
+ info.activityInfo.packageName, tag) != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(TAG, "Appop Denial: receiving "
+ + r.intent + " to "
+ + component.flattenToShortString()
+ + " requires appop " + AppOpsManager.opToName(opCode)
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")");
return false;
@@ -694,9 +762,10 @@
return mPermissionManager;
}
- private boolean hasPermissionForDataDelivery(
+ private boolean hasPermission(
@NonNull String permission,
@NonNull String message,
+ boolean preflight,
@NonNull AttributionSource... attributionSources) {
final PermissionManager permissionManager = getPermissionManager();
if (permissionManager == null) {
@@ -704,9 +773,14 @@
}
for (AttributionSource attributionSource : attributionSources) {
- final int permissionCheckResult =
- permissionManager.checkPermissionForDataDelivery(
- permission, attributionSource, message);
+ final int permissionCheckResult;
+ if (preflight) {
+ permissionCheckResult = permissionManager.checkPermissionForPreflight(
+ permission, attributionSource);
+ } else {
+ permissionCheckResult = permissionManager.checkPermissionForDataDelivery(
+ permission, attributionSource, message);
+ }
if (permissionCheckResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
index 7f169db..68e21a3 100644
--- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig
+++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
@@ -15,4 +15,15 @@
description: "Limit the scope of receiver priorities to within a process"
is_fixed_read_only: true
bug: "369487976"
+}
+
+flag {
+ name: "avoid_note_op_at_enqueue"
+ namespace: "backstage_power"
+ description: "Avoid triggering noteOp while enqueueing a broadcast"
+ is_fixed_read_only: true
+ bug: "268016162"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3cb2125..0f1228f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1164,9 +1164,11 @@
@GuardedBy("mAccessibilityServiceUidsLock")
private int[] mAccessibilityServiceUids;
- // Uid of the active input method service to check if caller is the one or not.
- private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
+ // Input Method
private final Object mInputMethodServiceUidLock = new Object();
+ // Uid of the active input method service to check if caller is the one or not.
+ @GuardedBy("mInputMethodServiceUidLock")
+ private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
private int mEncodedSurroundMode;
private String mEnabledSurroundFormats;
@@ -11405,7 +11407,7 @@
/** see {@link AudioManager#getFocusDuckedUidsForTest()} */
@Override
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public @NonNull List<Integer> getFocusDuckedUidsForTest() {
super.getFocusDuckedUidsForTest_enforcePermission();
return mPlaybackMonitor.getFocusDuckedUids();
@@ -11432,7 +11434,7 @@
* @see AudioManager#getFocusFadeOutDurationForTest()
* @return the fade out duration, in ms
*/
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public long getFocusFadeOutDurationForTest() {
super.getFocusFadeOutDurationForTest_enforcePermission();
return mMediaFocusControl.getFocusFadeOutDurationForTest();
@@ -11445,7 +11447,7 @@
* @return the time gap after a fade out completion on focus loss, and fade in start, in ms
*/
@Override
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public long getFocusUnmuteDelayAfterFadeOutForTest() {
super.getFocusUnmuteDelayAfterFadeOutForTest_enforcePermission();
return mMediaFocusControl.getFocusUnmuteDelayAfterFadeOutForTest();
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 324f95a..964b97c 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -51,7 +51,6 @@
import android.view.SurfaceControl;
import com.android.internal.R;
-import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.display.BrightnessSynchronizer;
import com.android.internal.util.function.pooled.PooledLambda;
@@ -1548,9 +1547,7 @@
}
public static class Injector {
- // Ensure the callback is kept to preserve native weak reference lifecycle semantics.
@SuppressWarnings("unused")
- @KeepForWeakReference
private ProxyDisplayEventReceiver mReceiver;
public void setDisplayEventListenerLocked(Looper looper, DisplayEventListener listener) {
mReceiver = new ProxyDisplayEventReceiver(looper, listener);
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
index f14cda1..11b2805 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
@@ -96,8 +96,10 @@
public void onSendCompleted(int error) {
if (error != SendMessageResult.SUCCESS) {
HdmiLogger.debug("Failed to send <System Audio Mode Request>:" + error);
- setSystemAudioMode(false);
- finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
+ if (error == SendMessageResult.FAIL) {
+ setSystemAudioMode(false);
+ finishWithCallback(HdmiControlManager.RESULT_COMMUNICATION_FAILED);
+ }
}
}
});
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index fba0b04..ef5babf 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -35,6 +35,7 @@
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
+import android.hardware.display.DisplayManager;
import android.hardware.input.AidlInputGestureData;
import android.hardware.input.AidlKeyGestureEvent;
import android.hardware.input.AppLaunchData;
@@ -57,6 +58,7 @@
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+import android.view.Display;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -127,6 +129,7 @@
private final SettingsObserver mSettingsObserver;
private final AppLaunchShortcutManager mAppLaunchShortcutManager;
private final InputGestureManager mInputGestureManager;
+ private final DisplayManager mDisplayManager;
@GuardedBy("mInputDataStore")
private final InputDataStore mInputDataStore;
private static final Object mUserLock = new Object();
@@ -194,6 +197,7 @@
mSettingsObserver = new SettingsObserver(mHandler);
mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
mInputGestureManager = new InputGestureManager(mContext);
+ mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class));
mInputDataStore = inputDataStore;
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
initBehaviors();
@@ -246,12 +250,6 @@
new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
KeyEvent.KEYCODE_POWER) {
@Override
- public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
- }
-
- @Override
public void execute() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
@@ -274,12 +272,6 @@
new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_POWER,
KeyEvent.KEYCODE_STEM_PRIMARY) {
@Override
- public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
- }
-
- @Override
public void execute() {
handleMultiKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
KeyEvent.KEYCODE_STEM_PRIMARY},
@@ -333,9 +325,6 @@
KeyEvent.KEYCODE_POWER) {
@Override
public boolean preCondition() {
- if (!isKeyGestureSupported(getGestureType())) {
- return false;
- }
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
return mRingerToggleChord != Settings.Secure.VOLUME_HUSH_OFF;
@@ -423,12 +412,6 @@
new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_DPAD_CENTER) {
@Override
- public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
- }
-
- @Override
public void execute() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
@@ -468,10 +451,11 @@
if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
return false;
}
- final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
if (InputSettings.doesKeyGestureEventHandlerSupportMultiKeyGestures()
&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- return mKeyCombinationManager.interceptKey(event, interactive);
+ final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
+ final boolean isDefaultDisplayOn = isDefaultDisplayOn();
+ return mKeyCombinationManager.interceptKey(event, interactive && isDefaultDisplayOn);
}
return false;
}
@@ -1038,6 +1022,14 @@
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+ private boolean isDefaultDisplayOn() {
+ Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ if (defaultDisplay == null) {
+ return false;
+ }
+ return Display.isOnState(defaultDisplay.getState());
+ }
+
@MainThread
private void notifyKeyGestureEvent(AidlKeyGestureEvent event) {
InputDevice device = getInputDevice(event.deviceId);
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 7e80cbc..0944a54 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -48,7 +48,6 @@
import android.util.Slog;
import android.util.Xml;
-import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.util.FrameworkStatsLog;
@@ -101,7 +100,6 @@
private LocaleManagerBackupHelper mBackupHelper;
- @KeepForWeakReference
private final PackageMonitor mPackageMonitor;
private final Object mWriteLock = new Object();
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 940bcb4..f40d0dd 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -43,7 +43,10 @@
import com.android.internal.annotations.GuardedBy;
import java.util.Collection;
+import java.util.HashSet;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
/**
* A class that represents a broker for the endpoint registered by the client app. This class
@@ -111,6 +114,11 @@
private final boolean mRemoteInitiated;
+ /**
+ * The set of seq # for pending reliable messages started by this endpoint for this session.
+ */
+ private final Set<Integer> mPendingSequenceNumbers = new HashSet<>();
+
SessionInfo(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) {
mRemoteEndpointInfo = remoteEndpointInfo;
mRemoteInitiated = remoteInitiated;
@@ -131,6 +139,24 @@
public boolean isActive() {
return mSessionState == SessionState.ACTIVE;
}
+
+ public boolean isReliableMessagePending(int sequenceNumber) {
+ return mPendingSequenceNumbers.contains(sequenceNumber);
+ }
+
+ public void setReliableMessagePending(int sequenceNumber) {
+ mPendingSequenceNumbers.add(sequenceNumber);
+ }
+
+ public void setReliableMessageCompleted(int sequenceNumber) {
+ mPendingSequenceNumbers.remove(sequenceNumber);
+ }
+
+ public void forEachPendingReliableMessage(Consumer<Integer> consumer) {
+ for (int sequenceNumber : mPendingSequenceNumbers) {
+ consumer.accept(sequenceNumber);
+ }
+ }
}
/** A map between a session ID which maps to its current state. */
@@ -208,10 +234,7 @@
try {
mSessionInfoMap.put(sessionId, new SessionInfo(destination, false));
mHubInterface.openEndpointSession(
- sessionId,
- halEndpointInfo.id,
- mHalEndpointInfo.id,
- serviceDescriptor);
+ sessionId, halEndpointInfo.id, mHalEndpointInfo.id, serviceDescriptor);
} catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) {
Log.e(TAG, "Exception while calling HAL openEndpointSession", e);
cleanupSessionResources(sessionId);
@@ -286,34 +309,42 @@
public void sendMessage(
int sessionId, HubMessage message, IContextHubTransactionCallback callback) {
super.sendMessage_enforcePermission();
- Message halMessage = ContextHubServiceUtil.createHalMessage(message);
- if (!isSessionActive(sessionId)) {
- throw new SecurityException(
- "sendMessage called on inactive session (id= " + sessionId + ")");
- }
-
- if (callback == null) {
- try {
- mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
- } catch (RemoteException e) {
- Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ synchronized (mOpenSessionLock) {
+ SessionInfo info = mSessionInfoMap.get(sessionId);
+ if (info == null) {
+ throw new IllegalArgumentException(
+ "sendMessage for invalid session id=" + sessionId);
}
- } else {
- ContextHubServiceTransaction transaction =
- mTransactionManager.createSessionMessageTransaction(
- mHubInterface, sessionId, halMessage, mPackageName, callback);
- try {
- mTransactionManager.addTransaction(transaction);
- } catch (IllegalStateException e) {
- Log.e(
- TAG,
- "Unable to add a transaction in sendMessageToEndpoint "
- + "(session ID = "
- + sessionId
- + ")",
- e);
- transaction.onTransactionComplete(
- ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+ if (!info.isActive()) {
+ throw new SecurityException(
+ "sendMessage called on inactive session (id= " + sessionId + ")");
+ }
+
+ Message halMessage = ContextHubServiceUtil.createHalMessage(message);
+ if (callback == null) {
+ try {
+ mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ }
+ } else {
+ ContextHubServiceTransaction transaction =
+ mTransactionManager.createSessionMessageTransaction(
+ mHubInterface, sessionId, halMessage, mPackageName, callback);
+ try {
+ mTransactionManager.addTransaction(transaction);
+ info.setReliableMessagePending(transaction.getMessageSequenceNumber());
+ } catch (IllegalStateException e) {
+ Log.e(
+ TAG,
+ "Unable to add a transaction in sendMessageToEndpoint "
+ + "(session ID = "
+ + sessionId
+ + ")",
+ e);
+ transaction.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+ }
}
}
}
@@ -393,7 +424,9 @@
int id = mSessionInfoMap.keyAt(i);
int count = i + 1;
sb.append(
- " " + count + ". id="
+ " "
+ + count
+ + ". id="
+ id
+ ", remote:"
+ mSessionInfoMap.get(id).getRemoteEndpointInfo());
@@ -461,13 +494,24 @@
/* package */ void onMessageReceived(int sessionId, HubMessage message) {
byte code = onMessageReceivedInternal(sessionId, message);
if (code != ErrorCode.OK && message.isResponseRequired()) {
- sendMessageDeliveryStatus(
- sessionId, message.getMessageSequenceNumber(), code);
+ sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), code);
}
}
/* package */ void onMessageDeliveryStatusReceived(
int sessionId, int sequenceNumber, byte errorCode) {
+ synchronized (mOpenSessionLock) {
+ SessionInfo info = mSessionInfoMap.get(sessionId);
+ if (info == null || !info.isActive()) {
+ Log.w(TAG, "Received delivery status for invalid session: id=" + sessionId);
+ return;
+ }
+ if (!info.isReliableMessagePending(sequenceNumber)) {
+ Log.w(TAG, "Received delivery status for unknown seq: " + sequenceNumber);
+ return;
+ }
+ info.setReliableMessageCompleted(sequenceNumber);
+ }
mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
}
@@ -492,7 +536,6 @@
onCloseEndpointSession(id, Reason.HUB_RESET);
}
}
- // TODO(b/390029594): Cancel any ongoing reliable communication transactions
}
private Optional<Byte> onEndpointSessionOpenRequestInternal(
@@ -515,9 +558,11 @@
mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true));
}
- boolean success = invokeCallback(
- (consumer) ->
- consumer.onSessionOpenRequest(sessionId, initiator, serviceDescriptor));
+ boolean success =
+ invokeCallback(
+ (consumer) ->
+ consumer.onSessionOpenRequest(
+ sessionId, initiator, serviceDescriptor));
return success ? Optional.empty() : Optional.of(Reason.UNSPECIFIED);
}
@@ -590,8 +635,15 @@
private boolean cleanupSessionResources(int sessionId) {
synchronized (mOpenSessionLock) {
SessionInfo info = mSessionInfoMap.get(sessionId);
- if (info != null && !info.isRemoteInitiated()) {
- mEndpointManager.returnSessionId(sessionId);
+ if (info != null) {
+ if (!info.isRemoteInitiated()) {
+ mEndpointManager.returnSessionId(sessionId);
+ }
+ info.forEachPendingReliableMessage(
+ (sequenceNumber) -> {
+ mTransactionManager.onMessageDeliveryResponse(
+ sequenceNumber, /* success= */ false);
+ });
mSessionInfoMap.remove(sessionId);
}
return info != null;
@@ -646,10 +698,7 @@
try {
mWakeLock.release();
} catch (RuntimeException e) {
- Log.e(
- TAG,
- "Releasing the wakelock for all acquisitions fails - ",
- e);
+ Log.e(TAG, "Releasing the wakelock for all acquisitions fails - ", e);
break;
}
}
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 a430a82..6a1db02 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -29,6 +29,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.time.Duration;
import java.util.ArrayDeque;
@@ -165,52 +166,61 @@
/**
* Creates a transaction for loading a nanoapp.
*
- * @param contextHubId the ID of the hub to load the nanoapp to
- * @param nanoAppBinary the binary of the nanoapp to load
+ * @param contextHubId the ID of the hub to load the nanoapp to
+ * @param nanoAppBinary the binary of the nanoapp to load
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createLoadTransaction(
- int contextHubId, NanoAppBinary nanoAppBinary,
- IContextHubTransactionCallback onCompleteCallback, String packageName) {
+ int contextHubId,
+ NanoAppBinary nanoAppBinary,
+ IContextHubTransactionCallback onCompleteCallback,
+ String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP,
- nanoAppBinary.getNanoAppId(), packageName) {
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_LOAD_NANOAPP,
+ nanoAppBinary.getNanoAppId(),
+ packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.loadNanoapp(
contextHubId, nanoAppBinary, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" +
- Long.toHexString(nanoAppBinary.getNanoAppId()), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to load nanoapp with ID 0x"
+ + Long.toHexString(nanoAppBinary.getNanoAppId()),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
ContextHubStatsLog.write(
ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
nanoAppBinary.getNanoAppId(),
nanoAppBinary.getNanoAppVersion(),
ContextHubStatsLog
- .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
toStatsTransactionResult(result));
- ContextHubEventLogger.getInstance().logNanoappLoad(
- contextHubId,
- nanoAppBinary.getNanoAppId(),
- nanoAppBinary.getNanoAppVersion(),
- nanoAppBinary.getBinary().length,
- result == ContextHubTransaction.RESULT_SUCCESS);
+ ContextHubEventLogger.getInstance()
+ .logNanoappLoad(
+ contextHubId,
+ nanoAppBinary.getNanoAppId(),
+ nanoAppBinary.getNanoAppVersion(),
+ nanoAppBinary.getBinary().length,
+ result == ContextHubTransaction.RESULT_SUCCESS);
if (result == ContextHubTransaction.RESULT_SUCCESS) {
// NOTE: The legacy JNI code used to do a query right after a load success
// to synchronize the service cache. Instead store the binary that was
// requested to load to update the cache later without doing a query.
mNanoAppStateManager.addNanoAppInstance(
- contextHubId, nanoAppBinary.getNanoAppId(),
+ contextHubId,
+ nanoAppBinary.getNanoAppId(),
nanoAppBinary.getNanoAppVersion());
}
try {
@@ -228,42 +238,51 @@
/**
* Creates a transaction for unloading a nanoapp.
*
- * @param contextHubId the ID of the hub to unload the nanoapp from
- * @param nanoAppId the ID of the nanoapp to unload
+ * @param contextHubId the ID of the hub to unload the nanoapp from
+ * @param nanoAppId the ID of the nanoapp to unload
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createUnloadTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
- nanoAppId, packageName) {
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
+ nanoAppId,
+ packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.unloadNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to unload nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
ContextHubStatsLog.write(
- ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppId,
+ ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
+ nanoAppId,
0 /* nanoappVersion */,
ContextHubStatsLog
- .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
toStatsTransactionResult(result));
- ContextHubEventLogger.getInstance().logNanoappUnload(
- contextHubId,
- nanoAppId,
- result == ContextHubTransaction.RESULT_SUCCESS);
+ ContextHubEventLogger.getInstance()
+ .logNanoappUnload(
+ contextHubId,
+ nanoAppId,
+ result == ContextHubTransaction.RESULT_SUCCESS);
if (result == ContextHubTransaction.RESULT_SUCCESS) {
mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId);
@@ -283,31 +302,37 @@
/**
* Creates a transaction for enabling a nanoapp.
*
- * @param contextHubId the ID of the hub to enable the nanoapp on
- * @param nanoAppId the ID of the nanoapp to enable
+ * @param contextHubId the ID of the hub to enable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to enable
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createEnableTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_ENABLE_NANOAPP,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.enableNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to enable nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
} catch (RemoteException e) {
@@ -320,31 +345,37 @@
/**
* Creates a transaction for disabling a nanoapp.
*
- * @param contextHubId the ID of the hub to disable the nanoapp on
- * @param nanoAppId the ID of the nanoapp to disable
+ * @param contextHubId the ID of the hub to disable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to disable
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createDisableTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_DISABLE_NANOAPP,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.disableNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to disable nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
} catch (RemoteException e) {
@@ -447,18 +478,20 @@
/**
* Creates a transaction for querying for a list of nanoapps.
*
- * @param contextHubId the ID of the hub to query
+ * @param contextHubId the ID of the hub to query
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createQueryTransaction(
- int contextHubId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_QUERY_NANOAPPS,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.queryNanoapps(contextHubId);
} catch (RemoteException e) {
@@ -468,12 +501,12 @@
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
onQueryResponse(result, Collections.emptyList());
}
@Override
- /* package */ void onQueryResponse(
+ /* package */ void onQueryResponse(
@ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
try {
onCompleteCallback.onQueryResponse(result, nanoAppStateList);
@@ -539,6 +572,14 @@
}
}
+ @VisibleForTesting
+ /* package */
+ int numReliableMessageTransactionPending() {
+ synchronized (mReliableMessageLock) {
+ return mReliableMessageTransactionMap.size();
+ }
+ }
+
/**
* Handles a transaction response from a Context Hub.
*
@@ -585,18 +626,21 @@
void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
if (!Flags.reliableMessageRetrySupportService()) {
TransactionAcceptConditions conditions =
- transaction -> transaction.getTransactionType()
- == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
- && transaction.getMessageSequenceNumber()
- == messageSequenceNumber;
+ transaction ->
+ transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+ && transaction.getMessageSequenceNumber()
+ == messageSequenceNumber;
ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
- Log.w(TAG, "Received unexpected message delivery response (expected"
- + " message sequence number = "
- + messageSequenceNumber
- + ", received messageSequenceNumber = "
- + messageSequenceNumber
- + ")");
+ Log.w(
+ TAG,
+ "Received unexpected message delivery response (expected"
+ + " message sequence number = "
+ + messageSequenceNumber
+ + ", received messageSequenceNumber = "
+ + messageSequenceNumber
+ + ")");
return;
}
@@ -640,8 +684,10 @@
*/
/* package */
void onQueryResponse(List<NanoAppState> nanoAppStateList) {
- TransactionAcceptConditions conditions = transaction ->
- transaction.getTransactionType() == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
+ TransactionAcceptConditions conditions =
+ transaction ->
+ transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
Log.w(TAG, "Received unexpected query response");
@@ -968,24 +1014,33 @@
private int toStatsTransactionResult(@ContextHubTransaction.Result int result) {
switch (result) {
case ContextHubTransaction.RESULT_SUCCESS:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
case ContextHubTransaction.RESULT_FAILED_BUSY:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
case ContextHubTransaction.RESULT_FAILED_AT_HUB:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
case ContextHubTransaction.RESULT_FAILED_TIMEOUT:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
case ContextHubTransaction.RESULT_FAILED_UNKNOWN:
default: /* fall through */
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
}
}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index 7a96195..9937049 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -21,6 +21,7 @@
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
@@ -204,6 +205,11 @@
return false;
}
final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp;
- return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+ try {
+ return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+ } catch (SQLiteDatabaseCorruptException e) {
+ Slog.e(TAG, "Error deleting records", e);
+ return false;
+ }
}
}
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3f2c222..dd52cce 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -46,6 +46,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
public class ConditionProviders extends ManagedServices {
@@ -202,7 +203,14 @@
@Override
protected void loadDefaultsFromConfig() {
- String defaultDndAccess = mContext.getResources().getString(
+ for (String dndPackage : getDefaultDndAccessPackages(mContext)) {
+ addDefaultComponentOrPackage(dndPackage);
+ }
+ }
+
+ static List<String> getDefaultDndAccessPackages(Context context) {
+ ArrayList<String> packages = new ArrayList<>();
+ String defaultDndAccess = context.getResources().getString(
R.string.config_defaultDndAccessPackages);
if (defaultDndAccess != null) {
String[] dnds = defaultDndAccess.split(ManagedServices.ENABLED_SERVICES_SEPARATOR);
@@ -210,9 +218,10 @@
if (TextUtils.isEmpty(dnds[i])) {
continue;
}
- addDefaultComponentOrPackage(dnds[i]);
+ packages.add(dnds[i]);
}
}
+ return packages;
}
@Override
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index fff812c..0fc182f 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -42,7 +42,6 @@
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
-import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_BUBBLE;
import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
import android.annotation.FlaggedApi;
@@ -287,7 +286,7 @@
if (!TAG_RANKING.equals(tag)) return;
final int xmlVersion = parser.getAttributeInt(null, ATT_VERSION, -1);
- boolean upgradeForBubbles = xmlVersion >= XML_VERSION_BUBBLES_UPGRADE;
+ boolean upgradeForBubbles = xmlVersion == XML_VERSION_BUBBLES_UPGRADE;
boolean migrateToPermission = (xmlVersion < XML_VERSION_NOTIF_PERMISSION);
if (mShowReviewPermissionsNotification
&& (xmlVersion < XML_VERSION_REVIEW_PERMISSIONS_NOTIFICATION)) {
@@ -338,19 +337,15 @@
}
boolean skipWarningLogged = false;
boolean skipGroupWarningLogged = false;
- int bubblePref = parser.getAttributeInt(null, ATT_ALLOW_BUBBLE,
- DEFAULT_BUBBLE_PREFERENCE);
- boolean bubbleLocked = (parser.getAttributeInt(null,
- ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS) & USER_LOCKED_BUBBLE)
- != 0;
- if (!bubbleLocked
- && upgradeForBubbles
- && uid != UNKNOWN_UID
- && mAppOps.noteOpNoThrow(OP_SYSTEM_ALERT_WINDOW, uid, name, null,
- "check-notif-bubble") == AppOpsManager.MODE_ALLOWED) {
- // User hasn't changed bubble pref & the app has SAW, so allow all bubbles.
- bubblePref = BUBBLE_PREFERENCE_ALL;
+ boolean hasSAWPermission = false;
+ if (upgradeForBubbles && uid != UNKNOWN_UID) {
+ hasSAWPermission = mAppOps.noteOpNoThrow(
+ OP_SYSTEM_ALERT_WINDOW, uid, name, null,
+ "check-notif-bubble") == AppOpsManager.MODE_ALLOWED;
}
+ int bubblePref = hasSAWPermission
+ ? BUBBLE_PREFERENCE_ALL
+ : parser.getAttributeInt(null, ATT_ALLOW_BUBBLE, DEFAULT_BUBBLE_PREFERENCE);
int appImportance = parser.getAttributeInt(null, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
// when data is loaded from disk it's loaded as USER_ALL, but restored data that
diff --git a/services/core/java/com/android/server/notification/ZenConfigTrimmer.java b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
new file mode 100644
index 0000000..d65954d
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class ZenConfigTrimmer {
+
+ private static final String TAG = "ZenConfigTrimmer";
+ private static final int MAXIMUM_PARCELED_SIZE = 150_000; // bytes
+
+ private final HashSet<String> mTrustedPackages;
+
+ ZenConfigTrimmer(Context context) {
+ mTrustedPackages = new HashSet<>();
+ mTrustedPackages.add(SystemZenRules.PACKAGE_ANDROID);
+ mTrustedPackages.addAll(ConditionProviders.getDefaultDndAccessPackages(context));
+ }
+
+ void trimToMaximumSize(ZenModeConfig config) {
+ Map<String, PackageRules> rulesPerPackage = new HashMap<>();
+ for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) {
+ PackageRules pkgRules = rulesPerPackage.computeIfAbsent(rule.pkg, PackageRules::new);
+ pkgRules.mRules.add(rule);
+ }
+
+ int totalSize = 0;
+ for (PackageRules pkgRules : rulesPerPackage.values()) {
+ totalSize += pkgRules.dataSize();
+ }
+
+ if (totalSize > MAXIMUM_PARCELED_SIZE) {
+ List<PackageRules> deletionCandidates = new ArrayList<>();
+ for (PackageRules pkgRules : rulesPerPackage.values()) {
+ if (!mTrustedPackages.contains(pkgRules.mPkg)) {
+ deletionCandidates.add(pkgRules);
+ }
+ }
+ deletionCandidates.sort(Comparator.comparingInt(PackageRules::dataSize).reversed());
+
+ evictPackagesFromConfig(config, deletionCandidates, totalSize);
+ }
+ }
+
+ private static void evictPackagesFromConfig(ZenModeConfig config,
+ List<PackageRules> deletionCandidates, int currentSize) {
+ while (currentSize > MAXIMUM_PARCELED_SIZE && !deletionCandidates.isEmpty()) {
+ PackageRules rulesToDelete = deletionCandidates.removeFirst();
+ Slog.w(TAG, String.format("Evicting %s zen rules from package '%s' (%s bytes)",
+ rulesToDelete.mRules.size(), rulesToDelete.mPkg, rulesToDelete.dataSize()));
+
+ for (ZenModeConfig.ZenRule rule : rulesToDelete.mRules) {
+ config.automaticRules.remove(rule.id);
+ }
+
+ currentSize -= rulesToDelete.dataSize();
+ }
+ }
+
+ private static class PackageRules {
+ private final String mPkg;
+ private final List<ZenModeConfig.ZenRule> mRules;
+ private int mParceledSize = -1;
+
+ PackageRules(String pkg) {
+ mPkg = pkg;
+ mRules = new ArrayList<>();
+ }
+
+ private int dataSize() {
+ if (mParceledSize >= 0) {
+ return mParceledSize;
+ }
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.writeParcelableList(mRules, 0);
+ mParceledSize = parcel.dataSize();
+ return mParceledSize;
+ } finally {
+ parcel.recycle();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 889df51..8b09c2a 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -48,6 +48,7 @@
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.server.notification.Flags.preventZenDeviceEffectsWhileDriving;
+import static com.android.server.notification.Flags.limitZenConfigSize;
import static java.util.Objects.requireNonNull;
@@ -192,6 +193,7 @@
private final ConditionProviders.Config mServiceConfig;
private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
private final ZenModeEventLogger mZenModeEventLogger;
+ private final ZenConfigTrimmer mConfigTrimmer;
@VisibleForTesting protected int mZenMode;
@VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy;
@@ -226,6 +228,7 @@
mClock = clock;
addCallback(mMetrics);
mAppOps = context.getSystemService(AppOpsManager.class);
+ mConfigTrimmer = new ZenConfigTrimmer(mContext);
mDefaultConfig = Flags.modesUi()
? ZenModeConfig.getDefaultConfig()
@@ -2061,20 +2064,20 @@
Log.w(TAG, "Invalid config in setConfigLocked; " + config);
return false;
}
+ if (limitZenConfigSize() && (origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP)) {
+ mConfigTrimmer.trimToMaximumSize(config);
+ }
+
if (config.user != mUser) {
// simply store away for background users
- synchronized (mConfigLock) {
- mConfigs.put(config.user, config);
- }
+ mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);
return true;
}
// handle CPS backed conditions - danger! may modify config
mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);
- synchronized (mConfigLock) {
- mConfigs.put(config.user, config);
- }
+ mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
ZenLog.traceConfig(origin, reason, triggeringComponent, mConfig, config, callingUid);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 76cd5c8..346d65a 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -212,6 +212,16 @@
}
flag {
+ name: "limit_zen_config_size"
+ namespace: "systemui"
+ description: "Enforce a maximum (serialized) size for the Zen configuration"
+ bug: "387498139"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "managed_services_concurrent_multiuser"
namespace: "systemui"
description: "Enables ManagedServices to support Concurrent multi user environment"
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index e613700..8d787fe 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -82,7 +82,6 @@
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.content.PackageMonitor;
import com.android.internal.content.om.OverlayConfig;
import com.android.internal.util.ArrayUtils;
@@ -267,7 +266,6 @@
private final OverlayActorEnforcer mActorEnforcer;
- @KeepForWeakReference
private final PackageMonitor mPackageMonitor = new OverlayManagerPackageMonitor();
private int mPrevStartedUserId = -1;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 80c2d41..5a6d7a2 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2937,28 +2937,20 @@
int flags = UserManager.SWITCHABILITY_STATUS_OK;
- t.traceBegin("TM.isInCall");
- final long identity = Binder.clearCallingIdentity();
- try {
- final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
- if (com.android.internal.telephony.flags
- .Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- if (mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TELECOM)) {
- if (telecomManager != null && telecomManager.isInCall()) {
- flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
- }
- }
- } else {
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) {
+ t.traceBegin("TM.isInCall");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final TelecomManager telecomManager = mContext.getSystemService(
+ TelecomManager.class);
if (telecomManager != null && telecomManager.isInCall()) {
flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
}
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
- } finally {
- Binder.restoreCallingIdentity(identity);
+ t.traceEnd();
}
- t.traceEnd();
-
t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java
index 1592ef3..1b98dd1 100644
--- a/services/core/java/com/android/server/policy/KeyCombinationManager.java
+++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java
@@ -148,19 +148,19 @@
* to a window.
* Return true if any active rule could be triggered by the key event, otherwise false.
*/
- public boolean interceptKey(KeyEvent event, boolean interactive) {
+ public boolean interceptKey(KeyEvent event, boolean isDefaultDisplayInteractive) {
synchronized (mLock) {
- return interceptKeyLocked(event, interactive);
+ return interceptKeyLocked(event, isDefaultDisplayInteractive);
}
}
- private boolean interceptKeyLocked(KeyEvent event, boolean interactive) {
+ private boolean interceptKeyLocked(KeyEvent event, boolean isDefaultDisplayInteractive) {
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
final int keyCode = event.getKeyCode();
final int count = mActiveRules.size();
final long eventTime = event.getEventTime();
- if (interactive && down) {
+ if (isDefaultDisplayInteractive && down) {
if (mDownTimes.size() > 0) {
if (count > 0
&& eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d11f5e7..f27194a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4283,22 +4283,19 @@
case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
- return mDefaultDisplayPolicy.isAwake();
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
- .isAccessibilityShortcutAvailable(isKeyguardLocked());
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
- .isAccessibilityShortcutAvailable(false);
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
- return enableTalkbackAndMagnifierKeyGestures();
case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
- return enableVoiceAccessKeyGestures();
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ isKeyguardLocked());
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ false);
default:
return false;
}
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 16658e3..a64e38e 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -131,7 +131,6 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.camera.flags.Flags;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.os.BackgroundThread;
@@ -2008,11 +2007,7 @@
}
private class CallStateHelper {
- // TelephonyCallback instances are only weakly referenced when registered, so we need
- // to ensure these fields are kept during optimization to preserve lifecycle semantics.
- @KeepForWeakReference
private final OutgoingEmergencyStateCallback mEmergencyStateCallback;
- @KeepForWeakReference
private final CallStateCallback mCallStateCallback;
private boolean mIsInEmergencyCall;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index fab19b6..1afbb34 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -160,8 +160,10 @@
* @param displayId The changed display Id.
* @param rootDisplayAreaId The changed display area Id.
* @param isImmersiveMode {@code true} if the display area get into immersive mode.
+ * @param windowType The window type of the controlling window.
*/
- void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode);
+ void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType);
/**
* Show a rotation suggestion that a user may approve to rotate the screen.
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index da9d016..798c794 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -732,7 +732,7 @@
@Override
public void immersiveModeChanged(int displayId, int rootDisplayAreaId,
- boolean isImmersiveMode) {
+ boolean isImmersiveMode, int windowType) {
if (mBar == null) {
return;
}
@@ -746,7 +746,7 @@
if (!CLIENT_TRANSIENT) {
// Only call from here when the client transient is not enabled.
try {
- mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode, windowType);
} catch (RemoteException ex) {
}
}
diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
index 94e52cd..d4b20fb 100644
--- a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
+++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
@@ -68,6 +68,10 @@
return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo);
}
+ public ImmutableVolumeInfo getClonedImmutableVolumeInfo() {
+ return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo.clone());
+ }
+
public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
}
diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
index bda3d44..621a128 100644
--- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
@@ -51,6 +51,7 @@
final class VendorVibrationSession extends IVibrationSession.Stub
implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient {
private static final String TAG = "VendorVibrationSession";
+ private static final boolean DEBUG = false;
/** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */
interface VibratorManagerHooks {
@@ -73,8 +74,8 @@
private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport();
private final int[] mVibratorIds;
private final long mCreateUptime;
- private final long mCreateTime; // for debugging
- private final IVibrationSessionCallback mCallback;
+ private final long mCreateTime;
+ private final VendorCallbackWrapper mCallback;
private final CallerInfo mCallerInfo;
private final VibratorManagerHooks mManagerHooks;
private final DeviceAdapter mDeviceAdapter;
@@ -88,11 +89,11 @@
@GuardedBy("mLock")
private boolean mEndedByVendor;
@GuardedBy("mLock")
- private long mStartTime; // for debugging
+ private long mStartTime;
@GuardedBy("mLock")
private long mEndUptime;
@GuardedBy("mLock")
- private long mEndTime; // for debugging
+ private long mEndTime;
@GuardedBy("mLock")
private VibrationStepConductor mConductor;
@@ -103,7 +104,7 @@
mCreateTime = System.currentTimeMillis();
mVibratorIds = deviceAdapter.getAvailableVibratorIds();
mHandler = handler;
- mCallback = callback;
+ mCallback = new VendorCallbackWrapper(callback, handler);
mCallerInfo = callerInfo;
mManagerHooks = managerHooks;
mDeviceAdapter = deviceAdapter;
@@ -119,7 +120,9 @@
@Override
public void finishSession() {
- Slog.d(TAG, "Session finish requested, ending vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session finish requested, ending vibration session...");
+ }
// Do not abort session in HAL, wait for ongoing vibration requests to complete.
// This might take a while to end the session, but it can be aborted by cancelSession.
requestEndSession(Status.FINISHED, /* shouldAbort= */ false, /* isVendorRequest= */ true);
@@ -127,7 +130,9 @@
@Override
public void cancelSession() {
- Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+ }
// Always abort session in HAL while cancelling it.
// This might be triggered after finishSession was already called.
requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
@@ -156,7 +161,7 @@
@Override
public IBinder getCallerToken() {
- return mCallback.asBinder();
+ return mCallback.getBinderToken();
}
@Override
@@ -176,36 +181,30 @@
@Override
public void onCancel() {
- Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+ }
requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
/* isVendorRequest= */ true);
}
@Override
public void binderDied() {
- Slog.d(TAG, "Session binder died, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session binder died, aborting vibration session...");
+ }
requestEndSession(Status.CANCELLED_BINDER_DIED, /* shouldAbort= */ true,
/* isVendorRequest= */ false);
}
@Override
public boolean linkToDeath() {
- try {
- mCallback.asBinder().linkToDeath(this, 0);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error linking session to token death", e);
- return false;
- }
- return true;
+ return mCallback.linkToDeath(this);
}
@Override
public void unlinkToDeath() {
- try {
- mCallback.asBinder().unlinkToDeath(this, 0);
- } catch (NoSuchElementException e) {
- Slog.wtf(TAG, "Failed to unlink session to token death", e);
- }
+ mCallback.unlinkToDeath(this);
}
@Override
@@ -219,26 +218,37 @@
@Override
public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) {
- Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + " step " + stepId
- + " on vibrator " + vibratorId + ", ignoring...");
+ if (DEBUG) {
+ Slog.d(TAG, "Vibration callback received for vibration " + vibrationId
+ + " step " + stepId + " on vibrator " + vibratorId + ", ignoring...");
+ }
}
@Override
public void notifySyncedVibratorsCallback(long vibrationId) {
- Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
- + ", ignoring...");
+ if (DEBUG) {
+ Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
+ + ", ignoring...");
+ }
}
@Override
public void notifySessionCallback() {
- Slog.d(TAG, "Session callback received, ending vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session callback received, ending vibration session...");
+ }
synchronized (mLock) {
// If end was not requested then the HAL has cancelled the session.
- maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
+ notifyEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
/* isVendorRequest= */ false);
maybeSetStatusToRequestedLocked();
clearVibrationConductor();
- mHandler.post(() -> mManagerHooks.onSessionReleased(mSessionId));
+ final Status endStatus = mStatus;
+ mHandler.post(() -> {
+ mManagerHooks.onSessionReleased(mSessionId);
+ // Only trigger client callback after session is released in the manager.
+ mCallback.notifyFinished(endStatus);
+ });
}
}
@@ -271,7 +281,7 @@
public boolean isEnded() {
synchronized (mLock) {
- return mStatus != Status.RUNNING;
+ return mEndTime > 0;
}
}
@@ -297,19 +307,17 @@
// Session already ended, skip start callbacks.
isAlreadyEnded = true;
} else {
+ if (DEBUG) {
+ Slog.d(TAG, "Session started at the HAL");
+ }
mStartTime = System.currentTimeMillis();
- // Run client callback in separate thread.
- mHandler.post(() -> {
- try {
- mCallback.onStarted(this);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session started", e);
- }
- });
+ mCallback.notifyStarted(this);
}
}
if (isAlreadyEnded) {
- Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+ }
mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true));
}
}
@@ -337,8 +345,10 @@
public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) {
synchronized (mLock) {
if (mConductor != null) {
- Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
- + conductor.getVibration().id + " ignored");
+ if (DEBUG) {
+ Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
+ + conductor.getVibration().id + " ignored");
+ }
return false;
}
mConductor = conductor;
@@ -347,53 +357,45 @@
}
private void requestEndSession(Status status, boolean shouldAbort, boolean isVendorRequest) {
- Slog.d(TAG, "Session end request received with status " + status);
- boolean shouldTriggerSessionHook = false;
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request received with status " + status);
+ }
synchronized (mLock) {
- maybeSetEndRequestLocked(status, isVendorRequest);
+ notifyEndRequestLocked(status, isVendorRequest);
if (!isEnded() && isStarted()) {
// Trigger session hook even if it was already triggered, in case a second request
// is aborting the ongoing/ending session. This might cause it to end right away.
// Wait for HAL callback before setting the end status.
- shouldTriggerSessionHook = true;
+ if (DEBUG) {
+ Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
+ }
+ mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort));
} else {
- // Session not active in the HAL, set end status right away.
+ // Session not active in the HAL, try to set end status right away.
maybeSetStatusToRequestedLocked();
+ // Use status used to end this session, which might be different from requested.
+ mCallback.notifyFinished(mStatus);
}
}
- if (shouldTriggerSessionHook) {
- Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
- mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort));
- }
}
@GuardedBy("mLock")
- private void maybeSetEndRequestLocked(Status status, boolean isVendorRequest) {
+ private void notifyEndRequestLocked(Status status, boolean isVendorRequest) {
if (mEndStatusRequest != null) {
- // End already requested, keep first requested status and time.
+ // End already requested, keep first requested status.
return;
}
- Slog.d(TAG, "Session end request accepted for status " + status);
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request accepted for status " + status);
+ }
mEndStatusRequest = status;
mEndedByVendor = isVendorRequest;
- mEndTime = System.currentTimeMillis();
- mEndUptime = SystemClock.uptimeMillis();
+ mCallback.notifyFinishing();
if (mConductor != null) {
// Vibration is being dispatched when session end was requested, cancel it.
mConductor.notifyCancelled(new Vibration.EndInfo(status),
/* immediate= */ status != Status.FINISHED);
}
- if (isStarted()) {
- // Only trigger "finishing" callback if session started.
- // Run client callback in separate thread.
- mHandler.post(() -> {
- try {
- mCallback.onFinishing();
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session is finishing", e);
- }
- });
- }
}
@GuardedBy("mLock")
@@ -406,40 +408,123 @@
// No end status was requested, nothing to set.
return;
}
- Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+ }
mStatus = mEndStatusRequest;
- // Run client callback in separate thread.
- final Status endStatus = mStatus;
- mHandler.post(() -> {
- try {
- mCallback.onFinished(toSessionStatus(endStatus));
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session finished", e);
- }
- });
+ mEndTime = System.currentTimeMillis();
+ mEndUptime = SystemClock.uptimeMillis();
}
- @android.os.vibrator.VendorVibrationSession.Status
- private static int toSessionStatus(Status status) {
- // Exhaustive switch to cover all possible internal status.
- return switch (status) {
- case FINISHED
- -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
- case IGNORED_UNSUPPORTED
- -> STATUS_UNSUPPORTED;
- case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
- CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
- CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
- -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
- case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
- IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
- IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
- IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
- -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
- case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING,
- IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING
- -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
- };
+ /**
+ * Wrapper class to handle client callbacks asynchronously.
+ *
+ * <p>This class is also responsible for link/unlink to the client process binder death, and for
+ * making sure the callbacks are only triggered once. The conversion between session status and
+ * the API status code is also defined here.
+ */
+ private static final class VendorCallbackWrapper {
+ private final IVibrationSessionCallback mCallback;
+ private final Handler mHandler;
+
+ private boolean mIsStarted;
+ private boolean mIsFinishing;
+ private boolean mIsFinished;
+
+ VendorCallbackWrapper(@NonNull IVibrationSessionCallback callback,
+ @NonNull Handler handler) {
+ mCallback = callback;
+ mHandler = handler;
+ }
+
+ synchronized IBinder getBinderToken() {
+ return mCallback.asBinder();
+ }
+
+ synchronized boolean linkToDeath(DeathRecipient recipient) {
+ try {
+ mCallback.asBinder().linkToDeath(recipient, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error linking session to token death", e);
+ return false;
+ }
+ return true;
+ }
+
+ synchronized void unlinkToDeath(DeathRecipient recipient) {
+ try {
+ mCallback.asBinder().unlinkToDeath(recipient, 0);
+ } catch (NoSuchElementException e) {
+ Slog.wtf(TAG, "Failed to unlink session to token death", e);
+ }
+ }
+
+ synchronized void notifyStarted(IVibrationSession session) {
+ if (mIsStarted) {
+ return;
+ }
+ mIsStarted = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onStarted(session);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session started", e);
+ }
+ });
+ }
+
+ synchronized void notifyFinishing() {
+ if (!mIsStarted || mIsFinishing || mIsFinished) {
+ // Ignore if never started or if already finishing or finished.
+ return;
+ }
+ mIsFinishing = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onFinishing();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session is finishing", e);
+ }
+ });
+ }
+
+ synchronized void notifyFinished(Status status) {
+ if (mIsFinished) {
+ return;
+ }
+ mIsFinished = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onFinished(toSessionStatus(status));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session finished", e);
+ }
+ });
+ }
+
+ @android.os.vibrator.VendorVibrationSession.Status
+ private static int toSessionStatus(Status status) {
+ // Exhaustive switch to cover all possible internal status.
+ return switch (status) {
+ case FINISHED
+ -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
+ case IGNORED_UNSUPPORTED
+ -> STATUS_UNSUPPORTED;
+ case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
+ CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
+ CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
+ -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
+ case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
+ IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
+ IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
+ IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
+ -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
+ case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING,
+ IGNORED_ERROR_SCHEDULING, IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES,
+ FINISHED_UNEXPECTED, RUNNING
+ -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
+ };
+ }
}
/**
@@ -499,7 +584,7 @@
@Override
public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
if (mStartTime > 0) {
- // Only log sessions that have started.
+ // Only log sessions that have started in the HAL.
statsLogger.logVibrationVendorSessionStarted(mCallerInfo.uid);
statsLogger.logVibrationVendorSessionVibrations(mCallerInfo.uid,
mVibrations.size());
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index dcc3cf7..57b82c3 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -344,7 +344,6 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ResolverActivity;
import com.android.internal.content.ReferrerIntent;
@@ -854,8 +853,6 @@
private RemoteCallbackList<IScreenCaptureObserver> mCaptureCallbacks;
- // Ensure the field is kept during optimization to preserve downstream weak refs.
- @KeepForWeakReference
private final ColorDisplayService.ColorTransformController mColorTransformController =
(matrix, translation) -> mWmService.mH.post(() -> {
synchronized (mWmService.mGlobalLock) {
@@ -2329,13 +2326,16 @@
if (isActivityTypeHome()) {
// The snapshot of home is only used once because it won't be updated while screen
// is on (see {@link TaskSnapshotController#screenTurningOff}).
- mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
final Transition transition = mTransitionController.getCollectingTransition();
if (transition != null && (transition.getFlags()
& WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) {
+ mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
// Only use snapshot of home as starting window when unlocking directly.
return false;
}
+ // Add a reference before removing snapshot from cache.
+ snapshot.addReference(TaskSnapshot.REFERENCE_WRITE_TO_PARCEL);
+ mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
}
return createSnapshot(snapshot, typeParameter);
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 119709e..f51e60c 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -279,16 +279,24 @@
mSupervisor = supervisor;
}
+ private ActivityTaskManagerService getService() {
+ return mService;
+ }
+
+ private ActivityTaskSupervisor getSupervisor() {
+ return mSupervisor;
+ }
+
private boolean isHomeApp(int uid, @Nullable String packageName) {
- if (mService.mHomeProcess != null) {
+ if (getService().mHomeProcess != null) {
// Fast check
- return uid == mService.mHomeProcess.mUid;
+ return uid == getService().mHomeProcess.mUid;
}
if (packageName == null) {
return false;
}
ComponentName activity =
- mService.getPackageManagerInternalLocked()
+ getService().getPackageManagerInternalLocked()
.getDefaultHomeActivity(UserHandle.getUserId(uid));
return activity != null && packageName.equals(activity.getPackageName());
}
@@ -342,7 +350,8 @@
mAllowBalExemptionForSystemProcess = allowBalExemptionForSystemProcess;
mOriginatingPendingIntent = originatingPendingIntent;
mIntent = intent;
- mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+ mRealCallingPackage = getService().getPackageNameIfUnique(realCallingUid,
+ realCallingPid);
mIsCallForResult = resultRecord != null;
mCheckedOptions = checkedOptions;
@BackgroundActivityStartMode int callerBackgroundActivityStartMode =
@@ -401,13 +410,13 @@
checkedOptions, realCallingUid, mRealCallingPackage);
}
- mAppSwitchState = mService.getBalAppSwitchesState();
- mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
+ mAppSwitchState = getService().getBalAppSwitchesState();
+ mCallingUidProcState = getService().mActiveUids.getUidState(callingUid);
mIsCallingUidPersistentSystemProcess =
mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
mCallingUidHasVisibleActivity =
- mService.mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
- mCallingUidHasNonAppVisibleWindow = mService.mActiveUids.hasNonAppVisibleWindow(
+ getService().mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
+ mCallingUidHasNonAppVisibleWindow = getService().mActiveUids.hasNonAppVisibleWindow(
callingUid);
if (realCallingUid == NO_PROCESS_UID) {
// no process provided
@@ -422,16 +431,17 @@
mRealCallingUidHasNonAppVisibleWindow = mCallingUidHasNonAppVisibleWindow;
// In the PendingIntent case callerApp is not passed in, so resolve it ourselves.
mRealCallerApp = callerApp == null
- ? mService.getProcessController(realCallingPid, realCallingUid)
+ ? getService().getProcessController(realCallingPid, realCallingUid)
: callerApp;
mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess;
} else {
- mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid);
+ mRealCallingUidProcState = getService().mActiveUids.getUidState(realCallingUid);
mRealCallingUidHasVisibleActivity =
- mService.mVisibleActivityProcessTracker.hasVisibleActivity(realCallingUid);
+ getService().mVisibleActivityProcessTracker.hasVisibleActivity(
+ realCallingUid);
mRealCallingUidHasNonAppVisibleWindow =
- mService.mActiveUids.hasNonAppVisibleWindow(realCallingUid);
- mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid);
+ getService().mActiveUids.hasNonAppVisibleWindow(realCallingUid);
+ mRealCallerApp = getService().getProcessController(realCallingPid, realCallingUid);
mIsRealCallingUidPersistentSystemProcess =
mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
}
@@ -481,7 +491,7 @@
if (uid == 0) {
return "root[debugOnly]";
}
- String name = mService.getPackageManagerInternalLocked().getNameForUid(uid);
+ String name = getService().getPackageManagerInternalLocked().getNameForUid(uid);
if (name == null) {
name = "uid=" + uid;
}
@@ -783,7 +793,7 @@
Process.getAppUidForSdkSandboxUid(state.mRealCallingUid);
// realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition
// to realCallingUid when calculating resultForRealCaller below.
- if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
+ if (getService().hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX,
/*background*/ false,
"uid in SDK sandbox has visible (non-toast) window"));
@@ -1000,37 +1010,45 @@
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
+ boolean evaluateVisibleOnly = balAdditionalStartModes()
+ && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+ if (evaluateVisibleOnly) {
+ return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+ mCheckCallerProcessAllowsForeground);
+ }
if (state.isPendingIntent()) {
// PendingIntents should mostly be allowed by the sender (real caller) or a permission
// the creator of the PendingIntent has. Visibility should be the exceptional case, so
// test it last (this does not change the result, just the bal code).
- BalVerdict result = BalVerdict.BLOCK;
- if (!(balAdditionalStartModes()
- && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
- }
- if (result == BalVerdict.BLOCK) {
- result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
-
- }
- return result;
- } else {
- BalVerdict result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
- if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
- && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
- }
- return result;
+ return evaluateChain(state, mCheckCallerIsAllowlistedUid,
+ mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+ mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+ mCheckCallerProcessAllowsBackground, mCheckCallerVisible,
+ mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground);
}
+ return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+ mCheckCallerProcessAllowsForeground, mCheckCallerIsAllowlistedUid,
+ mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+ mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+ mCheckCallerProcessAllowsBackground);
}
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByCallerInForeground(BalState state) {
+ interface BalExemptionCheck {
+ BalVerdict evaluate(BalState state);
+ }
+
+ private BalVerdict evaluateChain(BalState state, BalExemptionCheck... checks) {
+ for (BalExemptionCheck check : checks) {
+ BalVerdict verdict = check.evaluate(state);
+ if (verdict != BalVerdict.BLOCK) {
+ return verdict;
+ }
+ }
+ return BalVerdict.BLOCK;
+ }
+
+ private final BalExemptionCheck mCheckCallerVisible = state -> {
// This is used to block background activity launch even if the app is still
// visible to user after user clicking home button.
@@ -1044,21 +1062,19 @@
return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
/*background*/ false, "callingUid has visible window");
}
+ return BalVerdict.BLOCK;
+ };
+
+ private final BalExemptionCheck mCheckCallerNonAppVisible = state -> {
if (state.mCallingUidHasNonAppVisibleWindow) {
return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
/*background*/ false, "callingUid has non-app visible window "
- + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
+ + getService().mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
}
- // Don't abort if the callerApp or other processes of that uid are considered to be in the
- // foreground.
- return checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_FOREGROUND);
- }
+ return BalVerdict.BLOCK;
+ };
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByCallerInBackground(BalState state) {
+ private final BalExemptionCheck mCheckCallerIsAllowlistedUid = state -> {
// don't abort for the most important UIDs
final int callingAppId = UserHandle.getAppId(state.mCallingUid);
if (state.mCallingUid == Process.ROOT_UID
@@ -1066,9 +1082,12 @@
|| callingAppId == Process.NFC_UID) {
return new BalVerdict(
BAL_ALLOW_ALLOWLISTED_UID, /*background*/ false,
- "Important callingUid");
+ "Important callingUid");
}
+ return BalVerdict.BLOCK;
+ };
+ private final BalExemptionCheck mCheckCallerIsAllowlistedComponent = state -> {
// Always allow home application to start activities.
if (isHomeApp(state.mCallingUid, state.mCallingPackage)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
@@ -1076,9 +1095,10 @@
"Home app");
}
+ final int callingAppId = UserHandle.getAppId(state.mCallingUid);
// IME should always be allowed to start activity, like IME settings.
final WindowState imeWindow =
- mService.mRootWindowContainer.getCurrentInputMethodWindow();
+ getService().mRootWindowContainer.getCurrentInputMethodWindow();
if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
@@ -1091,71 +1111,88 @@
/*background*/ false, "callingUid is persistent system process");
}
+ // don't abort if the caller has the same uid as the recents component
+ if (getSupervisor().mRecentTasks.isCallerRecents(state.mCallingUid)) {
+ return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+ /*background*/ true, "Recents Component");
+ }
+ // don't abort if the callingUid is the device owner
+ if (getService().isDeviceOwner(state.mCallingUid)) {
+ return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+ /*background*/ true, "Device Owner");
+ }
+ // don't abort if the callingUid is a affiliated profile owner
+ if (getService().isAffiliatedProfileOwner(state.mCallingUid)) {
+ return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+ /*background*/ true, "Affiliated Profile Owner");
+ }
+ // don't abort if the callingUid has companion device
+ final int callingUserId = UserHandle.getUserId(state.mCallingUid);
+ if (getService().isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
+ return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
+ /*background*/ true, "Companion App");
+ }
+ return BalVerdict.BLOCK;
+ };
+
+ private final BalExemptionCheck mCheckCallerHasBackgroundPermission = state -> {
// don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
if (hasBalPermission(state.mCallingUid, state.mCallingPid)) {
return new BalVerdict(BAL_ALLOW_PERMISSION,
/*background*/ true,
"START_ACTIVITIES_FROM_BACKGROUND permission granted");
}
- // don't abort if the caller has the same uid as the recents component
- if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
- return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
- /*background*/ true, "Recents Component");
- }
- // don't abort if the callingUid is the device owner
- if (mService.isDeviceOwner(state.mCallingUid)) {
- return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
- /*background*/ true, "Device Owner");
- }
- // don't abort if the callingUid is a affiliated profile owner
- if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
- return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
- /*background*/ true, "Affiliated Profile Owner");
- }
- // don't abort if the callingUid has companion device
- final int callingUserId = UserHandle.getUserId(state.mCallingUid);
- if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
- return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
- /*background*/ true, "Companion App");
- }
+ return BalVerdict.BLOCK;
+ };
+ private final BalExemptionCheck mCheckCallerHasSawPermission = state -> {
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
- if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+ if (getService().hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
state.mCallingPackage)) {
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
}
+ return BalVerdict.BLOCK;
+ };
+ private final BalExemptionCheck mCheckCallerHasBgStartAppOp = state -> {
// don't abort if the callingUid and callingPackage have the
// OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
- if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
+ if (isSystemExemptFlagEnabled() && getService().getAppOpsManager().checkOpNoThrow(
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
"OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
}
+ return BalVerdict.BLOCK;
+ };
- // Don't abort if the callerApp or other processes of that uid are allowed in any way.
- return checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_BACKGROUND);
- }
+
+ // Don't abort if the callerApp or other processes of that uid are considered to be in the
+ // foreground.
+ private final BalExemptionCheck mCheckCallerProcessAllowsForeground =
+ state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_FOREGROUND);
+ // Don't abort if the callerApp or other processes of that uid are allowed in any way.
+ private final BalExemptionCheck mCheckCallerProcessAllowsBackground =
+ state -> checkProcessAllowsBal(state.mCallerApp, state, BAL_CHECK_BACKGROUND);
/**
* @return A code denoting which BAL rule allows an activity to be started,
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) {
- BalVerdict result = checkBackgroundActivityStartAllowedByRealCallerInForeground(state);
- if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
+ boolean evaluateVisibleOnly = balAdditionalStartModes()
&& state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByRealCallerInBackground(state);
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+ if (evaluateVisibleOnly) {
+ return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+ mCheckRealCallerProcessAllowsBalForeground);
}
- return result;
+ return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+ mCheckRealCallerProcessAllowsBalForeground, mCheckRealCallerBalPermission,
+ mCheckRealCallerSawPermission, mCheckRealCallerAllowlistedUid,
+ mCheckRealCallerAllowlistedComponent, mCheckRealCallerProcessAllowsBalBackground);
}
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByRealCallerInForeground(BalState state) {
+ private final BalExemptionCheck mCheckRealCallerVisible = state -> {
// Normal apps with visible app window will be allowed to start activity if app switching
// is allowed, or apps like live wallpaper with non app visible window will be allowed.
// The home app can start apps even if app switches are usually disallowed.
@@ -1166,22 +1203,29 @@
return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
/*background*/ false, "realCallingUid has visible window");
}
+ return BalVerdict.BLOCK;
+ };
+
+ private final BalExemptionCheck mCheckRealCallerNonAppVisible = state -> {
if (state.mRealCallingUidHasNonAppVisibleWindow) {
return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
/*background*/ false, "realCallingUid has non-app visible window "
- + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mRealCallingUid));
+ + getService().mActiveUids.getNonAppVisibleWindowDetails(
+ state.mRealCallingUid));
}
+ return BalVerdict.BLOCK;
+ };
- // Don't abort if the realCallerApp or other processes of that uid are considered to be in
- // the foreground.
- return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
- }
+ // Don't abort if the realCallerApp or other processes of that uid are considered to be in
+ // the foreground.
+ private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground =
+ state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) {
+ // don't abort if the callerApp or other processes of that uid are allowed in any way
+ private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground =
+ state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
+
+ private final BalExemptionCheck mCheckRealCallerBalPermission = state -> {
boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
== MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
if (allowAlways
@@ -1190,15 +1234,25 @@
/*background*/ false,
"realCallingUid has BAL permission.");
}
+ return BalVerdict.BLOCK;
+ };
+ private final BalExemptionCheck mCheckRealCallerSawPermission = state -> {
+ boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
// don't abort if the realCallingUid has SYSTEM_ALERT_WINDOW permission
if (allowAlways
- && mService.hasSystemAlertWindowPermission(state.mRealCallingUid,
+ && getService().hasSystemAlertWindowPermission(state.mRealCallingUid,
state.mRealCallingPid, state.mRealCallingPackage)) {
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
}
+ return BalVerdict.BLOCK;
+ };
+ private final BalExemptionCheck mCheckRealCallerAllowlistedUid = state -> {
+ boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
// if the realCallingUid is a persistent system process, abort if the IntentSender
// wasn't allowed to start an activity
if ((allowAlways || state.mAllowBalExemptionForSystemProcess)
@@ -1208,17 +1262,19 @@
"realCallingUid is persistent system process AND intent "
+ "sender forced to allow.");
}
+ return BalVerdict.BLOCK;
+ };
+
+ private final BalExemptionCheck mCheckRealCallerAllowlistedComponent = state -> {
// don't abort if the realCallingUid is an associated companion app
- if (mService.isAssociatedCompanionApp(
+ if (getService().isAssociatedCompanionApp(
UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
"realCallingUid is a companion app.");
}
-
- // don't abort if the callerApp or other processes of that uid are allowed in any way
- return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
- }
+ return BalVerdict.BLOCK;
+ };
@VisibleForTesting boolean hasBalPermission(int uid, int pid) {
return ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
@@ -1245,7 +1301,7 @@
} else {
// only if that one wasn't allowed, check the other ones
final ArraySet<WindowProcessController> uidProcesses =
- mService.mProcessMap.getProcesses(app.mUid);
+ getService().mProcessMap.getProcesses(app.mUid);
if (uidProcesses != null) {
for (int i = uidProcesses.size() - 1; i >= 0; i--) {
final WindowProcessController proc = uidProcesses.valueAt(i);
@@ -1416,7 +1472,7 @@
if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) {
String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
+ (enforceBlock ? " blocked " : " would block ")
- + getApplicationLabel(mService.mContext.getPackageManager(),
+ + getApplicationLabel(getService().mContext.getPackageManager(),
launchedFromPackageName);
showToast(toastText);
@@ -1438,7 +1494,7 @@
}
@VisibleForTesting void showToast(String toastText) {
- UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
+ UiThread.getHandler().post(() -> Toast.makeText(getService().mContext,
toastText, Toast.LENGTH_LONG).show());
}
@@ -1515,7 +1571,7 @@
return;
}
- String packageName = mService.mContext.getPackageManager().getNameForUid(callingUid);
+ String packageName = getService().mContext.getPackageManager().getNameForUid(callingUid);
BalState state = new BalState(callingUid, callingPid, packageName, INVALID_UID,
INVALID_PID, null, null, false, null, null, ActivityOptions.makeBasic());
@BalCode int balCode = checkBackgroundActivityStartAllowedByCaller(state).mCode;
@@ -1576,7 +1632,7 @@
boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
.shouldRestrictActivitySwitch(callingUid) && bas.mTopActivityOptedIn;
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
String callingPackage = pm.getNameForUid(callingUid);
final CharSequence callingLabel;
if (callingPackage == null) {
@@ -1737,7 +1793,7 @@
return bas.optedIn(ar);
}
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
ApplicationInfo applicationInfo;
final int sourceUserId = UserHandle.getUserId(sourceUid);
@@ -1794,7 +1850,7 @@
if (sourceRecord == null) {
joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage);
- String realCallingPackage = mService.mContext.getPackageManager().getNameForUid(
+ String realCallingPackage = getService().mContext.getPackageManager().getNameForUid(
realCallingUid);
joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
} else {
@@ -1829,7 +1885,7 @@
joiner.add(prefix + "BalCode: " + balCodeToString(balCode));
joiner.add(prefix + "Allowed By Grace Period: " + allowedByGracePeriod);
joiner.add(prefix + "LastResumedActivity: "
- + recordToString.apply(mService.mLastResumedActivity));
+ + recordToString.apply(getService().mLastResumedActivity));
joiner.add(prefix + "System opted into enforcement: " + asmOptSystemIntoEnforcement());
if (mTopFinishedActivity != null) {
@@ -1902,7 +1958,7 @@
}
private BalVerdict statsLog(BalVerdict finalVerdict, BalState state) {
- if (finalVerdict.blocks() && mService.isActivityStartsLoggingEnabled()) {
+ if (finalVerdict.blocks() && getService().isActivityStartsLoggingEnabled()) {
// log aborted activity start to TRON
mSupervisor
.getActivityMetricsLogger()
@@ -2138,7 +2194,7 @@
return -1;
}
try {
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
return pm.getTargetSdkVersion(packageName);
} catch (Exception e) {
return -1;
@@ -2159,8 +2215,8 @@
this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1;
this.mDebugInfo = getDebugStringForActivityRecord(ar);
- mService.mH.postDelayed(() -> {
- synchronized (mService.mGlobalLock) {
+ getService().mH.postDelayed(() -> {
+ synchronized (getService().mGlobalLock) {
if (mTaskIdToFinishedActivity.get(taskId) == this) {
mTaskIdToFinishedActivity.remove(taskId);
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index c2255d8..dc42b32 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -79,7 +79,7 @@
}
@VisibleForTesting
- static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
if (!shouldEnforceDeviceRestrictions()) {
return true;
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a874ef6..50f12c3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -157,6 +157,7 @@
import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
import static com.android.window.flags.Flags.enablePersistingDensityScaleForConnectedDisplays;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -3835,13 +3836,18 @@
/**
* Looking for the focused window on this display if the top focused display hasn't been
- * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed.
+ * found yet (topFocusedDisplayId is INVALID_DISPLAY), per-display focused was allowed, or
+ * the display is presenting. The last one is needed to update system bar visibility in response
+ * to presentation visibility because per-display focus is needed to change system bar
+ * visibility, but the display shouldn't get global focus when a presentation gets shown.
*
* @param topFocusedDisplayId Id of the top focused display.
* @return The focused window or null if there isn't any or no need to seek.
*/
WindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) {
- return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY)
+ return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY
+ || (enablePresentationForConnectedDisplays()
+ && mWmService.mPresentationController.isPresentationVisible(mDisplayId)))
? findFocusedWindow() : null;
}
@@ -6932,6 +6938,8 @@
/** The actual requested visible inset types for this display */
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ private @InsetsType int mAnimatingTypes = 0;
+
/** The component name of the top focused window on this display */
private ComponentName mTopFocusedComponentName = null;
@@ -7069,6 +7077,18 @@
}
return 0;
}
+
+ @Override
+ public @InsetsType int getAnimatingTypes() {
+ return mAnimatingTypes;
+ }
+
+ @Override
+ public void setAnimatingTypes(@InsetsType int animatingTypes) {
+ if (mAnimatingTypes != animatingTypes) {
+ mAnimatingTypes = animatingTypes;
+ }
+ }
}
MagnificationSpec getMagnificationSpec() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 4908df0..ec5b503f 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2564,7 +2564,7 @@
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
// TODO(b/277290737): Move this to the client side, instead of using a proxy.
callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(getDisplayId(),
- rootDisplayAreaId, isImmersiveMode));
+ rootDisplayAreaId, isImmersiveMode, win.getWindowType()));
}
// Show transient bars for panic if needed.
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index cee4967..6462a37 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -97,6 +97,20 @@
@NonNull ImeTracker.Token statsToken) {
}
+ /**
+ * @return {@link WindowInsets.Type.InsetsType}s which are currently animating (showing or
+ * hiding).
+ */
+ default @InsetsType int getAnimatingTypes() {
+ return 0;
+ }
+
+ /**
+ * @param animatingTypes the {@link InsetsType}s, that are currently animating
+ */
+ default void setAnimatingTypes(@InsetsType int animatingTypes) {
+ }
+
/** Returns {@code target.getWindow()}, or null if {@code target} is {@code null}. */
static WindowState asWindowOrNull(InsetsControlTarget target) {
return target != null ? target.getWindow() : null;
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 009d482..2872214 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -790,8 +790,6 @@
private final Handler mHandler;
private final String mName;
- private boolean mInsetsAnimationRunning;
-
Host(Handler handler, String name) {
mHandler = handler;
mName = name;
@@ -901,10 +899,5 @@
public IBinder getWindowToken() {
return null;
}
-
- @Override
- public void notifyAnimationRunningStateChanged(boolean running) {
- mInsetsAnimationRunning = running;
- }
}
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 164abab..5e0395f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -225,13 +225,16 @@
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
final @InsetsType int type = provider.getSource().getType();
+ final boolean isImeProvider = type == WindowInsets.Type.ime();
if ((type & changedTypes) != 0) {
- final boolean isImeProvider = type == WindowInsets.Type.ime();
changed |= provider.updateClientVisibility(
- caller, isImeProvider ? statsToken : null)
+ caller, isImeProvider ? statsToken : null)
// Fake control target cannot change the client visibility, but it should
// change the insets with its newly requested visibility.
|| (caller == provider.getFakeControlTarget());
+ } else if (isImeProvider && android.view.inputmethod.Flags.refactorInsetsController()) {
+ ImeTracker.forLogging().onCancelled(statsToken,
+ ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
}
}
if (changed) {
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java
index b3cff9c..acc658b 100644
--- a/services/core/java/com/android/server/wm/PresentationController.java
+++ b/services/core/java/com/android/server/wm/PresentationController.java
@@ -16,10 +16,17 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+
+import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import android.annotation.NonNull;
-import android.util.IntArray;
+import android.annotation.Nullable;
+import android.hardware.display.DisplayManager;
+import android.util.SparseArray;
+import android.view.WindowManager.LayoutParams.WindowType;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
@@ -27,15 +34,125 @@
/**
* Manages presentation windows.
*/
-class PresentationController {
+class PresentationController implements DisplayManager.DisplayListener {
- // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
- private final IntArray mPresentingDisplayIds = new IntArray();
+ private static class Presentation {
+ @NonNull final WindowState mWin;
+ @NonNull final WindowContainerListener mPresentationListener;
+ // This is the task which started this presentation. This shouldn't be null in most cases
+ // because the intended usage of the Presentation API is that an activity that started a
+ // presentation should control the UI and lifecycle of the presentation window.
+ // However, the API doesn't necessarily requires a host activity to exist (e.g. a background
+ // service can launch a presentation), so this can be null.
+ @Nullable final Task mHostTask;
+ @Nullable final WindowContainerListener mHostTaskListener;
- PresentationController() {}
+ Presentation(@NonNull WindowState win,
+ @NonNull WindowContainerListener presentationListener,
+ @Nullable Task hostTask,
+ @Nullable WindowContainerListener hostTaskListener) {
+ mWin = win;
+ mPresentationListener = presentationListener;
+ mHostTask = hostTask;
+ mHostTaskListener = hostTaskListener;
+ }
- private boolean isPresenting(int displayId) {
- return mPresentingDisplayIds.contains(displayId);
+ @Override
+ public String toString() {
+ return "{win: " + mWin.getName() + ", display: " + mWin.getDisplayId()
+ + ", hostTask: " + (mHostTask != null ? mHostTask.getName() : null) + "}";
+ }
+ }
+
+ private final SparseArray<Presentation> mPresentations = new SparseArray();
+
+ @Nullable
+ private Presentation getPresentation(@Nullable WindowState win) {
+ if (win == null) return null;
+ for (int i = 0; i < mPresentations.size(); i++) {
+ final Presentation presentation = mPresentations.valueAt(i);
+ if (win == presentation.mWin) return presentation;
+ }
+ return null;
+ }
+
+ private boolean hasPresentationWindow(int displayId) {
+ return mPresentations.contains(displayId);
+ }
+
+ boolean isPresentationVisible(int displayId) {
+ final Presentation presentation = mPresentations.get(displayId);
+ return presentation != null && presentation.mWin.mToken.isVisibleRequested();
+ }
+
+ boolean canPresent(@NonNull WindowState win, @NonNull DisplayContent displayContent) {
+ return canPresent(win, displayContent, win.mAttrs.type, win.getUid());
+ }
+
+ /**
+ * Checks if a presentation window can be shown on the given display.
+ * If the given |win| is empty, a new presentation window is being created.
+ * If the given |win| is not empty, the window already exists as presentation, and we're
+ * revalidate if the |win| is still qualified to be shown.
+ */
+ boolean canPresent(@Nullable WindowState win, @NonNull DisplayContent displayContent,
+ @WindowType int type, int uid) {
+ if (type == TYPE_PRIVATE_PRESENTATION) {
+ // Private presentations can only be created on private displays.
+ return displayContent.isPrivate();
+ }
+
+ if (type != TYPE_PRESENTATION) {
+ return false;
+ }
+
+ if (!enablePresentationForConnectedDisplays()) {
+ return displayContent.getDisplay().isPublicPresentation();
+ }
+
+ boolean allDisplaysArePresenting = true;
+ for (int i = 0; i < displayContent.mWmService.mRoot.mChildren.size(); i++) {
+ final DisplayContent dc = displayContent.mWmService.mRoot.mChildren.get(i);
+ if (displayContent.mDisplayId != dc.mDisplayId
+ && !mPresentations.contains(dc.mDisplayId)) {
+ allDisplaysArePresenting = false;
+ break;
+ }
+ }
+ if (allDisplaysArePresenting) {
+ // All displays can't present simultaneously.
+ return false;
+ }
+
+ final int displayId = displayContent.mDisplayId;
+ if (hasPresentationWindow(displayId)
+ && win != null && win != mPresentations.get(displayId).mWin) {
+ // A display can't have multiple presentations.
+ return false;
+ }
+
+ Task hostTask = null;
+ final Presentation presentation = getPresentation(win);
+ if (presentation != null) {
+ hostTask = presentation.mHostTask;
+ } else if (win == null) {
+ final Task globallyFocusedTask =
+ displayContent.mWmService.mRoot.getTopDisplayFocusedRootTask();
+ if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+ hostTask = globallyFocusedTask;
+ }
+ }
+ if (hostTask != null && displayId == hostTask.getDisplayId()) {
+ // A presentation can't cover its own host task.
+ return false;
+ }
+ if (hostTask == null && !displayContent.getDisplay().isPublicPresentation()) {
+ // A globally focused host task on a different display is needed to show a
+ // presentation on a non-presenting display.
+ return false;
+ }
+
+ return true;
}
boolean shouldOccludeActivities(int displayId) {
@@ -45,32 +162,87 @@
// be shown on them.
// TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
// the presentation won't stop its controlling activity.
- return enablePresentationForConnectedDisplays() && isPresenting(displayId);
+ return enablePresentationForConnectedDisplays() && isPresentationVisible(displayId);
}
- void onPresentationAdded(@NonNull WindowState win) {
+ void onPresentationAdded(@NonNull WindowState win, int uid) {
final int displayId = win.getDisplayId();
- if (isPresenting(displayId)) {
- return;
- }
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
- win.getDisplayId(), win);
- mPresentingDisplayIds.add(win.getDisplayId());
+ displayId, win);
win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
+
+ final WindowContainerListener presentationWindowListener = new WindowContainerListener() {
+ @Override
+ public void onRemoved() {
+ if (!hasPresentationWindow(displayId)) {
+ ProtoLog.e(WM_ERROR, "Failed to remove presentation on"
+ + "non-presenting display %d: %s", displayId, win);
+ return;
+ }
+ final Presentation presentation = mPresentations.get(displayId);
+ win.mToken.unregisterWindowContainerListener(presentation.mPresentationListener);
+ if (presentation.mHostTask != null) {
+ presentation.mHostTask.unregisterWindowContainerListener(
+ presentation.mHostTaskListener);
+ }
+ mPresentations.remove(displayId);
+ win.mWmService.mDisplayManagerInternal.onPresentation(displayId, false /*isShown*/);
+ }
+ };
+ win.mToken.registerWindowContainerListener(presentationWindowListener);
+
+ Task hostTask = null;
+ if (enablePresentationForConnectedDisplays()) {
+ final Task globallyFocusedTask =
+ win.mWmService.mRoot.getTopDisplayFocusedRootTask();
+ if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+ hostTask = globallyFocusedTask;
+ }
+ }
+ WindowContainerListener hostTaskListener = null;
+ if (hostTask != null) {
+ hostTaskListener = new WindowContainerListener() {
+ public void onDisplayChanged(DisplayContent dc) {
+ final Presentation presentation = mPresentations.get(dc.getDisplayId());
+ if (presentation != null && !canPresent(presentation.mWin, dc)) {
+ removePresentation(dc.mDisplayId, "host task moved to display "
+ + dc.getDisplayId());
+ }
+ }
+
+ public void onRemoved() {
+ removePresentation(win.getDisplayId(), "host task removed");
+ }
+ };
+ hostTask.registerWindowContainerListener(hostTaskListener);
+ }
+
+ mPresentations.put(displayId, new Presentation(win, presentationWindowListener, hostTask,
+ hostTaskListener));
}
- void onPresentationRemoved(@NonNull WindowState win) {
- final int displayId = win.getDisplayId();
- if (!isPresenting(displayId)) {
- return;
+ void removePresentation(int displayId, @NonNull String reason) {
+ final Presentation presentation = mPresentations.get(displayId);
+ if (enablePresentationForConnectedDisplays() && presentation != null) {
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Removing Presentation %s for "
+ + "reason %s", mPresentations.get(displayId), reason);
+ final WindowState win = presentation.mWin;
+ win.mWmService.mAtmService.mH.post(() -> {
+ synchronized (win.mWmService.mGlobalLock) {
+ win.removeIfPossible();
+ }
+ });
}
- ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
- "Presentation removed from display %d: %s", win.getDisplayId(), win);
- // TODO(b/393945496): Make sure that there's one presentation at most per display.
- final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
- if (displayIdIndex != -1) {
- mPresentingDisplayIds.remove(displayIdIndex);
- }
- win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
}
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ removePresentation(displayId, "display removed " + displayId);
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {}
}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8d198b2..3ed16db 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -737,6 +737,17 @@
}
}
+ @Override
+ public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+ synchronized (mService.mGlobalLock) {
+ final WindowState win = mService.windowForClientLocked(this, window,
+ false /* throwOnError */);
+ if (win != null) {
+ win.setAnimatingTypes(animatingTypes);
+ }
+ }
+ }
+
void onWindowAdded(WindowState w) {
if (mPackageName == null) {
mPackageName = mProcess.mInfo.packageName;
@@ -1015,15 +1026,4 @@
}
}
}
-
- @Override
- public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
- synchronized (mService.mGlobalLock) {
- final WindowState win = mService.windowForClientLocked(this, window,
- false /* throwOnError */);
- if (win != null) {
- win.notifyInsetsAnimationRunningStateChanged(running);
- }
- }
- }
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 821c040..28f2825 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1583,14 +1583,18 @@
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
- if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
+ if (type == TYPE_PRIVATE_PRESENTATION
+ && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+ callingUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add private presentation window to a non-private display. "
+ "Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
- if (type == TYPE_PRESENTATION && !displayContent.getDisplay().isPublicPresentation()) {
+ if (type == TYPE_PRESENTATION
+ && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+ callingUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add presentation window to a non-suitable display. "
+ "Aborting.");
@@ -1830,7 +1834,8 @@
}
win.mTransitionController.collect(win.mToken);
res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
- outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+ outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+ callingUid);
// A presentation hides all activities behind on the same display.
win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
/*notifyClients=*/ true);
@@ -1841,7 +1846,8 @@
}
} else {
res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
- outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+ outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+ callingUid);
}
}
@@ -1854,7 +1860,7 @@
@NonNull ActivityRecord activity, @NonNull DisplayContent displayContent,
@NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame,
@NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client,
- @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) {
+ @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs, int uid) {
int res = 0;
final int type = attrs.type;
boolean imMayMove = true;
@@ -1971,7 +1977,7 @@
outSizeCompatScale[0] = win.getCompatScaleForClient();
if (res >= ADD_OKAY && win.isPresentation()) {
- mPresentationController.onPresentationAdded(win);
+ mPresentationController.onPresentationAdded(win, uid);
}
return res;
@@ -4767,6 +4773,26 @@
}
}
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @Override
+ public void updateDisplayWindowAnimatingTypes(int displayId, @InsetsType int animatingTypes) {
+ updateDisplayWindowAnimatingTypes_enforcePermission();
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ final DisplayContent dc = mRoot.getDisplayContent(displayId);
+ if (dc == null || dc.mRemoteInsetsControlTarget == null) {
+ return;
+ }
+ dc.mRemoteInsetsControlTarget.setAnimatingTypes(animatingTypes);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
@Override
public int watchRotation(IRotationWatcher watcher, int displayId) {
final DisplayContent displayContent;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ec67dd87..3b7d312 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -736,6 +736,8 @@
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ private @InsetsType int mAnimatingTypes = 0;
+
/**
* Freeze the insets state in some cases that not necessarily keeps up-to-date to the client.
* (e.g app exiting transition)
@@ -842,6 +844,27 @@
mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
}
+ @Override
+ public @InsetsType int getAnimatingTypes() {
+ return mAnimatingTypes;
+ }
+
+ @Override
+ public void setAnimatingTypes(@InsetsType int animatingTypes) {
+ if (mAnimatingTypes != animatingTypes) {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ Trace.instant(TRACE_TAG_WINDOW_MANAGER,
+ TextUtils.formatSimple("%s: setAnimatingTypes(%s)",
+ getName(),
+ animatingTypes));
+ }
+ mInsetsAnimationRunning = animatingTypes != 0;
+ mWmService.scheduleAnimationLocked();
+
+ mAnimatingTypes = animatingTypes;
+ }
+ }
+
/**
* Set a freeze state for the window to ignore dispatching its insets state to the client.
*
@@ -2435,7 +2458,6 @@
mAnimatingExit = true;
mRemoveOnExit = true;
mToken.setVisibleRequested(false);
- mWmService.mPresentationController.onPresentationRemoved(this);
// A presentation hides all activities behind on the same display.
mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
/*notifyClients=*/ true);
@@ -2656,7 +2678,7 @@
// The client gave us a touchable region and so first
// we calculate the untouchable region, then punch that out of our
// expanded modal region.
- mTmpRegion.set(0, 0, frame.right, frame.bottom);
+ mTmpRegion.set(0, 0, frame.width(), frame.height());
mTmpRegion.op(mGivenTouchableRegion, Region.Op.DIFFERENCE);
region.op(mTmpRegion, Region.Op.DIFFERENCE);
}
@@ -6079,17 +6101,6 @@
mWmService.scheduleAnimationLocked();
}
- void notifyInsetsAnimationRunningStateChanged(boolean running) {
- if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
- Trace.instant(TRACE_TAG_WINDOW_MANAGER,
- TextUtils.formatSimple("%s: notifyInsetsAnimationRunningStateChanged(%s)",
- getName(),
- Boolean.toString(running)));
- }
- mInsetsAnimationRunning = running;
- mWmService.scheduleAnimationLocked();
- }
-
boolean isInsetsAnimationRunning() {
return mInsetsAnimationRunning;
}
diff --git a/services/core/jni/BroadcastRadio/OWNERS b/services/core/jni/BroadcastRadio/OWNERS
index ea4421e..a993823 100644
--- a/services/core/jni/BroadcastRadio/OWNERS
+++ b/services/core/jni/BroadcastRadio/OWNERS
@@ -1,2 +1 @@
twasilczyk@google.com
-randolphs@google.com
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e158310..860b6fb 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1814,7 +1814,7 @@
t.traceEnd();
}
- if (!isWatch && !isTv && !isAutomotive
+ if (!isWatch && !isTv && !isAutomotive && !isDesktop
&& android.security.Flags.aapmApi()) {
t.traceBegin("StartAdvancedProtectionService");
mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index 5eb23a2..1286648 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -16,29 +16,43 @@
package com.android.server.am;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.BackgroundStartPrivileges;
+import android.app.BroadcastOptions;
+import android.app.SystemServiceRegistry;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.TestLooperManager;
import android.os.UserHandle;
+import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -47,7 +61,6 @@
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.FrameworkStatsLog;
import com.android.modules.utils.testing.ExtendedMockitoRule;
import com.android.server.AlarmManagerInternal;
@@ -55,6 +68,7 @@
import com.android.server.LocalServices;
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
+import com.android.server.firewall.IntentFirewall;
import com.android.server.wm.ActivityTaskManagerService;
import org.junit.Rule;
@@ -63,8 +77,11 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
public abstract class BaseBroadcastQueueTest {
@@ -97,6 +114,8 @@
public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
.spyStatic(FrameworkStatsLog.class)
.spyStatic(ProcessList.class)
+ .spyStatic(SystemServiceRegistry.class)
+ .mockStatic(AppGlobals.class)
.build();
@@ -119,6 +138,16 @@
ProcessList mProcessList;
@Mock
PlatformCompat mPlatformCompat;
+ @Mock
+ IntentFirewall mIntentFirewall;
+ @Mock
+ IPackageManager mIPackageManager;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ IPermissionManager mIPermissionManager;
+ @Mock
+ PermissionManager mPermissionManager;
@Mock
AppStartInfoTracker mAppStartInfoTracker;
@@ -167,22 +196,22 @@
return getUidForPackage(invocation.getArgument(0));
}).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
+ final Context spyContext = spy(mContext);
+ doReturn(mPermissionManager).when(spyContext).getSystemService(PermissionManager.class);
final ActivityManagerService realAms = new ActivityManagerService(
- new TestInjector(mContext), mServiceThreadRule.getThread());
+ new TestInjector(spyContext), mServiceThreadRule.getThread());
realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
realAms.mOomAdjuster.mCachedAppOptimizer = mock(CachedAppOptimizer.class);
realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
- ExtendedMockito.doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
+ doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
realAms.mPackageManagerInt = mPackageManagerInt;
realAms.mUsageStatsService = mUsageStatsManagerInt;
realAms.mProcessesReady = true;
mAms = spy(realAms);
- mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
- doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
- doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
+ mSkipPolicy = createBroadcastSkipPolicy();
doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
@@ -198,6 +227,14 @@
}
}
+ public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+ final BroadcastSkipPolicy skipPolicy = spy(new BroadcastSkipPolicy(mAms));
+ doReturn(null).when(skipPolicy).shouldSkipAtEnqueueMessage(any(), any());
+ doReturn(null).when(skipPolicy).shouldSkipMessage(any(), any());
+ doReturn(false).when(skipPolicy).disallowBackgroundStart(any());
+ return skipPolicy;
+ }
+
static int getUidForPackage(@NonNull String packageName) {
switch (packageName) {
case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
@@ -240,6 +277,11 @@
public BroadcastQueue getBroadcastQueue(ActivityManagerService service) {
return null;
}
+
+ @Override
+ public IntentFirewall getIntentFirewall() {
+ return mIntentFirewall;
+ }
}
abstract String getTag();
@@ -281,24 +323,35 @@
ri.activityInfo.packageName = packageName;
ri.activityInfo.processName = processName;
ri.activityInfo.name = name;
+ ri.activityInfo.exported = true;
ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
return ri;
}
+ // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
+ @SuppressWarnings("GuardedBy")
+ ProcessRecord makeProcessRecord(ApplicationInfo info) {
+ final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
+ r.setPid(mNextPid.incrementAndGet());
+ ProcessRecord.updateProcessRecordNodes(r);
+ return r;
+ }
+
BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
return makeRegisteredReceiver(app, 0);
}
BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
- return makeRegisteredReceiver(receiverList, priority);
+ return makeRegisteredReceiver(receiverList, priority, null);
}
- static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) {
+ static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority,
+ String requiredPermission) {
final IntentFilter filter = new IntentFilter();
filter.setPriority(priority);
final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
- receiverList.app.info.packageName, null, null, null, receiverList.uid,
+ receiverList.app.info.packageName, null, null, requiredPermission, receiverList.uid,
receiverList.userId, false, false, true, receiverList.app.info,
mock(PlatformCompat.class));
receiverList.add(res);
@@ -313,4 +366,62 @@
ArgumentMatcher<ApplicationInfo> appInfoEquals(int uid) {
return test -> (test.uid == uid);
}
+
+ static final class BroadcastRecordBuilder {
+ private BroadcastQueue mQueue = mock(BroadcastQueue.class);
+ private Intent mIntent = mock(Intent.class);
+ private ProcessRecord mProcessRecord = mock(ProcessRecord.class);
+ private String mCallerPackage;
+ private String mCallerFeatureId;
+ private int mCallingPid;
+ private int mCallingUid;
+ private boolean mCallerInstantApp;
+ private String mResolvedType;
+ private String[] mRequiredPermissions;
+ private String[] mExcludedPermissions;
+ private String[] mExcludedPackages;
+ private int mAppOp;
+ private BroadcastOptions mOptions = BroadcastOptions.makeBasic();
+ private List mReceivers = Collections.emptyList();
+ private ProcessRecord mResultToApp;
+ private IIntentReceiver mResultTo;
+ private int mResultCode = Activity.RESULT_OK;
+ private String mResultData;
+ private Bundle mResultExtras;
+ private boolean mSerialized;
+ private boolean mSticky;
+ private boolean mInitialSticky;
+ private int mUserId = UserHandle.USER_SYSTEM;
+ private BackgroundStartPrivileges mBackgroundStartPrivileges =
+ BackgroundStartPrivileges.NONE;
+ private boolean mTimeoutExempt;
+ private BiFunction<Integer, Bundle, Bundle> mFilterExtrasForReceiver;
+ private int mCallerAppProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
+ private PlatformCompat mPlatformCompat = mock(PlatformCompat.class);
+
+ public BroadcastRecordBuilder setIntent(Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ public BroadcastRecordBuilder setRequiredPermissions(String[] requiredPermissions) {
+ mRequiredPermissions = requiredPermissions;
+ return this;
+ }
+
+ public BroadcastRecordBuilder setAppOp(int appOp) {
+ mAppOp = appOp;
+ return this;
+ }
+
+ public BroadcastRecord build() {
+ return new BroadcastRecord(mQueue, mIntent, mProcessRecord, mCallerPackage,
+ mCallerFeatureId, mCallingPid, mCallingUid, mCallerInstantApp, mResolvedType,
+ mRequiredPermissions, mExcludedPermissions, mExcludedPackages, mAppOp,
+ mOptions, mReceivers, mResultToApp, mResultTo, mResultCode, mResultData,
+ mResultExtras, mSerialized, mSticky, mInitialSticky, mUserId,
+ mBackgroundStartPrivileges, mTimeoutExempt, mFilterExtrasForReceiver,
+ mCallerAppProcState, mPlatformCompat);
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
index 409706b..b32ce49 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
@@ -1803,6 +1803,46 @@
assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
}
+ @SuppressWarnings("GuardedBy")
+ @DisableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+ @Test
+ public void testSkipPolicy_atEnqueueTime_flagDisabled() throws Exception {
+ final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
+ final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED);
+
+ final BroadcastRecord userPresentRecord = makeBroadcastRecord(userPresent,
+ List.of(greenReceiver, redReceiver));
+
+ final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+ final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+ List.of(greenReceiver, redReceiver));
+
+ doAnswer(invocation -> {
+ final BroadcastRecord r = invocation.getArgument(0);
+ final Object o = invocation.getArgument(1);
+ if (userPresent.getAction().equals(r.intent.getAction())
+ && isReceiverEquals(o, greenReceiver)) {
+ return "receiver skipped by test";
+ }
+ return null;
+ }).when(mSkipPolicy).shouldSkipMessage(any(BroadcastRecord.class), any());
+
+ mImpl.enqueueBroadcastLocked(userPresentRecord);
+ mImpl.enqueueBroadcastLocked(timeTickRecord);
+
+ final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ // There should be only one broadcast for green process as the other would have
+ // been skipped.
+ verifyPendingRecords(greenQueue, List.of(timeTick));
+ final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+ getUidForPackage(PACKAGE_RED));
+ verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
@Test
public void testSkipPolicy_atEnqueueTime() throws Exception {
final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
@@ -1824,7 +1864,7 @@
return "receiver skipped by test";
}
return null;
- }).when(mSkipPolicy).shouldSkipMessage(any(BroadcastRecord.class), any());
+ }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
mImpl.enqueueBroadcastLocked(userPresentRecord);
mImpl.enqueueBroadcastLocked(timeTickRecord);
@@ -2270,19 +2310,11 @@
assertFalse(mImpl.isProcessFreezable(greenProcess));
}
- // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
- private ProcessRecord makeProcessRecord(ApplicationInfo info) {
- final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
- r.setPid(mNextPid.incrementAndGet());
- ProcessRecord.updateProcessRecordNodes(r);
- return r;
- }
-
BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
final IIntentReceiver receiver = mock(IIntentReceiver.class);
final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
UserHandle.getUserId(app.info.uid), receiver);
- return makeRegisteredReceiver(receiverList, priority);
+ return makeRegisteredReceiver(receiverList, priority, null /* requiredPermission */);
}
private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad35b25..3a9c99d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -2301,6 +2301,52 @@
}
/**
+ * Verify that we skip broadcasts at enqueue if {@link BroadcastSkipPolicy} decides it
+ * should be skipped.
+ */
+ @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+ @Test
+ public void testSkipPolicy_atEnqueueTime() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ final Object greenReceiver = makeRegisteredReceiver(receiverGreenApp);
+ final Object blueReceiver = makeRegisteredReceiver(receiverBlueApp);
+ final Object yellowReceiver = makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW);
+ final Object orangeReceiver = makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE);
+
+ doAnswer(invocation -> {
+ final BroadcastRecord r = invocation.getArgument(0);
+ final Object o = invocation.getArgument(1);
+ if (airplane.getAction().equals(r.intent.getAction())
+ && (isReceiverEquals(o, greenReceiver)
+ || isReceiverEquals(o, orangeReceiver))) {
+ return "test skipped receiver";
+ }
+ return null;
+ }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+ List.of(greenReceiver, blueReceiver, yellowReceiver, orangeReceiver)));
+
+ waitForIdle();
+ // Verify that only blue and yellow receiver apps received the broadcast.
+ verifyScheduleRegisteredReceiver(never(), receiverGreenApp, USER_SYSTEM);
+ verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+ eq(greenReceiver));
+ verifyScheduleRegisteredReceiver(receiverBlueApp, airplane);
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ verifyScheduleReceiver(receiverYellowApp, airplane);
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+ assertNull(receiverOrangeApp);
+ verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+ eq(orangeReceiver));
+ }
+
+ /**
* Verify broadcasts to runtime receivers in cached processes are deferred
* until that process leaves the cached state.
*/
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
new file mode 100644
index 0000000..c8aad79e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class BroadcastSkipPolicyTest extends BaseBroadcastQueueTest {
+ private static final String TAG = "BroadcastSkipPolicyTest";
+
+ BroadcastSkipPolicy mBroadcastSkipPolicy;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mBroadcastSkipPolicy = new BroadcastSkipPolicy(mAms);
+
+ doReturn(true).when(mIntentFirewall).checkBroadcast(any(Intent.class),
+ anyInt(), anyInt(), nullable(String.class), anyInt());
+
+ doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
+ doReturn(true).when(mIPackageManager).isPackageAvailable(anyString(), anyInt());
+
+ doReturn(ActivityManager.APP_START_MODE_NORMAL).when(mAms).getAppStartModeLOSP(anyInt(),
+ anyString(), anyInt(), anyInt(), eq(true), eq(false), eq(false));
+
+ doReturn(mAppOpsManager).when(mAms).getAppOpsManager();
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).checkOpNoThrow(anyString(),
+ anyInt(), anyString(), nullable(String.class));
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOpNoThrow(anyString(),
+ anyInt(), anyString(), nullable(String.class), anyString());
+
+ doReturn(mIPermissionManager).when(AppGlobals::getPermissionManager);
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mIPermissionManager).checkUidPermission(
+ anyInt(), anyString(), anyInt());
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+ return new BroadcastSkipPolicy(mAms);
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withCompPerm_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+ anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withCompPerm_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+ anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withCompPerm_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withCompPerm_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withAppOp_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, receiver);
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(receiver.activityInfo.applicationInfo.uid),
+ eq(receiver.activityInfo.packageName), nullable(String.class), anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withAppOp_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */);
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, filter);
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(filter.receiverList.uid),
+ eq(filter.packageName), nullable(String.class), anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withAppOp_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, receiver);
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(receiver.activityInfo.applicationInfo.uid),
+ eq(receiver.activityInfo.applicationInfo.packageName), nullable(String.class));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withAppOp_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */);
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, filter);
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(filter.receiverList.uid),
+ eq(filter.packageName), nullable(String.class));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withRequiredPerms_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+ assertNull(msg);
+ verify(mPermissionManager).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager, never()).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withRequiredPerms_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */));
+ assertNull(msg);
+ verify(mPermissionManager).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager, never()).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withRequiredPerms_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+ assertNull(msg);
+ verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withRequiredPerms_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */));
+ assertNull(msg);
+ verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ private ResolveInfo makeManifestReceiverWithPermission(String packageName, String name,
+ String permission) {
+ final ResolveInfo resolveInfo = makeManifestReceiver(packageName, name);
+ resolveInfo.activityInfo.permission = permission;
+ return resolveInfo;
+ }
+
+ private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority,
+ String requiredPermission) {
+ final IIntentReceiver receiver = mock(IIntentReceiver.class);
+ final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
+ UserHandle.getUserId(app.info.uid), receiver);
+ return makeRegisteredReceiver(receiverList, priority, requiredPermission);
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 4e4b3df..3e87943 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -2388,6 +2388,104 @@
}
}
+ @Test
+ @EnableFlags({Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS,
+ Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER})
+ public void testGetTimeUntilQuotaConsumedLocked_Installer() {
+ PackageInfo pi = new PackageInfo();
+ pi.packageName = SOURCE_PACKAGE;
+ pi.requestedPermissions = new String[]{Manifest.permission.INSTALL_PACKAGES};
+ pi.requestedPermissionsFlags = new int[]{PackageInfo.REQUESTED_PERMISSION_GRANTED};
+ pi.applicationInfo = new ApplicationInfo();
+ pi.applicationInfo.uid = mSourceUid;
+ doReturn(List.of(pi)).when(mPackageManager).getInstalledPackagesAsUser(anyInt(), anyInt());
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mContext).checkPermission(
+ eq(Manifest.permission.INSTALL_PACKAGES), anyInt(), eq(mSourceUid));
+ mQuotaController.onSystemServicesReady();
+
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ // Close to RARE boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.WINDOW_SIZE_RARE_MS - 30 * SECOND_IN_MILLIS),
+ 90 * SECOND_IN_MILLIS, 5), false);
+ // Far away from FREQUENT boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.WINDOW_SIZE_FREQUENT_MS - HOUR_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), false);
+ // Overlap WORKING_SET boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.WINDOW_SIZE_WORKING_MS + MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), false);
+ // Close to ACTIVE boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.WINDOW_SIZE_ACTIVE_MS - MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), false);
+ // Close to EXEMPTED boundary.
+ mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+ createTimingSession(
+ now - (mQcConstants.WINDOW_SIZE_EXEMPTED_MS - MINUTE_IN_MILLIS),
+ 2 * MINUTE_IN_MILLIS, 5), false);
+
+ // No additional quota for the system installer when the app is in RARE, FREQUENT,
+ // WORKING_SET or ACTIVE bucket.
+ setStandbyBucket(RARE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(30 * SECOND_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(2 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ setStandbyBucket(FREQUENT_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(2 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(2 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ setStandbyBucket(WORKING_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(5 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(6 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ // ACTIVE window != allowed time.
+ setStandbyBucket(ACTIVE_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(6 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(8 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
+ // Additional quota for the system installer when the app is in EXEMPTED bucket.
+ // EXEMPTED window == allowed time.
+ setStandbyBucket(EXEMPTED_INDEX);
+ synchronized (mQuotaController.mLock) {
+ assertEquals(18 * MINUTE_IN_MILLIS,
+ mQuotaController.getRemainingExecutionTimeLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 8 * MINUTE_IN_MILLIS,
+ mQuotaController.getTimeUntilQuotaConsumedLocked(
+ SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+ }
+
/**
* Test getTimeUntilQuotaConsumedLocked when the app is close to the max execution limit.
*/
@@ -4119,6 +4217,12 @@
assertEquals(85 * SECOND_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs());
assertEquals(84 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs());
assertEquals(83 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs());
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+ setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+ 6 * MINUTE_IN_MILLIS);
+ assertEquals(6 * MINUTE_IN_MILLIS,
+ mQuotaController.getAllowedTimePeriodAdditionInstallerMs());
}
@Test
@@ -4222,6 +4326,13 @@
assertEquals(0, mQuotaController.getEJGracePeriodTempAllowlistMs());
assertEquals(0, mQuotaController.getEJGracePeriodTopAppMs());
+ mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+ setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+ -MINUTE_IN_MILLIS);
+ assertEquals(0,
+ mQuotaController.getAllowedTimePeriodAdditionInstallerMs());
+ mSetFlagsRule.disableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+
// Invalid configurations.
// In_QUOTA_BUFFER should never be greater than ALLOWED_TIME_PER_PERIOD
setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
@@ -4237,9 +4348,18 @@
10 * MINUTE_IN_MILLIS);
setDeviceConfigLong(QcConstants.KEY_IN_QUOTA_BUFFER_MS, 5 * MINUTE_IN_MILLIS);
- assertTrue(mQuotaController.getInQuotaBufferMs()
+ assertTrue(mQuotaController.getAllowedTimePeriodAdditionInstallerMs()
<= mQuotaController.getAllowedTimePerPeriodMs()[FREQUENT_INDEX]);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+ // ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER should never be greater than
+ // ALLOWED_TIME_PER_PERIOD.
+ setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+ 15 * MINUTE_IN_MILLIS);
+ assertTrue(mQuotaController.getInQuotaBufferMs()
+ <= mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]);
+ mSetFlagsRule.disableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+
// Test larger than a day. Controller should cap at one day.
setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS,
25 * HOUR_IN_MILLIS);
@@ -4318,6 +4438,12 @@
assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs());
assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs());
assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs());
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
+ setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_ADDITION_INSTALLER_MS,
+ 25 * HOUR_IN_MILLIS);
+ assertEquals(0, mQuotaController.getAllowedTimePeriodAdditionInstallerMs());
+ mSetFlagsRule.disableFlags(Flags.FLAG_ADDITIONAL_QUOTA_FOR_SYSTEM_INSTALLER);
}
/** Tests that TimingSessions aren't saved when the device is charging. */
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index bada337..6b8ef88 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -64,7 +64,6 @@
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
import android.graphics.Color;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
@@ -95,6 +94,7 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;
+import com.android.server.wm.DesktopModeHelper;
import com.android.server.wm.WindowManagerInternal;
import org.hamcrest.CoreMatchers;
@@ -155,8 +155,6 @@
private IPackageManager mIpm = AppGlobals.getPackageManager();
- private Resources mResources = sContext.getResources();
-
@Mock
private DisplayManager mDisplayManager;
@@ -178,6 +176,7 @@
.spyStatic(WallpaperUtils.class)
.spyStatic(LocalServices.class)
.spyStatic(WallpaperManager.class)
+ .spyStatic(DesktopModeHelper.class)
.startMocking();
sWindowManagerInternal = mock(WindowManagerInternal.class);
@@ -246,6 +245,8 @@
int userId = (invocation.getArgument(0));
return getWallpaperTestDir(userId);
}).when(() -> WallpaperUtils.getWallpaperDir(anyInt()));
+ ExtendedMockito.doAnswer(invocation -> true).when(
+ () -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
sContext.addMockSystemService(DisplayManager.class, mDisplayManager);
@@ -257,10 +258,6 @@
doReturn(displays).when(mDisplayManager).getDisplays();
spyOn(mIpm);
- spyOn(mResources);
- doReturn(true).when(mResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
- doReturn(true).when(mResources).getBoolean(
- eq(R.bool.config_canInternalDisplayHostDesktops));
mService = new TestWallpaperManagerService(sContext);
spyOn(mService);
mService.systemReady();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 563baac..b0ffebb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -2441,6 +2441,80 @@
assertFalse(mWasCecDisabledOnStandbyByLowEnergyMode);
}
+ @Test
+ public void sendSystemAudioModeRequest_sendsRequest_retry() throws Exception {
+ // Enable System Audio Control
+ mHdmiControlService.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+ HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+ mNativeWrapper.setPortConnectionStatus(1, true);
+
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ HdmiCecMessage reportPowerStatus =
+ HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_AUDIO_SYSTEM, ADDR_TV,
+ HdmiControlManager.POWER_STATUS_STANDBY);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mNativeWrapper.onCecMessage(reportPowerStatus);
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setMessageSendResult(Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
+ SendMessageResult.NACK);
+ mTestLooper.dispatchAll();
+
+ // Use SystemAudioAutoInitiationAction to trigger SystemAudioActionFromTv
+ HdmiCecFeatureAction systemAudioAutoInitiationAction =
+ new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
+ mHdmiCecLocalDeviceTv.addAndStartAction(systemAudioAutoInitiationAction);
+ HdmiCecMessage reportSystemAudioMode =
+ HdmiCecMessageBuilder.buildReportSystemAudioMode(
+ ADDR_AUDIO_SYSTEM,
+ mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(),
+ true);
+ mHdmiControlService.handleCecCommand(reportSystemAudioMode);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(1);
+ }
+
+ @Test
+ public void sendSystemAudioModeRequest_sendsRequest_return() throws Exception {
+ // Enable System Audio Control
+ mHdmiControlService.getHdmiCecConfig().setIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
+ HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
+ mNativeWrapper.setPortConnectionStatus(1, true);
+
+ HdmiCecMessage reportPhysicalAddress =
+ HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+ ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+ HdmiCecMessage reportPowerStatus =
+ HdmiCecMessageBuilder.buildReportPowerStatus(ADDR_AUDIO_SYSTEM, ADDR_TV,
+ HdmiControlManager.POWER_STATUS_STANDBY);
+ mNativeWrapper.onCecMessage(reportPhysicalAddress);
+ mNativeWrapper.onCecMessage(reportPowerStatus);
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setMessageSendResult(Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
+ SendMessageResult.FAIL);
+ mTestLooper.dispatchAll();
+
+ // Use SystemAudioAutoInitiationAction to trigger SystemAudioActionFromTv
+ HdmiCecFeatureAction systemAudioAutoInitiationAction =
+ new SystemAudioAutoInitiationAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM);
+ mHdmiCecLocalDeviceTv.addAndStartAction(systemAudioAutoInitiationAction);
+ HdmiCecMessage reportSystemAudioMode =
+ HdmiCecMessageBuilder.buildReportSystemAudioMode(
+ ADDR_AUDIO_SYSTEM,
+ mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress(),
+ true);
+ mHdmiControlService.handleCecCommand(reportSystemAudioMode);
+ mTestLooper.dispatchAll();
+
+ assertThat(mHdmiCecLocalDeviceTv.getActions(SystemAudioActionFromTv.class)).hasSize(0);
+ }
+
protected static class MockTvDevice extends HdmiCecLocalDeviceTv {
MockTvDevice(HdmiControlService service) {
super(service);
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index a4e77c0..1de864c 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -17,9 +17,9 @@
package com.android.server.location.contexthub;
import static com.google.common.truth.Truth.assertThat;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,15 +33,21 @@
import android.hardware.contexthub.IContextHubEndpoint;
import android.hardware.contexthub.IContextHubEndpointCallback;
import android.hardware.contexthub.IEndpointCommunication;
+import android.hardware.contexthub.Message;
import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.contexthub.Reason;
+import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
import android.os.Binder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-
+import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
+import java.util.Collections;
+import java.util.List;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -51,11 +57,11 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.Collections;
-
@RunWith(AndroidJUnit4.class)
@Presubmit
public class ContextHubEndpointTest {
+ private static final String TAG = "ContextHubEndpointTest";
+
private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE;
private static final int MIN_SESSION_ID = 0;
private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1;
@@ -206,6 +212,68 @@
assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
}
+ @Test
+ public void testMessageTransaction() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true);
+
+ unregisterExampleEndpoint(endpoint);
+ }
+
+ @Test
+ public void testMessageTransactionCleanupOnUnregistration() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ false);
+
+ unregisterExampleEndpoint(endpoint);
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+ }
+
+ /** A helper method to create a session and validates reliable message sending. */
+ private void testMessageTransactionInternal(
+ IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
+ HubEndpointInfo targetInfo =
+ new HubEndpointInfo(
+ TARGET_ENDPOINT_NAME,
+ TARGET_ENDPOINT_ID,
+ ENDPOINT_PACKAGE_NAME,
+ Collections.emptyList());
+ int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null);
+ mEndpointManager.onEndpointSessionOpenComplete(sessionId);
+
+ final int messageType = 1234;
+ HubMessage message =
+ new HubMessage.Builder(messageType, new byte[] {1, 2, 3, 4, 5})
+ .setResponseRequired(true)
+ .build();
+ IContextHubTransactionCallback callback =
+ new IContextHubTransactionCallback.Stub() {
+ @Override
+ public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
+ Log.i(TAG, "Received onQueryResponse callback, result=" + result);
+ }
+
+ @Override
+ public void onTransactionComplete(int result) {
+ Log.i(TAG, "Received onTransactionComplete callback, result=" + result);
+ }
+ };
+ endpoint.sendMessage(sessionId, message, callback);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockEndpointCommunications, timeout(1000))
+ .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+ Message halMessage = messageCaptor.getValue();
+ assertThat(halMessage.type).isEqualTo(message.getMessageType());
+ assertThat(halMessage.content).isEqualTo(message.getMessageBody());
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(1);
+
+ if (deliverMessageStatus) {
+ mEndpointManager.onMessageDeliveryStatusReceived(
+ sessionId, halMessage.sequenceNumber, ErrorCode.OK);
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+ }
+ }
+
private IContextHubEndpoint registerExampleEndpoint() throws RemoteException {
HubEndpointInfo info =
new HubEndpointInfo(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
index b332331..6b989cb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -38,6 +38,7 @@
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.Condition;
+import com.android.internal.R;
import com.android.server.UiServiceTestCase;
import org.junit.Before;
@@ -46,6 +47,8 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+
public class ConditionProvidersTest extends UiServiceTestCase {
private ConditionProviders mProviders;
@@ -169,4 +172,15 @@
assertTrue(mProviders.getApproved(userId, true).isEmpty());
}
+
+ @Test
+ public void getDefaultDndAccessPackages_returnsPackages() {
+ mContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultDndAccessPackages,
+ "com.example.a:com.example.b::::com.example.c");
+
+ List<String> packages = ConditionProviders.getDefaultDndAccessPackages(mContext);
+
+ assertThat(packages).containsExactly("com.example.a", "com.example.b", "com.example.c");
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 67e85ff..5dea44d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -4495,27 +4495,6 @@
}
@Test
- public void testBubblePreference_sameVersionWithSAWPermission() throws Exception {
- when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(),
- anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED);
-
- final String xml = "<ranking version=\"4\">\n"
- + "<package name=\"" + PKG_O + "\" uid=\"" + UID_O + "\">\n"
- + "<channel id=\"someId\" name=\"hi\""
- + " importance=\"3\"/>"
- + "</package>"
- + "</ranking>";
- TypedXmlPullParser parser = Xml.newFastPullParser();
- parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())),
- null);
- parser.nextTag();
- mHelper.readXml(parser, false, UserHandle.USER_ALL);
-
- assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O));
- assertEquals(0, mHelper.getAppLockedFields(PKG_O, UID_O));
- }
-
- @Test
public void testBubblePreference_upgradeWithSAWThenUserOverride() throws Exception {
when(mAppOpsManager.noteOpNoThrow(eq(OP_SYSTEM_ALERT_WINDOW), anyInt(),
anyString(), eq(null), anyString())).thenReturn(MODE_ALLOWED);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
new file mode 100644
index 0000000..154a905
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ZenConfigTrimmerTest extends UiServiceTestCase {
+
+ private static final String TRUSTED_PACKAGE = "com.trust.me";
+ private static final int ONE_PERCENT = 1_500;
+
+ private ZenConfigTrimmer mTrimmer;
+
+ @Before
+ public void setUp() {
+ mContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultDndAccessPackages, TRUSTED_PACKAGE);
+
+ mTrimmer = new ZenConfigTrimmer(mContext);
+ }
+
+ @Test
+ public void trimToMaximumSize_belowMax_untouched() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ public void trimToMaximumSize_exceedsMax_removesAllRulesOfLargestPackages() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+ addZenRule(config, "7", "pkg4", 38 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "6");
+ assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+ .containsExactly("pkg1", "pkg3");
+ }
+
+ @Test
+ public void trimToMaximumSize_keepsRulesFromTrustedPackages() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", TRUSTED_PACKAGE, 60 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("4", "5");
+ assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+ .containsExactly(TRUSTED_PACKAGE, "pkg2");
+ }
+
+ /**
+ * Create a ZenRule that, when serialized to a Parcel, will take <em>approximately</em>
+ * {@code desiredSize} bytes (within 100 bytes). Try to make the tests not rely on a very tight
+ * fit.
+ */
+ private static void addZenRule(ZenModeConfig config, String id, String pkg, int desiredSize) {
+ ZenRule rule = new ZenRule();
+ rule.id = id;
+ rule.pkg = pkg;
+ config.automaticRules.put(id, rule);
+
+ // Make the ZenRule as large as desired. Not to the exact byte, because otherwise this
+ // test would have to be adjusted whenever we change the parceling of ZenRule in any way.
+ // (Still might need adjustment if we change the serialization _significantly_).
+ int nameLength = desiredSize - id.length() - pkg.length() - 232;
+ rule.name = "A".repeat(nameLength);
+
+ Parcel verification = Parcel.obtain();
+ try {
+ verification.writeParcelable(rule, 0);
+ assertThat(verification.dataSize()).isWithin(100).of(desiredSize);
+ } finally {
+ verification.recycle();
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index f8387a4..51891ef 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -90,6 +90,7 @@
import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
+import static com.android.server.notification.Flags.FLAG_LIMIT_ZEN_CONFIG_SIZE;
import static com.android.server.notification.Flags.FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING;
import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL;
import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
@@ -236,6 +237,7 @@
@SmallTest
@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
@RunWith(ParameterizedAndroidJunit4.class)
+@EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE) // Should be parameterization, but off path does nothing.
@TestableLooper.RunWithLooper
public class ZenModeHelperTest extends UiServiceTestCase {
@@ -7480,6 +7482,45 @@
assertThat(getZenRule(ruleId).lastActivation).isNull();
}
+ @Test
+ @EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE)
+ public void addAutomaticZenRule_trimsConfiguration() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ AutomaticZenRule smallRule = new AutomaticZenRule.Builder("Reasonable", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .build();
+ AutomaticZenRule systemRule = new AutomaticZenRule.Builder("System", CONDITION_ID)
+ .setOwner(new ComponentName("android", "ScheduleConditionProvider"))
+ .build();
+
+ AutomaticZenRule bigRule = new AutomaticZenRule.Builder("Yuge", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName("evil.package", "cls"))
+ .setTriggerDescription("0123456789".repeat(6000)) // ~60k bytes utf16.
+ .build();
+
+ String systemRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android",
+ systemRule, ORIGIN_SYSTEM, "add", SYSTEM_UID);
+ String smallRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, smallRule,
+ ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ String bigRuleId1 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId, bigRuleId1);
+
+ String bigRuleId2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId, bigRuleId1, bigRuleId2);
+
+ // This should go over the threshold
+ String bigRuleId3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+
+ // Rules from evil.package are gone.
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId);
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/TransitionSubject.java b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
new file mode 100644
index 0000000..07026b9
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.Nullable;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TransitionSubject extends Subject {
+
+ @Nullable
+ private final Transition actual;
+
+ /**
+ * Internal constructor.
+ *
+ * @see TransitionSubject#assertThat(Transition)
+ */
+ private TransitionSubject(FailureMetadata metadata, @Nullable Transition actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /**
+ * In a fluent assertion chain, the argument to the "custom" overload of {@link
+ * StandardSubjectBuilder#about(CustomSubjectBuilder.Factory) about}, the method that specifies
+ * what kind of {@link Subject} to create.
+ */
+ public static Factory<TransitionSubject, Transition> transitions() {
+ return TransitionSubject::new;
+ }
+
+ /**
+ * Typical entry point for making assertions about Transitions.
+ *
+ * @see @Truth#assertThat(Object)
+ */
+ public static TransitionSubject assertThat(Transition transition) {
+ return Truth.assertAbout(transitions()).that(transition);
+ }
+
+ /**
+ * Converts to a {@link IterableSubject} containing {@link Transition#getFlags()} separated into
+ * a list of individual flags for assertions such as {@code flags().contains(TRANSIT_FLAG_XYZ)}.
+ *
+ * <p>If the subject is null, this will fail instead of returning a null subject.
+ */
+ public IterableSubject flags() {
+ isNotNull();
+
+ final List<Integer> sortedFlags = new ArrayList<>();
+ for (int i = 0; i < 32; i++) {
+ if ((actual.getFlags() & (1 << i)) != 0) {
+ sortedFlags.add((1 << i));
+ }
+ }
+ return com.google.common.truth.Truth.assertThat(sortedFlags);
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index cfd501a..61ed0b5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -63,6 +63,9 @@
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -80,6 +83,7 @@
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
+import static com.android.server.wm.TransitionSubject.assertThat;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT;
@@ -147,6 +151,7 @@
import com.android.server.LocalServices;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.WmDisplayCutout;
+import com.android.window.flags.Flags;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -2620,6 +2625,7 @@
final KeyguardController keyguard = mAtm.mKeyguardController;
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
final int displayId = mDisplayContent.getDisplayId();
+ final TestTransitionPlayer transitions = registerTestTransitionPlayer();
final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2629,21 +2635,40 @@
keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+ transitions.flush();
// Start unlocking from AOD.
keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).isNull();
+ } else {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+ }
+ transitions.flush();
+
// Clear AOD. This does *not* clear the going-away status.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.aodTransition()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+ } else {
+ assertThat(transitions.mLastTransit).isNull();
+ }
+ transitions.flush();
+
// Finish unlock
keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+
+ assertThat(transitions.mLastTransit).isNull();
}
@Test
@@ -2653,6 +2678,7 @@
final KeyguardController keyguard = mAtm.mKeyguardController;
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
final int displayId = mDisplayContent.getDisplayId();
+ final TestTransitionPlayer transitions = registerTestTransitionPlayer();
final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2662,22 +2688,44 @@
keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+ transitions.flush();
// Start unlocking from AOD.
keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (!Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+ }
+ transitions.flush();
+
// Clear AOD. This does *not* clear the going-away status.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.aodTransition()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+ } else {
+ assertThat(transitions.mLastTransit).isNull();
+ }
+ transitions.flush();
+
// Same API call a second time cancels the unlock, because AOD isn't changing.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardShowing.getAsBoolean());
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+
+ if (Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).isNull();
+ } else {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_APPEARING);
+ }
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
index 2d4101e..6e0f7fb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
@@ -16,9 +16,12 @@
package com.android.server.wm;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_PRESENTATION;
+import static android.view.Display.FLAG_TRUSTED;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_WAKE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
@@ -30,6 +33,7 @@
import android.annotation.NonNull;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -118,6 +122,112 @@
assertFalse(window.isAttached());
}
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotCoverHostTask() {
+ int uid = Binder.getCallingUid();
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task task = createTask(presentationDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Adding a presentation window over its host task must fail.
+ assertAddPresentationWindowFails(uid, presentationDisplay.mDisplayId);
+
+ // Adding a presentation window on the other display must succeed.
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Moving the host task to the presenting display will remove the presentation.
+ task.reparent(mDefaultDisplay.getDefaultTaskDisplayArea(), true);
+ waitHandlerIdle(window.mWmService.mAtmService.mH);
+ final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+ assertEquals(TRANSIT_CLOSE, removeTransition.mType);
+ completeTransition(removeTransition, /*abortSync=*/ false);
+ assertFalse(window.isVisible());
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotLaunchOnAllDisplays() {
+ final int uid = Binder.getCallingUid();
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task task = createTask(presentationDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Add a presentation window on the default display.
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Adding another presentation window over the task even if it's a different UID because
+ // it would end up showing presentations on all displays.
+ assertAddPresentationWindowFails(uid + 1, presentationDisplay.mDisplayId);
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotLaunchOnNonPresentationDisplayWithoutHostHavingGlobalFocus() {
+ final int uid = Binder.getCallingUid();
+ // Adding a presentation window on an internal display requires a host task
+ // with global focus on another display.
+ assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task taskWiSameUid = createTask(presentationDisplay);
+ taskWiSameUid.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(taskWiSameUid);
+ assertTrue(activity.isVisible());
+ final Task taskWithDifferentUid = createTask(presentationDisplay);
+ taskWithDifferentUid.effectiveUid = uid + 1;
+ createActivityRecord(taskWithDifferentUid);
+ assertEquals(taskWithDifferentUid, presentationDisplay.getFocusedRootTask());
+
+ // The task with the same UID is covered by another task with a different UID, so this must
+ // also fail.
+ assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+ // Moving the task with the same UID to front and giving it global focus allows a
+ // presentation to show on the default display.
+ taskWiSameUid.moveToFront("test");
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testReparentingActivityToSameDisplayClosesPresentation() {
+ final int uid = Binder.getCallingUid();
+ final Task task = createTask(mDefaultDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Add a presentation window on a presentation display.
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final WindowState window = addPresentationWindow(uid, presentationDisplay.getDisplayId());
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Reparenting the host task below the presentation must close the presentation.
+ task.reparent(presentationDisplay.getDefaultTaskDisplayArea(), true);
+ waitHandlerIdle(window.mWmService.mAtmService.mH);
+ final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+ // It's a WAKE transition instead of CLOSE because
+ assertEquals(TRANSIT_WAKE, removeTransition.mType);
+ completeTransition(removeTransition, /*abortSync=*/ false);
+ assertFalse(window.isVisible());
+ }
+
private WindowState addPresentationWindow(int uid, int displayId) {
final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
final int userId = UserHandle.getUserId(uid);
@@ -134,10 +244,29 @@
return window;
}
+ private void assertAddPresentationWindowFails(int uid, int displayId) {
+ final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+ final IWindow clientWindow = new TestIWindow();
+ final int res = addPresentationWindowInner(uid, displayId, session, clientWindow);
+ assertEquals(WindowManagerGlobal.ADD_INVALID_DISPLAY, res);
+ }
+
+ private int addPresentationWindowInner(int uid, int displayId, Session session,
+ IWindow clientWindow) {
+ final int userId = UserHandle.getUserId(uid);
+ doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_PRESENTATION);
+ return mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId,
+ WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+ new InsetsSourceControl.Array(), new Rect(), new float[1]);
+ }
+
private DisplayContent createPresentationDisplay() {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.copyFrom(mDisplayInfo);
- displayInfo.flags = FLAG_PRESENTATION;
+ displayInfo.flags = FLAG_PRESENTATION | FLAG_TRUSTED;
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
final DisplayContent dc = createNewDisplay(displayInfo);
final int displayId = dc.getDisplayId();
doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 45436e4..d3f3269 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -33,6 +33,7 @@
import android.platform.test.annotations.Presubmit;
import android.view.Display.Mode;
import android.view.Surface;
+import android.view.WindowInsets;
import android.view.WindowManager.LayoutParams;
import androidx.test.filters.SmallTest;
@@ -283,7 +284,7 @@
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
- overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+ overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
@@ -303,7 +304,7 @@
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
- overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+ overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index c0642f5..57ab13f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -2151,6 +2151,14 @@
mLastRequest = null;
}
+ void flush() {
+ if (mLastTransit != null) {
+ start();
+ finish();
+ clear();
+ }
+ }
+
@Override
public void onTransitionReady(IBinder transitToken, TransitionInfo transitionInfo,
SurfaceControl.Transaction transaction, SurfaceControl.Transaction finishT)
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index ca4a643..ae7346e 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -1737,13 +1737,8 @@
private int getCardIdForDefaultEuicc() {
int cardId = TelephonyManager.UNINITIALIZED_CARD_ID;
- if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- PackageManager pm = mContext.getPackageManager();
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
- TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
- cardId = tm.getCardIdForDefaultEuicc();
- }
- } else {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
cardId = tm.getCardIdForDefaultEuicc();
}
diff --git a/tools/processors/view_inspector/OWNERS b/tools/processors/view_inspector/OWNERS
index 0473f54..38d21e1 100644
--- a/tools/processors/view_inspector/OWNERS
+++ b/tools/processors/view_inspector/OWNERS
@@ -1,3 +1,2 @@
alanv@google.com
-ashleyrose@google.com
-aurimas@google.com
\ No newline at end of file
+aurimas@google.com