Merge "Use only one BurnInParameters for AoD Burn in" into main
diff --git a/core/java/android/app/BroadcastStickyCache.java b/core/java/android/app/BroadcastStickyCache.java
new file mode 100644
index 0000000..d6f061b
--- /dev/null
+++ b/core/java/android/app/BroadcastStickyCache.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.TetheringManager;
+import android.net.nsd.NsdManager;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.SystemProperties;
+import android.os.UpdateLock;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.view.WindowManagerPolicyConstants;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+
+/** @hide */
+public class BroadcastStickyCache {
+
+ private static final String[] CACHED_BROADCAST_ACTIONS = {
+ AudioManager.ACTION_HDMI_AUDIO_PLUG,
+ AudioManager.ACTION_HEADSET_PLUG,
+ AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED,
+ AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED,
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ AudioManager.RINGER_MODE_CHANGED_ACTION,
+ ConnectivityManager.CONNECTIVITY_ACTION,
+ Intent.ACTION_BATTERY_CHANGED,
+ Intent.ACTION_DEVICE_STORAGE_FULL,
+ Intent.ACTION_DEVICE_STORAGE_LOW,
+ Intent.ACTION_SIM_STATE_CHANGED,
+ NsdManager.ACTION_NSD_STATE_CHANGED,
+ TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED,
+ TetheringManager.ACTION_TETHER_STATE_CHANGED,
+ UpdateLock.UPDATE_LOCK_CHANGED,
+ UsbManager.ACTION_USB_STATE,
+ WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED,
+ WifiManager.NETWORK_STATE_CHANGED_ACTION,
+ WifiManager.SUPPLICANT_STATE_CHANGED_ACTION,
+ WifiManager.WIFI_STATE_CHANGED_ACTION,
+ WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
+ WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED,
+ "android.net.conn.INET_CONDITION_ACTION" // ConnectivityManager.INET_CONDITION_ACTION
+ };
+
+ @GuardedBy("sCachedStickyBroadcasts")
+ private static final ArrayList<CachedStickyBroadcast> sCachedStickyBroadcasts =
+ new ArrayList<>();
+
+ @GuardedBy("sCachedPropertyHandles")
+ private static final ArrayMap<String, SystemProperties.Handle> sCachedPropertyHandles =
+ new ArrayMap<>();
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static boolean useCache(@Nullable IntentFilter filter) {
+ if (!shouldCache(filter)) {
+ return false;
+ }
+ synchronized (sCachedStickyBroadcasts) {
+ final CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+ if (cachedStickyBroadcast == null) {
+ return false;
+ }
+ final long version = cachedStickyBroadcast.propertyHandle.getLong(-1 /* def */);
+ return version > 0 && cachedStickyBroadcast.version == version;
+ }
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static void add(@Nullable IntentFilter filter, @Nullable Intent intent) {
+ if (!shouldCache(filter)) {
+ return;
+ }
+ synchronized (sCachedStickyBroadcasts) {
+ CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+ if (cachedStickyBroadcast == null) {
+ final String key = getKey(filter.getAction(0));
+ final SystemProperties.Handle handle = SystemProperties.find(key);
+ final long version = handle == null ? -1 : handle.getLong(-1 /* def */);
+ if (version == -1) {
+ return;
+ }
+ cachedStickyBroadcast = new CachedStickyBroadcast(filter, handle);
+ sCachedStickyBroadcasts.add(cachedStickyBroadcast);
+ cachedStickyBroadcast.intent = intent;
+ cachedStickyBroadcast.version = version;
+ } else {
+ cachedStickyBroadcast.intent = intent;
+ cachedStickyBroadcast.version = cachedStickyBroadcast.propertyHandle
+ .getLong(-1 /* def */);
+ }
+ }
+ }
+
+ private static boolean shouldCache(@Nullable IntentFilter filter) {
+ if (!Flags.useStickyBcastCache()) {
+ return false;
+ }
+ if (filter == null || filter.safeCountActions() != 1) {
+ return false;
+ }
+ if (!ArrayUtils.contains(CACHED_BROADCAST_ACTIONS, filter.getAction(0))) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public static String getKey(@NonNull String action) {
+ return "cache_key.system_server.sticky_bcast." + action;
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Nullable
+ public static Intent getIntentUnchecked(@NonNull IntentFilter filter) {
+ synchronized (sCachedStickyBroadcasts) {
+ final CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+ return cachedStickyBroadcast.intent;
+ }
+ }
+
+ @GuardedBy("sCachedStickyBroadcasts")
+ @Nullable
+ private static CachedStickyBroadcast getValueUncheckedLocked(@NonNull IntentFilter filter) {
+ for (int i = sCachedStickyBroadcasts.size() - 1; i >= 0; --i) {
+ final CachedStickyBroadcast cachedStickyBroadcast = sCachedStickyBroadcasts.get(i);
+ if (IntentFilter.filterEquals(filter, cachedStickyBroadcast.filter)) {
+ return cachedStickyBroadcast;
+ }
+ }
+ return null;
+ }
+
+ public static void incrementVersion(@NonNull String action) {
+ if (!shouldIncrementVersion(action)) {
+ return;
+ }
+ final String key = getKey(action);
+ synchronized (sCachedPropertyHandles) {
+ SystemProperties.Handle handle = sCachedPropertyHandles.get(key);
+ final long version;
+ if (handle == null) {
+ handle = SystemProperties.find(key);
+ if (handle != null) {
+ sCachedPropertyHandles.put(key, handle);
+ }
+ }
+ version = handle == null ? 0 : handle.getLong(0 /* def */);
+ SystemProperties.set(key, String.valueOf(version + 1));
+ if (handle == null) {
+ sCachedPropertyHandles.put(key, SystemProperties.find(key));
+ }
+ }
+ }
+
+ public static void incrementVersionIfExists(@NonNull String action) {
+ if (!shouldIncrementVersion(action)) {
+ return;
+ }
+ final String key = getKey(action);
+ synchronized (sCachedPropertyHandles) {
+ final SystemProperties.Handle handle = sCachedPropertyHandles.get(key);
+ if (handle == null) {
+ return;
+ }
+ final long version = handle.getLong(0 /* def */);
+ SystemProperties.set(key, String.valueOf(version + 1));
+ }
+ }
+
+ private static boolean shouldIncrementVersion(@NonNull String action) {
+ if (!Flags.useStickyBcastCache()) {
+ return false;
+ }
+ if (!ArrayUtils.contains(CACHED_BROADCAST_ACTIONS, action)) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ public static void clearForTest() {
+ synchronized (sCachedStickyBroadcasts) {
+ sCachedStickyBroadcasts.clear();
+ }
+ synchronized (sCachedPropertyHandles) {
+ sCachedPropertyHandles.clear();
+ }
+ }
+
+ private static final class CachedStickyBroadcast {
+ @NonNull public final IntentFilter filter;
+ @Nullable public Intent intent;
+ @IntRange(from = 0) public long version;
+ @NonNull public final SystemProperties.Handle propertyHandle;
+
+ CachedStickyBroadcast(@NonNull IntentFilter filter,
+ @NonNull SystemProperties.Handle propertyHandle) {
+ this.filter = filter;
+ this.propertyHandle = propertyHandle;
+ }
+ }
+}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 90fba29..ecef0db 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1921,10 +1921,19 @@
}
}
try {
- final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
- mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
- AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
- flags);
+ final Intent intent;
+ if (receiver == null && BroadcastStickyCache.useCache(filter)) {
+ intent = BroadcastStickyCache.getIntentUnchecked(filter);
+ } else {
+ intent = ActivityManager.getService().registerReceiverWithFeature(
+ mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
+ AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission,
+ userId,
+ flags);
+ if (receiver == null) {
+ BroadcastStickyCache.add(filter, intent);
+ }
+ }
if (intent != null) {
intent.setExtrasClassLoader(getClassLoader());
// TODO: determine at registration time if caller is
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 5ed1f4e..637187e 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -177,6 +177,10 @@
{
"file_patterns": ["(/|^)AppOpsManager.java"],
"name": "CtsAppOpsTestCases"
+ },
+ {
+ "file_patterns": ["(/|^)BroadcastStickyCache.java"],
+ "name": "BroadcastUnitTests"
}
]
}
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 38bd576..56488e7 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -147,3 +147,14 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "use_sticky_bcast_cache"
+ description: "Use cache for sticky broadcast intents"
+ is_fixed_read_only: true
+ bug: "356148006"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index b0791e3..bca5bcc 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -251,17 +251,18 @@
}
flag {
- name: "replace_body_sensors_permission_enabled"
- is_exported: true
- namespace: "android_health_services"
- description: "This flag is used to enable replacing permission BODY_SENSORS(and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE(and READ_HEALTH_DATA_IN_BACKGROUND)"
- bug: "364638912"
-}
-
-flag {
name: "appop_access_tracking_logging_enabled"
is_fixed_read_only: true
namespace: "permissions"
description: "Enables logging of the AppOp access tracking"
bug: "365584286"
}
+
+flag {
+ name: "replace_body_sensor_permission_enabled"
+ is_fixed_read_only: true
+ is_exported: true
+ namespace: "android_health_services"
+ description: "This fixed read-only flag is used to enable replacing permission BODY_SENSORS (and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE (and READ_HEALTH_DATA_IN_BACKGROUND)"
+ bug: "364638912"
+}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index c83285a..3599332 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -168,3 +168,13 @@
description: "Decouple variation settings, weight and style information from Typeface class"
bug: "361260253"
}
+
+flag {
+ name: "handwriting_track_disabled"
+ namespace: "text"
+ description: "Handwriting initiator tracks focused view even if handwriting is disabled to fix initiation bug."
+ bug: "361256391"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index ab9bd1f..f132963 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -17,6 +17,7 @@
package android.view;
import static com.android.text.flags.Flags.handwritingCursorPosition;
+import static com.android.text.flags.Flags.handwritingTrackDisabled;
import static com.android.text.flags.Flags.handwritingUnsupportedMessage;
import android.annotation.FlaggedApi;
@@ -352,7 +353,7 @@
final View focusedView = getFocusedView();
- if (!view.isAutoHandwritingEnabled()) {
+ if (!handwritingTrackDisabled() && !view.isAutoHandwritingEnabled()) {
clearFocusedView(focusedView);
return;
}
@@ -363,7 +364,8 @@
updateFocusedView(view);
if (mState != null && mState.mPendingFocusedView != null
- && mState.mPendingFocusedView.get() == view) {
+ && mState.mPendingFocusedView.get() == view
+ && (!handwritingTrackDisabled() || view.isAutoHandwritingEnabled())) {
startHandwriting(view);
}
}
@@ -416,7 +418,7 @@
*/
@VisibleForTesting
public boolean updateFocusedView(@NonNull View view) {
- if (!view.shouldInitiateHandwriting()) {
+ if (!handwritingTrackDisabled() && !view.shouldInitiateHandwriting()) {
mFocusedView = null;
return false;
}
@@ -424,8 +426,10 @@
final View focusedView = getFocusedView();
if (focusedView != view) {
mFocusedView = new WeakReference<>(view);
- // A new view just gain focus. By default, we should show hover icon for it.
- mShowHoverIconForConnectedView = true;
+ if (!handwritingTrackDisabled() || view.shouldInitiateHandwriting()) {
+ // A new view just gain focus. By default, we should show hover icon for it.
+ mShowHoverIconForConnectedView = true;
+ }
}
return true;
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index e65b4b6..c0a7383 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -16,14 +16,19 @@
package com.android.internal.widget;
+import static java.lang.Float.NaN;
+
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Path;
+import android.graphics.PorterDuff;
import android.graphics.RectF;
import android.graphics.Region;
import android.hardware.input.InputManager;
@@ -65,11 +70,14 @@
private static final PointerState EMPTY_POINTER_STATE = new PointerState();
public static class PointerState {
- // Trace of previous points.
- private float[] mTraceX = new float[32];
- private float[] mTraceY = new float[32];
- private boolean[] mTraceCurrent = new boolean[32];
- private int mTraceCount;
+ private float mCurrentX = NaN;
+ private float mCurrentY = NaN;
+ private float mPreviousX = NaN;
+ private float mPreviousY = NaN;
+ private float mFirstX = NaN;
+ private float mFirstY = NaN;
+ private boolean mPreviousPointIsHistorical;
+ private boolean mCurrentPointIsHistorical;
// True if the pointer is down.
@UnsupportedAppUsage
@@ -96,31 +104,20 @@
public PointerState() {
}
- public void clearTrace() {
- mTraceCount = 0;
- }
-
- public void addTrace(float x, float y, boolean current) {
- int traceCapacity = mTraceX.length;
- if (mTraceCount == traceCapacity) {
- traceCapacity *= 2;
- float[] newTraceX = new float[traceCapacity];
- System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
- mTraceX = newTraceX;
-
- float[] newTraceY = new float[traceCapacity];
- System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
- mTraceY = newTraceY;
-
- boolean[] newTraceCurrent = new boolean[traceCapacity];
- System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount);
- mTraceCurrent= newTraceCurrent;
+ public void addTrace(float x, float y, boolean isHistorical) {
+ if (Float.isNaN(mFirstX)) {
+ mFirstX = x;
+ }
+ if (Float.isNaN(mFirstY)) {
+ mFirstY = y;
}
- mTraceX[mTraceCount] = x;
- mTraceY[mTraceCount] = y;
- mTraceCurrent[mTraceCount] = current;
- mTraceCount += 1;
+ mPreviousX = mCurrentX;
+ mPreviousY = mCurrentY;
+ mCurrentX = x;
+ mCurrentY = y;
+ mPreviousPointIsHistorical = mCurrentPointIsHistorical;
+ mCurrentPointIsHistorical = isHistorical;
}
}
@@ -149,6 +146,12 @@
private final SparseArray<PointerState> mPointers = new SparseArray<PointerState>();
private final PointerCoords mTempCoords = new PointerCoords();
+ // Draw the trace of all pointers in the current gesture in a separate layer
+ // that is not cleared on every frame so that we don't have to re-draw the
+ // entire trace on each frame.
+ private final Bitmap mTraceBitmap;
+ private final Canvas mTraceCanvas;
+
private final Region mSystemGestureExclusion = new Region();
private final Region mSystemGestureExclusionRejected = new Region();
private final Path mSystemGestureExclusionPath = new Path();
@@ -197,6 +200,10 @@
mPathPaint.setARGB(255, 0, 96, 255);
mPathPaint.setStyle(Paint.Style.STROKE);
+ mTraceBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics().widthPixels,
+ getResources().getDisplayMetrics().heightPixels, Bitmap.Config.ARGB_8888);
+ mTraceCanvas = new Canvas(mTraceBitmap);
+
configureDensityDependentFactors();
mSystemGestureExclusionPaint = new Paint();
@@ -256,7 +263,7 @@
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mTextPaint.getFontMetricsInt(mTextMetrics);
- mHeaderBottom = mHeaderPaddingTop-mTextMetrics.ascent+mTextMetrics.descent+2;
+ mHeaderBottom = mHeaderPaddingTop - mTextMetrics.ascent + mTextMetrics.descent + 2;
if (false) {
Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
+ " descent=" + mTextMetrics.descent
@@ -269,6 +276,7 @@
// Draw an oval. When angle is 0 radians, orients the major axis vertically,
// angles less than or greater than 0 radians rotate the major axis left or right.
private RectF mReusableOvalRect = new RectF();
+
private void drawOval(Canvas canvas, float x, float y, float major, float minor,
float angle, Paint paint) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
@@ -285,6 +293,8 @@
protected void onDraw(Canvas canvas) {
final int NP = mPointers.size();
+ canvas.drawBitmap(mTraceBitmap, 0, 0, null);
+
if (!mSystemGestureExclusion.isEmpty()) {
mSystemGestureExclusionPath.reset();
mSystemGestureExclusion.getBoundaryPath(mSystemGestureExclusionPath);
@@ -303,32 +313,9 @@
// Pointer trace.
for (int p = 0; p < NP; p++) {
final PointerState ps = mPointers.valueAt(p);
+ float lastX = ps.mCurrentX, lastY = ps.mCurrentY;
- // Draw path.
- final int N = ps.mTraceCount;
- float lastX = 0, lastY = 0;
- boolean haveLast = false;
- boolean drawn = false;
- mPaint.setARGB(255, 128, 255, 255);
- for (int i=0; i < N; i++) {
- float x = ps.mTraceX[i];
- float y = ps.mTraceY[i];
- if (Float.isNaN(x) || Float.isNaN(y)) {
- haveLast = false;
- continue;
- }
- if (haveLast) {
- canvas.drawLine(lastX, lastY, x, y, mPathPaint);
- final Paint paint = ps.mTraceCurrent[i - 1] ? mCurrentPointPaint : mPaint;
- canvas.drawPoint(lastX, lastY, paint);
- drawn = true;
- }
- lastX = x;
- lastY = y;
- haveLast = true;
- }
-
- if (drawn) {
+ if (!Float.isNaN(lastX) && !Float.isNaN(lastY)) {
// Draw velocity vector.
mPaint.setARGB(255, 255, 64, 128);
float xVel = ps.mXVelocity * (1000 / 60);
@@ -353,7 +340,7 @@
Math.max(getHeight(), getWidth()), mTargetPaint);
// Draw current point.
- int pressureLevel = (int)(ps.mCoords.pressure * 255);
+ int pressureLevel = (int) (ps.mCoords.pressure * 255);
mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
@@ -424,8 +411,7 @@
.append(" / ").append(mMaxNumPointers)
.toString(), 1, base, mTextPaint);
- final int count = ps.mTraceCount;
- if ((mCurDown && ps.mCurDown) || count == 0) {
+ if ((mCurDown && ps.mCurDown) || Float.isNaN(ps.mCurrentX)) {
canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
mTextBackgroundPaint);
canvas.drawText(mText.clear()
@@ -437,8 +423,8 @@
.append("Y: ").append(ps.mCoords.y, 1)
.toString(), 1 + itemW * 2, base, mTextPaint);
} else {
- float dx = ps.mTraceX[count - 1] - ps.mTraceX[0];
- float dy = ps.mTraceY[count - 1] - ps.mTraceY[0];
+ float dx = ps.mCurrentX - ps.mFirstX;
+ float dy = ps.mCurrentY - ps.mFirstY;
canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
Math.abs(dx) < mVC.getScaledTouchSlop()
? mTextBackgroundPaint : mTextLevelPaint);
@@ -565,9 +551,9 @@
.append(" TouchMinor=").append(coords.touchMinor, 3)
.append(" ToolMajor=").append(coords.toolMajor, 3)
.append(" ToolMinor=").append(coords.toolMinor, 3)
- .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
+ .append(" Orientation=").append((float) (coords.orientation * 180 / Math.PI), 1)
.append("deg")
- .append(" Tilt=").append((float)(
+ .append(" Tilt=").append((float) (
coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
.append("deg")
.append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
@@ -598,6 +584,7 @@
mCurNumPointers = 0;
mMaxNumPointers = 0;
mVelocity.clear();
+ mTraceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
if (mAltVelocity != null) {
mAltVelocity.clear();
}
@@ -646,7 +633,8 @@
logCoords("Pointer", action, i, coords, id, event);
}
if (ps != null) {
- ps.addTrace(coords.x, coords.y, false);
+ ps.addTrace(coords.x, coords.y, true);
+ updateDrawTrace(ps);
}
}
}
@@ -659,7 +647,8 @@
logCoords("Pointer", action, i, coords, id, event);
}
if (ps != null) {
- ps.addTrace(coords.x, coords.y, true);
+ ps.addTrace(coords.x, coords.y, false);
+ updateDrawTrace(ps);
ps.mXVelocity = mVelocity.getXVelocity(id);
ps.mYVelocity = mVelocity.getYVelocity(id);
if (mAltVelocity != null) {
@@ -702,13 +691,26 @@
if (mActivePointerId == id) {
mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
}
- ps.addTrace(Float.NaN, Float.NaN, false);
+ ps.addTrace(Float.NaN, Float.NaN, true);
}
}
invalidate();
}
+ private void updateDrawTrace(PointerState ps) {
+ mPaint.setARGB(255, 128, 255, 255);
+ float x = ps.mCurrentX;
+ float y = ps.mCurrentY;
+ float lastX = ps.mPreviousX;
+ float lastY = ps.mPreviousY;
+ if (!Float.isNaN(x) && !Float.isNaN(y) && !Float.isNaN(lastX) && !Float.isNaN(lastY)) {
+ mTraceCanvas.drawLine(lastX, lastY, x, y, mPathPaint);
+ Paint paint = ps.mPreviousPointIsHistorical ? mPaint : mCurrentPointPaint;
+ mTraceCanvas.drawPoint(lastX, lastY, paint);
+ }
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
onPointerEvent(event);
@@ -767,7 +769,7 @@
return true;
default:
return KeyEvent.isGamepadButton(keyCode)
- || KeyEvent.isModifierKey(keyCode);
+ || KeyEvent.isModifierKey(keyCode);
}
}
@@ -887,7 +889,7 @@
public FasterStringBuilder append(int value, int zeroPadWidth) {
final boolean negative = value < 0;
if (negative) {
- value = - value;
+ value = -value;
if (value < 0) {
append("-2147483648");
return this;
@@ -973,26 +975,27 @@
private ISystemGestureExclusionListener mSystemGestureExclusionListener =
new ISystemGestureExclusionListener.Stub() {
- @Override
- public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion,
- Region systemGestureExclusionUnrestricted) {
- Region exclusion = Region.obtain(systemGestureExclusion);
- Region rejected = Region.obtain();
- if (systemGestureExclusionUnrestricted != null) {
- rejected.set(systemGestureExclusionUnrestricted);
- rejected.op(exclusion, Region.Op.DIFFERENCE);
- }
- Handler handler = getHandler();
- if (handler != null) {
- handler.post(() -> {
- mSystemGestureExclusion.set(exclusion);
- mSystemGestureExclusionRejected.set(rejected);
- exclusion.recycle();
- invalidate();
- });
- }
- }
- };
+ @Override
+ public void onSystemGestureExclusionChanged(int displayId,
+ Region systemGestureExclusion,
+ Region systemGestureExclusionUnrestricted) {
+ Region exclusion = Region.obtain(systemGestureExclusion);
+ Region rejected = Region.obtain();
+ if (systemGestureExclusionUnrestricted != null) {
+ rejected.set(systemGestureExclusionUnrestricted);
+ rejected.op(exclusion, Region.Op.DIFFERENCE);
+ }
+ Handler handler = getHandler();
+ if (handler != null) {
+ handler.post(() -> {
+ mSystemGestureExclusion.set(exclusion);
+ mSystemGestureExclusionRejected.set(rejected);
+ exclusion.recycle();
+ invalidate();
+ });
+ }
+ }
+ };
@Override
protected void onConfigurationChanged(Configuration newConfig) {
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 69437b4..9854030 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -287,6 +287,11 @@
<string name="config_satellite_demo_mode_sos_intent_action" translatable="false"></string>
<java-symbol type="string" name="config_satellite_demo_mode_sos_intent_action" />
+ <!-- The action of the intent that hidden menu sends to the app to launch esp loopback test mode
+ for sos emergency messaging via satellite. -->
+ <string name="config_satellite_test_with_esp_replies_intent_action" translatable="false"></string>
+ <java-symbol type="string" name="config_satellite_test_with_esp_replies_intent_action" />
+
<!-- Whether outgoing satellite datagrams should be sent to modem in demo mode. When satellite
is enabled for demo mode, if this config is enabled, outgoing datagrams will be sent to
modem; otherwise, success results will be returned. If demo mode is disabled, outgoing
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
index c0ff192..1d1cdfa 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
@@ -28,6 +28,8 @@
android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height"
android:paddingVertical="16dp"
android:paddingHorizontal="10dp"
+ android:screenReaderFocusable="true"
+ android:importantForAccessibility="yes"
android:contentDescription="@string/handle_text"
android:src="@drawable/decor_handle_dark"
tools:tint="@color/desktop_mode_caption_handle_bar_dark"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
index 7dcb3c2..3dbf754 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
@@ -31,14 +31,16 @@
android:orientation="horizontal"
android:clickable="true"
android:focusable="true"
+ android:contentDescription="@string/desktop_mode_app_header_chip_text"
android:layout_marginStart="12dp">
<ImageView
android:id="@+id/application_icon"
android:layout_width="@dimen/desktop_mode_caption_icon_radius"
android:layout_height="@dimen/desktop_mode_caption_icon_radius"
android:layout_gravity="center_vertical"
- android:contentDescription="@string/app_icon_text"
android:layout_marginStart="6dp"
+ android:clickable="false"
+ android:focusable="false"
android:scaleType="centerCrop"/>
<TextView
@@ -53,18 +55,22 @@
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:layout_marginStart="8dp"
+ android:clickable="false"
+ android:focusable="false"
tools:text="Gmail"/>
<ImageButton
android:id="@+id/expand_menu_button"
android:layout_width="16dp"
android:layout_height="16dp"
- android:contentDescription="@string/expand_menu_text"
android:src="@drawable/ic_baseline_expand_more_24"
android:background="@null"
android:scaleType="fitCenter"
android:clickable="false"
android:focusable="false"
+ android:screenReaderFocusable="false"
+ android:importantForAccessibility="no"
+ android:contentDescription="@null"
android:layout_marginHorizontal="8dp"
android:layout_gravity="center_vertical"/>
@@ -90,6 +96,7 @@
<com.android.wm.shell.windowdecor.MaximizeButtonView
android:id="@+id/maximize_button_view"
+ android:importantForAccessibility="no"
android:layout_width="44dp"
android:layout_height="40dp"
android:layout_gravity="end"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index 64f71c7..6913e54 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -43,13 +43,15 @@
android:layout_height="@dimen/desktop_mode_caption_icon_radius"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
- android:contentDescription="@string/app_icon_text"/>
+ android:contentDescription="@string/app_icon_text"
+ android:importantForAccessibility="no"/>
<TextView
android:id="@+id/application_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Gmail"
+ android:importantForAccessibility="no"
android:textColor="?androidprv:attr/materialColorOnSurface"
android:textSize="14sp"
android:textFontWeight="500"
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 5fe3f2a..35ef239 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
@@ -41,6 +41,8 @@
android:id="@+id/maximize_menu_maximize_button"
style="?android:attr/buttonBarButtonStyle"
android:stateListAnimator="@null"
+ android:importantForAccessibility="yes"
+ android:contentDescription="@string/desktop_mode_maximize_menu_maximize_button_text"
android:layout_marginRight="8dp"
android:layout_marginBottom="4dp"
android:alpha="0"/>
@@ -53,6 +55,7 @@
android:layout_marginBottom="76dp"
android:gravity="center"
android:fontFamily="google-sans-text"
+ android:importantForAccessibility="no"
android:text="@string/desktop_mode_maximize_menu_maximize_text"
android:textColor="?androidprv:attr/materialColorOnSurface"
android:alpha="0"/>
@@ -78,6 +81,8 @@
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:layout_marginRight="4dp"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
+ android:importantForAccessibility="yes"
+ android:contentDescription="@string/desktop_mode_maximize_menu_snap_left_button_text"
android:stateListAnimator="@null"/>
<Button
@@ -86,6 +91,8 @@
android:layout_width="41dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
+ android:importantForAccessibility="yes"
+ android:contentDescription="@string/desktop_mode_maximize_menu_snap_right_button_text"
android:stateListAnimator="@null"/>
</LinearLayout>
<TextView
@@ -96,6 +103,7 @@
android:layout_marginBottom="76dp"
android:layout_gravity="center"
android:gravity="center"
+ android:importantForAccessibility="no"
android:fontFamily="google-sans-text"
android:text="@string/desktop_mode_maximize_menu_snap_text"
android:textColor="?androidprv:attr/materialColorOnSurface"
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
index cf1b894..b734d2d 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -19,7 +19,8 @@
<FrameLayout
android:layout_width="44dp"
- android:layout_height="40dp">
+ android:layout_height="40dp"
+ android:importantForAccessibility="noHideDescendants">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index a6da421..bda5686 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -300,12 +300,19 @@
<string name="close_text">Close</string>
<!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
<string name="collapse_menu_text">Close Menu</string>
- <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] -->
- <string name="expand_menu_text">Open Menu</string>
+ <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] -->
+ <string name="desktop_mode_app_header_chip_text">Open Menu</string>
<!-- Maximize menu maximize button string. -->
<string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
<!-- Maximize menu snap buttons string. -->
<string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
<!-- Snap resizing non-resizable string. -->
<string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string>
+ <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] -->
+ <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string>
+ <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] -->
+ <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string>
+ <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] -->
+ <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string>
+
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 1537a1e..23f8e6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -610,7 +610,7 @@
|| !Flags.enableHandleInputFix()) {
return;
}
- ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer();
+ asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer();
}
private WindowDecorationViewHolder createViewHolder() {
@@ -647,6 +647,22 @@
return viewHolder instanceof AppHandleViewHolder;
}
+ @Nullable
+ private AppHandleViewHolder asAppHandle(WindowDecorationViewHolder viewHolder) {
+ if (viewHolder instanceof AppHandleViewHolder) {
+ return (AppHandleViewHolder) viewHolder;
+ }
+ return null;
+ }
+
+ @Nullable
+ private AppHeaderViewHolder asAppHeader(WindowDecorationViewHolder viewHolder) {
+ if (viewHolder instanceof AppHeaderViewHolder) {
+ return (AppHeaderViewHolder) viewHolder;
+ }
+ return null;
+ }
+
@VisibleForTesting
static void updateRelayoutParams(
RelayoutParams relayoutParams,
@@ -1025,7 +1041,15 @@
*/
void closeMaximizeMenu() {
if (!isMaximizeMenuActive()) return;
- mMaximizeMenu.close();
+ mMaximizeMenu.close(() -> {
+ // Request the accessibility service to refocus on the maximize button after closing
+ // the menu.
+ final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder);
+ if (appHeader != null) {
+ appHeader.requestAccessibilityFocus();
+ }
+ return Unit.INSTANCE;
+ });
mMaximizeMenu = null;
}
@@ -1408,7 +1432,7 @@
void setAnimatingTaskResizeOrReposition(boolean animatingTaskResizeOrReposition) {
if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return;
- ((AppHeaderViewHolder) mWindowDecorViewHolder)
+ asAppHeader(mWindowDecorViewHolder)
.setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition);
}
@@ -1416,16 +1440,14 @@
* Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
*/
void onMaximizeButtonHoverExit() {
- ((AppHeaderViewHolder) mWindowDecorViewHolder)
- .onMaximizeWindowHoverExit();
+ asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverExit();
}
/**
* Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button.
*/
void onMaximizeButtonHoverEnter() {
- ((AppHeaderViewHolder) mWindowDecorViewHolder)
- .onMaximizeWindowHoverEnter();
+ asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverEnter();
}
@Override
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 5b3f263..9a5b4f5 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
@@ -23,8 +23,6 @@
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Bitmap
-import android.graphics.BlendMode
-import android.graphics.BlendModeColorFilter
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
@@ -568,9 +566,7 @@
appIconBitmap: Bitmap?,
appName: CharSequence?
) {
- appInfoPill.background.colorFilter = BlendModeColorFilter(
- style.backgroundColor, BlendMode.MULTIPLY
- )
+ appInfoPill.background.setTint(style.backgroundColor)
collapseMenuButton.apply {
imageTintList = ColorStateList.valueOf(style.textColor)
@@ -584,9 +580,7 @@
}
private fun bindWindowingPill(style: MenuStyle) {
- windowingPill.background.colorFilter = BlendModeColorFilter(
- style.backgroundColor, BlendMode.MULTIPLY
- )
+ windowingPill.background.setTint(style.backgroundColor)
// TODO: Remove once implemented.
floatingBtn.visibility = View.GONE
@@ -612,23 +606,19 @@
}
screenshotBtn.apply {
isGone = !SHOULD_SHOW_SCREENSHOT_BUTTON
- background.colorFilter =
- BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY
- )
+ background.setTint(style.backgroundColor)
setTextColor(style.textColor)
compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
}
newWindowBtn.apply {
isGone = !shouldShowNewWindowButton
- background.colorFilter =
- BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
+ background.setTint(style.backgroundColor)
setTextColor(style.textColor)
compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
}
manageWindowBtn.apply {
isGone = !shouldShowManageWindowsButton
- background.colorFilter =
- BlendModeColorFilter(style.backgroundColor, BlendMode.MULTIPLY)
+ background.setTint(style.backgroundColor)
setTextColor(style.textColor)
compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
}
@@ -637,9 +627,7 @@
private fun bindOpenInBrowserPill(style: MenuStyle) {
openInBrowserPill.apply {
isGone = !shouldShowBrowserPill
- background.colorFilter = BlendModeColorFilter(
- style.backgroundColor, BlendMode.MULTIPLY
- )
+ background.setTint(style.backgroundColor)
}
browserBtn.apply {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 9590ccd..0c475f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -26,6 +26,7 @@
import android.view.View.TRANSLATION_Y
import android.view.View.TRANSLATION_Z
import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
import android.widget.Button
import androidx.core.animation.doOnEnd
import androidx.core.view.children
@@ -83,7 +84,12 @@
animateWindowingPillOpen()
animateMoreActionsPillOpen()
animateOpenInBrowserPill()
- runAnimations()
+ runAnimations {
+ appInfoPill.post {
+ appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ }
+ }
}
/**
@@ -98,7 +104,12 @@
animateWindowingPillOpen()
animateMoreActionsPillOpen()
animateOpenInBrowserPill()
- runAnimations()
+ runAnimations {
+ appInfoPill.post {
+ appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
+ AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ }
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 9c73e4a..0cb219a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -51,6 +51,7 @@
import android.view.ViewGroup
import android.view.WindowManager
import android.view.WindowlessWindowManager
+import android.view.accessibility.AccessibilityEvent
import android.widget.Button
import android.widget.TextView
import android.window.TaskConstants
@@ -116,19 +117,24 @@
onHoverListener = onHoverListener,
onOutsideTouchListener = onOutsideTouchListener
)
- maximizeMenuView?.animateOpenMenu()
+ maximizeMenuView?.let { view ->
+ view.animateOpenMenu(onEnd = {
+ view.requestAccessibilityFocus()
+ })
+ }
}
/** Closes the maximize window and releases its view. */
- fun close() {
+ fun close(onEnd: () -> Unit) {
val view = maximizeMenuView
val menu = maximizeMenu
if (view == null) {
menu?.releaseView()
} else {
- view.animateCloseMenu {
+ view.animateCloseMenu(onEnd = {
menu?.releaseView()
- }
+ onEnd.invoke()
+ })
}
maximizeMenu = null
maximizeMenuView = null
@@ -351,7 +357,7 @@
}
/** Animate the opening of the menu */
- fun animateOpenMenu() {
+ fun animateOpenMenu(onEnd: () -> Unit) {
maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
menuAnimatorSet = AnimatorSet()
@@ -419,6 +425,7 @@
onEnd = {
maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ onEnd.invoke()
}
)
menuAnimatorSet?.start()
@@ -499,6 +506,14 @@
menuAnimatorSet?.start()
}
+ /** Request that the accessibility service focus on the menu. */
+ fun requestAccessibilityFocus() {
+ // Focus the first button in the menu by default.
+ maximizeButton.post {
+ maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ }
+ }
+
/** Cancel the menu animation. */
private fun cancelAnimation() {
menuAnimatorSet?.cancel()
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 af6a819..e996165 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
@@ -30,6 +30,7 @@
import android.view.View
import android.view.View.OnLongClickListener
import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.view.accessibility.AccessibilityEvent
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
@@ -263,7 +264,11 @@
override fun onHandleMenuOpened() {}
- override fun onHandleMenuClosed() {}
+ override fun onHandleMenuClosed() {
+ openMenuButton.post {
+ openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ }
+ }
fun setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition: Boolean) {
// If animating a task resize or reposition, cancel any running hover animations
@@ -309,6 +314,12 @@
)
}
+ fun requestAccessibilityFocus() {
+ maximizeWindowButton.post {
+ maximizeWindowButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+ }
+ }
+
private fun getHeaderStyle(header: Header): HeaderStyle {
return HeaderStyle(
background = getHeaderBackground(header),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index a1867f3..9c11ec3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -623,7 +623,7 @@
.postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS));
mCloseMaxMenuRunnable.getValue().run();
- verify(menu).close();
+ verify(menu).close(any());
assertFalse(decoration.isMaximizeMenuActive());
}
@@ -642,7 +642,7 @@
.postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS));
mCloseMaxMenuRunnable.getValue().run();
- verify(menu).close();
+ verify(menu).close(any());
assertFalse(decoration.isMaximizeMenuActive());
}
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 9603c0a..d17a9b6 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -60,7 +60,7 @@
method @FlaggedApi("android.nfc.nfc_oem_extension") @NonNull public java.util.List<java.lang.String> getActiveNfceeList();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
- method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOn(int);
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOnMode(int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull android.nfc.NfcOemExtension.Callback);
field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int DISABLE = 0; // 0x0
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 2804546..951702c 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -560,13 +560,13 @@
public @interface TagIntentAppPreferenceResult {}
/**
- * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+ * Mode Type for {@link NfcOemExtension#setControllerAlwaysOnMode(int)}.
* @hide
*/
public static final int CONTROLLER_ALWAYS_ON_MODE_DEFAULT = 1;
/**
- * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+ * Mode Type for {@link NfcOemExtension#setControllerAlwaysOnMode(int)}.
* @hide
*/
public static final int CONTROLLER_ALWAYS_ON_DISABLE = 0;
@@ -2323,7 +2323,7 @@
* <p>This API is for the NFCC internal state management. It allows to discriminate
* the controller function from the NFC function by keeping the NFC controller on without
* any NFC RF enabled if necessary.
- * <p>This call is asynchronous. Register a listener {@link #ControllerAlwaysOnListener}
+ * <p>This call is asynchronous. Register a listener {@link ControllerAlwaysOnListener}
* by {@link #registerControllerAlwaysOnListener} to find out when the operation is
* complete.
* <p>If this returns true, then either NFCC always on state has been set based on the value,
@@ -2337,7 +2337,7 @@
* FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
* FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
* are unavailable
- * @return true if feature is supported by the device and operation has bee initiated,
+ * @return true if feature is supported by the device and operation has been initiated,
* false if the feature is not supported by the device.
* @hide
*/
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 45038d4..011c60b 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -70,7 +70,7 @@
private boolean mRfDiscoveryStarted = false;
/**
- * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Enables the controller in default mode when NFC is disabled (existing API behavior).
* works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
* @hide
@@ -80,7 +80,7 @@
public static final int ENABLE_DEFAULT = NfcAdapter.CONTROLLER_ALWAYS_ON_MODE_DEFAULT;
/**
- * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Enables the controller in transparent mode when NFC is disabled.
* @hide
*/
@@ -89,7 +89,7 @@
public static final int ENABLE_TRANSPARENT = 2;
/**
- * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Enables the controller and initializes and enables the EE subsystem when NFC is disabled.
* @hide
*/
@@ -98,7 +98,7 @@
public static final int ENABLE_EE = 3;
/**
- * Mode Type for {@link #setControllerAlwaysOn(int)}.
+ * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
* Disable the Controller Always On Mode.
* works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
* @hide
@@ -108,7 +108,7 @@
public static final int DISABLE = NfcAdapter.CONTROLLER_ALWAYS_ON_DISABLE;
/**
- * Possible controller modes for {@link #setControllerAlwaysOn(int)}.
+ * Possible controller modes for {@link #setControllerAlwaysOnMode(int)}.
*
* @hide
*/
@@ -449,6 +449,9 @@
* <p>This call is asynchronous, register listener {@link NfcAdapter.ControllerAlwaysOnListener}
* by {@link NfcAdapter#registerControllerAlwaysOnListener} to find out when the operation is
* complete.
+ * <p> Note: This adds more always on modes on top of existing
+ * {@link NfcAdapter#setControllerAlwaysOn(boolean)} API which can be used to set the NFCC in
+ * only {@link #ENABLE_DEFAULT} and {@link #DISABLE} modes.
* @param mode one of {@link ControllerMode} modes
* @throws UnsupportedOperationException if
* <li> if FEATURE_NFC, FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
@@ -456,11 +459,12 @@
* are unavailable </li>
* <li> if the feature is unavailable @see NfcAdapter#isNfcControllerAlwaysOnSupported() </li>
* @hide
+ * @see NfcAdapter#setControllerAlwaysOn(boolean)
*/
@SystemApi
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
@RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
- public void setControllerAlwaysOn(@ControllerMode int mode) {
+ public void setControllerAlwaysOnMode(@ControllerMode int mode) {
if (!NfcAdapter.sHasNfcFeature && !NfcAdapter.sHasCeFeature) {
throw new UnsupportedOperationException();
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index f64c305..749ad0a 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -967,7 +967,7 @@
for (int i = 0; i < nameCount; i++) {
String name = names.get(i);
Setting setting = settingsState.getSettingLocked(name);
- pw.print("_id:"); pw.print(toDumpString(setting.getId()));
+ pw.print("_id:"); pw.print(toDumpString(String.valueOf(setting.getId())));
pw.print(" name:"); pw.print(toDumpString(name));
if (setting.getPackageName() != null) {
pw.print(" pkg:"); pw.print(setting.getPackageName());
@@ -2785,7 +2785,7 @@
switch (column) {
case Settings.NameValueTable._ID -> {
- values[i] = setting.getId();
+ values[i] = String.valueOf(setting.getId());
}
case Settings.NameValueTable.NAME -> {
values[i] = setting.getName();
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 4165339..3c634f06 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -567,7 +567,7 @@
}
try {
localCounter = Integer.parseInt(markerSetting.value);
- } catch(NumberFormatException e) {
+ } catch (NumberFormatException e) {
// reset local counter
markerSetting.value = "0";
}
@@ -1364,7 +1364,10 @@
}
try {
- if (writeSingleSetting(mVersion, serializer, setting.getId(),
+ if (writeSingleSetting(
+ mVersion,
+ serializer,
+ Long.toString(setting.getId()),
setting.getName(),
setting.getValue(), setting.getDefaultValue(),
setting.getPackageName(),
@@ -1633,7 +1636,7 @@
TypedXmlPullParser parser = Xml.resolvePullParser(in);
parseStateLocked(parser);
return true;
- } catch (XmlPullParserException | IOException e) {
+ } catch (XmlPullParserException | IOException | NumberFormatException e) {
Slog.e(LOG_TAG, "parse settings xml failed", e);
return false;
} finally {
@@ -1653,7 +1656,7 @@
}
private void parseStateLocked(TypedXmlPullParser parser)
- throws IOException, XmlPullParserException {
+ throws IOException, XmlPullParserException, NumberFormatException {
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -1709,7 +1712,7 @@
@GuardedBy("mLock")
private void parseSettingsLocked(TypedXmlPullParser parser)
- throws IOException, XmlPullParserException {
+ throws IOException, XmlPullParserException, NumberFormatException {
mVersion = parser.getAttributeInt(null, ATTR_VERSION);
@@ -1777,7 +1780,7 @@
}
}
mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag,
- fromSystem, id, isPreservedInRestore));
+ fromSystem, Long.valueOf(id), isPreservedInRestore));
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
@@ -1867,7 +1870,7 @@
private String value;
private String defaultValue;
private String packageName;
- private String id;
+ private long id;
private String tag;
// Whether the default is set by the system
private boolean defaultFromSystem;
@@ -1899,30 +1902,27 @@
}
public Setting(String name, String value, String defaultValue,
- String packageName, String tag, boolean fromSystem, String id) {
+ String packageName, String tag, boolean fromSystem, long id) {
this(name, value, defaultValue, packageName, tag, fromSystem, id,
/* isOverrideableByRestore */ false);
}
Setting(String name, String value, String defaultValue,
- String packageName, String tag, boolean fromSystem, String id,
+ String packageName, String tag, boolean fromSystem, long id,
boolean isValuePreservedInRestore) {
- mNextId = Math.max(mNextId, Long.parseLong(id) + 1);
- if (NULL_VALUE.equals(value)) {
- value = null;
- }
+ mNextId = Math.max(mNextId, id + 1);
init(name, value, tag, defaultValue, packageName, fromSystem, id,
isValuePreservedInRestore);
}
private void init(String name, String value, String tag, String defaultValue,
- String packageName, boolean fromSystem, String id,
+ String packageName, boolean fromSystem, long id,
boolean isValuePreservedInRestore) {
this.name = name;
- this.value = value;
+ this.value = internValue(value);
this.tag = tag;
- this.defaultValue = defaultValue;
- this.packageName = packageName;
+ this.defaultValue = internValue(defaultValue);
+ this.packageName = TextUtils.safeIntern(packageName);
this.id = id;
this.defaultFromSystem = fromSystem;
this.isValuePreservedInRestore = isValuePreservedInRestore;
@@ -1960,7 +1960,7 @@
return isValuePreservedInRestore;
}
- public String getId() {
+ public long getId() {
return id;
}
@@ -1993,9 +1993,6 @@
private boolean update(String value, boolean setDefault, String packageName, String tag,
boolean forceNonSystemPackage, boolean overrideableByRestore,
boolean resetToDefault) {
- if (NULL_VALUE.equals(value)) {
- value = null;
- }
final boolean callerSystem = !forceNonSystemPackage &&
!isNull() && (isCalledFromSystem(packageName)
|| isSystemPackage(mContext, packageName));
@@ -2040,7 +2037,7 @@
}
init(name, value, tag, defaultValue, packageName, defaultFromSystem,
- String.valueOf(mNextId++), isPreserved);
+ mNextId++, isPreserved);
return true;
}
@@ -2052,6 +2049,32 @@
+ " defaultFromSystem=" + defaultFromSystem + "}";
}
+ /**
+ * Interns a string if it's a common setting value.
+ * Otherwise returns the given string.
+ */
+ static String internValue(String str) {
+ if (str == null) {
+ return null;
+ }
+ switch (str) {
+ case "true":
+ return "true";
+ case "false":
+ return "false";
+ case "0":
+ return "0";
+ case "1":
+ return "1";
+ case "":
+ return "";
+ case "null":
+ return null; // explicit null has special handling
+ default:
+ return str;
+ }
+ }
+
private boolean shouldPreserveSetting(boolean overrideableByRestore,
boolean resetToDefault, String packageName, String value) {
if (resetToDefault) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 1b99a96..fe4a65b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -130,8 +130,8 @@
fun SceneScope.HeadsUpNotificationSpace(
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
+ useHunBounds: () -> Boolean = { true },
modifier: Modifier = Modifier,
- isPeekFromBottom: Boolean = false,
) {
Box(
modifier =
@@ -141,17 +141,25 @@
.notificationHeadsUpHeight(stackScrollView)
.debugBackground(viewModel, DEBUG_HUN_COLOR)
.onGloballyPositioned { coordinates: LayoutCoordinates ->
- val positionInWindow = coordinates.positionInWindow()
- val boundsInWindow = coordinates.boundsInWindow()
- debugLog(viewModel) {
- "HUNS onGloballyPositioned:" +
- " size=${coordinates.size}" +
- " bounds=$boundsInWindow"
+ // This element is sometimes opted out of the shared element system, so there
+ // can be multiple instances of it during a transition. Thus we need to
+ // determine which instance should feed its bounds to NSSL to avoid providing
+ // conflicting values
+ val useBounds = useHunBounds()
+ if (useBounds) {
+ val positionInWindow = coordinates.positionInWindow()
+ val boundsInWindow = coordinates.boundsInWindow()
+ debugLog(viewModel) {
+ "HUNS onGloballyPositioned:" +
+ " size=${coordinates.size}" +
+ " bounds=$boundsInWindow"
+ }
+ // Note: boundsInWindow doesn't scroll off the screen, so use
+ // positionInWindow
+ // for top bound, which can scroll off screen while snoozing
+ stackScrollView.setHeadsUpTop(positionInWindow.y)
+ stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
}
- // Note: boundsInWindow doesn't scroll off the screen, so use positionInWindow
- // for top bound, which can scroll off screen while snoozing
- stackScrollView.setHeadsUpTop(positionInWindow.y)
- stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
}
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index d91958a..0c69dbd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -416,11 +416,11 @@
HeadsUpNotificationSpace(
stackScrollView = notificationStackScrollView,
viewModel = notificationsPlaceholderViewModel,
+ useHunBounds = { shouldUseQuickSettingsHunBounds(layoutState.transitionState) },
modifier =
Modifier.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(horizontal = shadeHorizontalPadding),
- isPeekFromBottom = true,
)
NotificationScrollingStack(
shadeSession = shadeSession,
@@ -446,3 +446,7 @@
)
}
}
+
+private fun shouldUseQuickSettingsHunBounds(state: TransitionState): Boolean {
+ return state is TransitionState.Idle && state.currentScene == Scenes.QuickSettings
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index f64d0ed..58fbf43 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -77,6 +77,10 @@
}
from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() }
from(Scenes.Lockscreen, to = Scenes.Gone) { lockscreenToGoneTransition() }
+ from(Scenes.QuickSettings, to = Scenes.Shade) {
+ reversed { shadeToQuickSettingsTransition() }
+ sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
+ }
from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
// Overlay transitions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
index e58cf15..79a303d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractorTest.kt
@@ -85,12 +85,12 @@
runCurrent()
// Assert that the tile is removed from the large tiles after resizing
- underTest.resize(largeTile)
+ underTest.resize(largeTile, toIcon = true)
runCurrent()
assertThat(latest).doesNotContain(largeTile)
// Assert that the tile is added to the large tiles after resizing
- underTest.resize(largeTile)
+ underTest.resize(largeTile, toIcon = false)
runCurrent()
assertThat(latest).contains(largeTile)
}
@@ -122,7 +122,7 @@
val newTile = TileSpec.create("newTile")
// Remove the large tile from the current tiles
- underTest.resize(newTile)
+ underTest.resize(newTile, toIcon = false)
runCurrent()
// Assert that it's still small
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
index 484a8ff..3910903 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -24,7 +24,6 @@
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.SizedTileImpl
import com.android.systemui.qs.panels.ui.model.GridCell
-import com.android.systemui.qs.panels.ui.model.SpacerGridCell
import com.android.systemui.qs.panels.ui.model.TileGridCell
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -39,13 +38,6 @@
private val underTest = EditTileListState(TestEditTiles, 4)
@Test
- fun noDrag_listUnchanged() {
- underTest.tiles.forEach { assertThat(it).isNotInstanceOf(SpacerGridCell::class.java) }
- assertThat(underTest.tiles.map { (it as TileGridCell).tile.tileSpec })
- .containsExactly(*TestEditTiles.map { it.tile.tileSpec }.toTypedArray())
- }
-
- @Test
fun startDrag_listHasSpacers() {
underTest.onStarted(TestEditTiles[0])
@@ -109,16 +101,6 @@
}
@Test
- fun droppedNewTile_spacersDisappear() {
- underTest.onStarted(TestEditTiles[0])
- underTest.onDrop()
-
- assertThat(underTest.tiles.toStrings()).isEqualTo(listOf("a", "b", "c", "d", "e"))
- assertThat(underTest.isMoving(TestEditTiles[0].tile.tileSpec)).isFalse()
- assertThat(underTest.dragInProgress).isFalse()
- }
-
- @Test
fun movedTileOutOfBounds_tileDisappears() {
underTest.onStarted(TestEditTiles[0])
underTest.movedOutOfBounds()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
index fa72d74..4acf3ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionStateTest.kt
@@ -27,23 +27,25 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class MutableSelectionStateTest : SysuiTestCase() {
- private val underTest = MutableSelectionState()
+ private val underTest = MutableSelectionState({}, {})
@Test
fun selectTile_isCorrectlySelected() {
- assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+ assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC)
- underTest.select(TEST_SPEC)
- assertThat(underTest.isSelected(TEST_SPEC)).isTrue()
+ underTest.select(TEST_SPEC, manual = true)
+ assertThat(underTest.selection?.tileSpec).isEqualTo(TEST_SPEC)
+ assertThat(underTest.selection?.manual).isTrue()
underTest.unSelect()
- assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
+ assertThat(underTest.selection).isNull()
val newSpec = TileSpec.create("newSpec")
- underTest.select(TEST_SPEC)
- underTest.select(newSpec)
- assertThat(underTest.isSelected(TEST_SPEC)).isFalse()
- assertThat(underTest.isSelected(newSpec)).isTrue()
+ underTest.select(TEST_SPEC, manual = true)
+ underTest.select(newSpec, manual = false)
+ assertThat(underTest.selection?.tileSpec).isNotEqualTo(TEST_SPEC)
+ assertThat(underTest.selection?.tileSpec).isEqualTo(newSpec)
+ assertThat(underTest.selection?.manual).isFalse()
}
@Test
@@ -51,12 +53,12 @@
assertThat(underTest.resizingState).isNull()
// Resizing starts but no tile is selected
- underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+ underTest.onResizingDragStart(TileWidths(0, 0, 1))
assertThat(underTest.resizingState).isNull()
// Resizing starts with a selected tile
- underTest.select(TEST_SPEC)
- underTest.onResizingDragStart(TileWidths(0, 0, 1)) {}
+ underTest.select(TEST_SPEC, manual = true)
+ underTest.onResizingDragStart(TileWidths(0, 0, 1))
assertThat(underTest.resizingState).isNotNull()
}
@@ -66,8 +68,8 @@
val spec = TileSpec.create("testSpec")
// Resizing starts with a selected tile
- underTest.select(spec)
- underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+ underTest.select(spec, manual = true)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
assertThat(underTest.resizingState).isNotNull()
underTest.onResizingDragEnd()
@@ -77,8 +79,8 @@
@Test
fun unselect_clearsResizingState() {
// Resizing starts with a selected tile
- underTest.select(TEST_SPEC)
- underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+ underTest.select(TEST_SPEC, manual = true)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
assertThat(underTest.resizingState).isNotNull()
underTest.unSelect()
@@ -88,8 +90,8 @@
@Test
fun onResizingDrag_updatesResizingState() {
// Resizing starts with a selected tile
- underTest.select(TEST_SPEC)
- underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10)) {}
+ underTest.select(TEST_SPEC, manual = true)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
assertThat(underTest.resizingState).isNotNull()
underTest.onResizingDrag(5f)
@@ -105,11 +107,15 @@
@Test
fun onResizingDrag_receivesResizeCallback() {
var resized = false
- val onResize: () -> Unit = { resized = !resized }
+ val onResize: (TileSpec) -> Unit = {
+ assertThat(it).isEqualTo(TEST_SPEC)
+ resized = !resized
+ }
+ val underTest = MutableSelectionState(onResize = onResize, {})
// Resizing starts with a selected tile
- underTest.select(TEST_SPEC)
- underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10), onResize)
+ underTest.select(TEST_SPEC, true)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
assertThat(underTest.resizingState).isNotNull()
// Drag under the threshold
@@ -125,6 +131,37 @@
assertThat(resized).isFalse()
}
+ @Test
+ fun onResizingEnded_receivesResizeEndCallback() {
+ var resizeEnded = false
+ val onResizeEnd: (TileSpec) -> Unit = { resizeEnded = true }
+ val underTest = MutableSelectionState({}, onResizeEnd = onResizeEnd)
+
+ // Resizing starts with a selected tile
+ underTest.select(TEST_SPEC, true)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
+
+ underTest.onResizingDragEnd()
+ assertThat(resizeEnded).isTrue()
+ }
+
+ @Test
+ fun onResizingEnded_setsSelectionAutomatically() {
+ val underTest = MutableSelectionState({}, {})
+
+ // Resizing starts with a selected tile
+ underTest.select(TEST_SPEC, manual = true)
+ underTest.onResizingDragStart(TileWidths(base = 0, min = 0, max = 10))
+
+ // Assert the selection was manual
+ assertThat(underTest.selection?.manual).isTrue()
+
+ underTest.onResizingDragEnd()
+
+ // Assert the selection is no longer manual due to the resizing
+ assertThat(underTest.selection?.manual).isFalse()
+ }
+
companion object {
private val TEST_SPEC = TileSpec.create("testSpec")
}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index a3db776..24b6579 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1482,7 +1482,7 @@
<!-- Text which is shown in the expanded notification shade when there are currently no notifications visible that the user hasn't already seen. [CHAR LIMIT=30] -->
<string name="no_unseen_notif_text">No new notifications</string>
- <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=50] -->
+ <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=60] -->
<string name="adaptive_notification_edu_hun_title">Notification cooldown is on</string>
<!-- Text of heads up notification for adaptive notifications user education. [CHAR LIMIT=100] -->
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
index 02a607d..fc59a50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
@@ -40,7 +40,7 @@
private val currentTilesInteractor: CurrentTilesInteractor,
private val preferencesInteractor: QSPreferencesInteractor,
@PanelsLog private val logBuffer: LogBuffer,
- @Application private val applicationScope: CoroutineScope
+ @Application private val applicationScope: CoroutineScope,
) {
val largeTilesSpecs =
@@ -64,14 +64,15 @@
fun isIconTile(spec: TileSpec): Boolean = !largeTilesSpecs.value.contains(spec)
- fun resize(spec: TileSpec) {
+ fun resize(spec: TileSpec, toIcon: Boolean) {
if (!isCurrent(spec)) {
return
}
- if (largeTilesSpecs.value.contains(spec)) {
+ val isIcon = !largeTilesSpecs.value.contains(spec)
+ if (toIcon && !isIcon) {
preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value - spec)
- } else {
+ } else if (!toIcon && isIcon) {
preferencesInteractor.setLargeTilesSpecs(largeTilesSpecs.value + spec)
}
}
@@ -85,7 +86,7 @@
LOG_BUFFER_LARGE_TILES_SPECS_CHANGE_TAG,
LogLevel.DEBUG,
{ str1 = specs.toString() },
- { "Large tiles change: $str1" }
+ { "Large tiles change: $str1" },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index a4f977b..770fd78 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -60,10 +60,37 @@
return _tiles.filterIsInstance<TileGridCell>().map { it.tile.tileSpec }
}
- fun indexOf(tileSpec: TileSpec): Int {
+ private fun indexOf(tileSpec: TileSpec): Int {
return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec }
}
+ /**
+ * Whether the tile with this [TileSpec] is currently an icon in the [EditTileListState]
+ *
+ * @return true if the tile is an icon, false if it's large, null if the tile isn't in the list
+ */
+ fun isIcon(tileSpec: TileSpec): Boolean? {
+ val index = indexOf(tileSpec)
+ return if (index != -1) {
+ val cell = _tiles[index]
+ cell as TileGridCell
+ return cell.isIcon
+ } else {
+ null
+ }
+ }
+
+ /** Toggle the size of the tile corresponding to the [TileSpec] */
+ fun toggleSize(tileSpec: TileSpec) {
+ val fromIndex = indexOf(tileSpec)
+ if (fromIndex != -1) {
+ val cell = _tiles.removeAt(fromIndex)
+ cell as TileGridCell
+ _tiles.add(fromIndex, cell.copy(width = if (cell.isIcon) 2 else 1))
+ regenerateGrid(fromIndex)
+ }
+ }
+
override fun isMoving(tileSpec: TileSpec): Boolean {
return _draggedCell.value?.let { it.tile.tileSpec == tileSpec } ?: false
}
@@ -71,8 +98,8 @@
override fun onStarted(cell: SizedTile<EditTileViewModel>) {
_draggedCell.value = cell
- // Add visible spacers to the grid to indicate where the user can move a tile
- regenerateGrid(includeSpacers = true)
+ // Add spacers to the grid to indicate where the user can move a tile
+ regenerateGrid()
}
override fun onMoved(target: Int, insertAfter: Boolean) {
@@ -86,7 +113,7 @@
val insertionIndex = if (insertAfter) target + 1 else target
if (fromIndex != -1) {
val cell = _tiles.removeAt(fromIndex)
- regenerateGrid(includeSpacers = true)
+ regenerateGrid()
_tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
} else {
// Add the tile with a temporary row which will get reassigned when
@@ -94,7 +121,7 @@
_tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0))
}
- regenerateGrid(includeSpacers = true)
+ regenerateGrid()
}
override fun movedOutOfBounds() {
@@ -109,13 +136,28 @@
_draggedCell.value = null
// Remove the spacers
- regenerateGrid(includeSpacers = false)
+ regenerateGrid()
}
- private fun regenerateGrid(includeSpacers: Boolean) {
- _tiles.filterIsInstance<TileGridCell>().toGridCells(columns, includeSpacers).let {
+ /** Regenerate the list of [GridCell] with their new potential rows */
+ private fun regenerateGrid() {
+ _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
_tiles.clear()
_tiles.addAll(it)
}
}
+
+ /**
+ * Regenerate the list of [GridCell] with their new potential rows from [fromIndex], leaving
+ * cells before that untouched.
+ */
+ private fun regenerateGrid(fromIndex: Int) {
+ val fromRow = _tiles[fromIndex].row
+ val (pre, post) = _tiles.partition { it.row < fromRow }
+ post.filterIsInstance<TileGridCell>().toGridCells(columns, startingRow = fromRow).let {
+ _tiles.clear()
+ _tiles.addAll(pre)
+ _tiles.addAll(it)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 0e76e18..30bafae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -132,15 +132,23 @@
@Composable
fun DefaultEditTileGrid(
- currentListState: EditTileListState,
+ listState: EditTileListState,
otherTiles: List<SizedTile<EditTileViewModel>>,
columns: Int,
modifier: Modifier,
onRemoveTile: (TileSpec) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
- onResize: (TileSpec) -> Unit,
+ onResize: (TileSpec, toIcon: Boolean) -> Unit,
) {
- val selectionState = rememberSelectionState()
+ val currentListState by rememberUpdatedState(listState)
+ val selectionState =
+ rememberSelectionState(
+ onResize = { currentListState.toggleSize(it) },
+ onResizeEnd = { spec ->
+ // Commit the size currently in the list
+ currentListState.isIcon(spec)?.let { onResize(spec, it) }
+ },
+ )
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
Column(
@@ -149,11 +157,11 @@
modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()),
) {
AnimatedContent(
- targetState = currentListState.dragInProgress,
+ targetState = listState.dragInProgress,
modifier = Modifier.wrapContentSize(),
label = "",
) { dragIsInProgress ->
- EditGridHeader(Modifier.dragAndDropRemoveZone(currentListState, onRemoveTile)) {
+ EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) {
if (dragIsInProgress) {
RemoveTileTarget()
} else {
@@ -162,11 +170,11 @@
}
}
- CurrentTilesGrid(currentListState, selectionState, columns, onResize, onSetTiles)
+ CurrentTilesGrid(listState, selectionState, columns, onResize, onSetTiles)
// Hide available tiles when dragging
AnimatedVisibility(
- visible = !currentListState.dragInProgress,
+ visible = !listState.dragInProgress,
enter = fadeIn(),
exit = fadeOut(),
) {
@@ -177,7 +185,7 @@
) {
EditGridHeader { Text(text = "Hold and drag to add tiles.") }
- AvailableTileGrid(otherTiles, selectionState, columns, currentListState)
+ AvailableTileGrid(otherTiles, selectionState, columns, listState)
}
}
@@ -186,7 +194,7 @@
modifier =
Modifier.fillMaxWidth()
.weight(1f)
- .dragAndDropRemoveZone(currentListState, onRemoveTile)
+ .dragAndDropRemoveZone(listState, onRemoveTile)
)
}
}
@@ -229,7 +237,7 @@
listState: EditTileListState,
selectionState: MutableSelectionState,
columns: Int,
- onResize: (TileSpec) -> Unit,
+ onResize: (TileSpec, toIcon: Boolean) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
) {
val currentListState by rememberUpdatedState(listState)
@@ -242,19 +250,6 @@
)
val gridState = rememberLazyGridState()
var gridContentOffset by remember { mutableStateOf(Offset(0f, 0f)) }
- var droppedSpec by remember { mutableStateOf<TileSpec?>(null) }
-
- // Select the tile that was dropped. A delay is introduced to avoid clipping issues on the
- // selected border and resizing handle, as well as letting the selection animation play.
- LaunchedEffect(droppedSpec) {
- droppedSpec?.let {
- delay(200)
- selectionState.select(it)
-
- // Reset droppedSpec in case a tile is dropped twice in a row
- droppedSpec = null
- }
- }
TileLazyGrid(
state = gridState,
@@ -270,14 +265,17 @@
)
.dragAndDropTileList(gridState, { gridContentOffset }, listState) { spec ->
onSetTiles(currentListState.tileSpecs())
- droppedSpec = spec
+ selectionState.select(spec, manual = false)
}
.onGloballyPositioned { coordinates ->
gridContentOffset = coordinates.positionInRoot()
}
.testTag(CURRENT_TILES_GRID_TEST_TAG),
) {
- EditTiles(listState.tiles, listState, selectionState, onResize)
+ EditTiles(listState.tiles, listState, selectionState) { spec ->
+ // Toggle the current size of the tile
+ currentListState.isIcon(spec)?.let { onResize(spec, !it) }
+ }
}
}
@@ -348,11 +346,19 @@
}
}
+/**
+ * Adds a list of [GridCell] to the lazy grid
+ *
+ * @param cells the list of [GridCell]
+ * @param dragAndDropState the [DragAndDropState] for this grid
+ * @param selectionState the [MutableSelectionState] for this grid
+ * @param onToggleSize the callback when a tile's size is toggled
+ */
fun LazyGridScope.EditTiles(
cells: List<GridCell>,
dragAndDropState: DragAndDropState,
selectionState: MutableSelectionState,
- onResize: (TileSpec) -> Unit,
+ onToggleSize: (spec: TileSpec) -> Unit,
) {
items(
count = cells.size,
@@ -378,7 +384,7 @@
index = index,
dragAndDropState = dragAndDropState,
selectionState = selectionState,
- onResize = onResize,
+ onToggleSize = onToggleSize,
)
}
is SpacerGridCell -> SpacerGridCell()
@@ -392,16 +398,28 @@
index: Int,
dragAndDropState: DragAndDropState,
selectionState: MutableSelectionState,
- onResize: (TileSpec) -> Unit,
+ onToggleSize: (spec: TileSpec) -> Unit,
) {
- val selected = selectionState.isSelected(cell.tile.tileSpec)
val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
+ var selected by remember { mutableStateOf(false) }
val selectionAlpha by
animateFloatAsState(
targetValue = if (selected) 1f else 0f,
label = "QSEditTileSelectionAlpha",
)
+ LaunchedEffect(selectionState.selection?.tileSpec) {
+ selectionState.selection?.let {
+ // A delay is introduced on automatic selections such as dragged tiles or reflow caused
+ // by resizing. This avoids clipping issues on the border and resizing handle, as well
+ // as letting the selection animation play correctly.
+ if (!it.manual) {
+ delay(250)
+ }
+ }
+ selected = selectionState.selection?.tileSpec == cell.tile.tileSpec
+ }
+
val modifier =
Modifier.animateItem()
.semantics(mergeDescendants = true) {
@@ -411,7 +429,7 @@
listOf(
// TODO(b/367748260): Add final accessibility actions
CustomAccessibilityAction("Toggle size") {
- onResize(cell.tile.tileSpec)
+ onToggleSize(cell.tile.tileSpec)
true
}
)
@@ -438,11 +456,9 @@
if (selected) {
SelectedTile(
- tileSpec = cell.tile.tileSpec,
isIcon = cell.isIcon,
selectionAlpha = { selectionAlpha },
selectionState = selectionState,
- onResize = onResize,
modifier = modifier.zIndex(2f), // 2f to display this tile over neighbors when dragged
content = content,
)
@@ -458,11 +474,9 @@
@Composable
private fun SelectedTile(
- tileSpec: TileSpec,
isIcon: Boolean,
selectionAlpha: () -> Float,
selectionState: MutableSelectionState,
- onResize: (TileSpec) -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
@@ -492,9 +506,7 @@
selectionState = selectionState,
transition = selectionAlpha,
tileWidths = { tileWidths },
- ) {
- onResize(tileSpec)
- }
+ )
}
Layout(contents = listOf(content, handle)) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index 8a96065..e6edba5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -101,7 +101,7 @@
val (currentTiles, otherTiles) = sizedTiles.partition { it.tile.isCurrent }
val currentListState = rememberEditListState(currentTiles, columns)
DefaultEditTileGrid(
- currentListState = currentListState,
+ listState = currentListState,
otherTiles = otherTiles,
columns = columns,
modifier = modifier,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
index 2ea32e6..441d962 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -27,28 +27,39 @@
/** Creates the state of the current selected tile that is remembered across compositions. */
@Composable
-fun rememberSelectionState(): MutableSelectionState {
- return remember { MutableSelectionState() }
+fun rememberSelectionState(
+ onResize: (TileSpec) -> Unit,
+ onResizeEnd: (TileSpec) -> Unit,
+): MutableSelectionState {
+ return remember { MutableSelectionState(onResize, onResizeEnd) }
}
+/**
+ * Holds the selected [TileSpec] and whether the selection was manual, i.e. caused by a tap from the
+ * user.
+ */
+data class Selection(val tileSpec: TileSpec, val manual: Boolean)
+
/** Holds the state of the current selection. */
-class MutableSelectionState {
- private var _selectedTile = mutableStateOf<TileSpec?>(null)
+class MutableSelectionState(
+ val onResize: (TileSpec) -> Unit,
+ private val onResizeEnd: (TileSpec) -> Unit,
+) {
+ private var _selection = mutableStateOf<Selection?>(null)
private var _resizingState = mutableStateOf<ResizingState?>(null)
+ /** The [Selection] if a tile is selected, null if not. */
+ val selection by _selection
+
/** The [ResizingState] of the selected tile is currently being resized, null if not. */
val resizingState by _resizingState
- fun isSelected(tileSpec: TileSpec): Boolean {
- return _selectedTile.value?.let { it == tileSpec } ?: false
- }
-
- fun select(tileSpec: TileSpec) {
- _selectedTile.value = tileSpec
+ fun select(tileSpec: TileSpec, manual: Boolean) {
+ _selection.value = Selection(tileSpec, manual)
}
fun unSelect() {
- _selectedTile.value = null
+ _selection.value = null
onResizingDragEnd()
}
@@ -56,14 +67,21 @@
_resizingState.value?.onDrag(offset)
}
- fun onResizingDragStart(tileWidths: TileWidths, onResize: () -> Unit) {
- if (_selectedTile.value == null) return
-
- _resizingState.value = ResizingState(tileWidths, onResize)
+ fun onResizingDragStart(tileWidths: TileWidths) {
+ _selection.value?.let {
+ _resizingState.value = ResizingState(tileWidths) { onResize(it.tileSpec) }
+ }
}
fun onResizingDragEnd() {
_resizingState.value = null
+ _selection.value?.let {
+ onResizeEnd(it.tileSpec)
+
+ // Mark the selection as automatic in case the tile ends up moving to a different
+ // row with its new size.
+ _selection.value = it.copy(manual = false)
+ }
}
}
@@ -76,10 +94,10 @@
return pointerInput(Unit) {
detectTapGestures(
onTap = {
- if (selectionState.isSelected(tileSpec)) {
+ if (selectionState.selection?.tileSpec == tileSpec) {
selectionState.unSelect()
} else {
- selectionState.select(tileSpec)
+ selectionState.select(tileSpec, manual = true)
}
}
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index e3acf38..7c62e59 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -45,7 +45,6 @@
selectionState: MutableSelectionState,
transition: () -> Float,
tileWidths: () -> TileWidths? = { null },
- onResize: () -> Unit = {},
) {
if (enabled) {
// Manually creating the touch target around the resizing dot to ensure that the next tile
@@ -56,9 +55,7 @@
Modifier.size(minTouchTargetSize).pointerInput(Unit) {
detectHorizontalDragGestures(
onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
- onDragStart = {
- tileWidths()?.let { selectionState.onResizingDragStart(it, onResize) }
- },
+ onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } },
onDragEnd = selectionState::onResizingDragEnd,
onDragCancel = selectionState::onResizingDragEnd,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
index b16a707..b1841c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
@@ -27,6 +27,7 @@
sealed interface GridCell {
val row: Int
val span: GridItemSpan
+ val s: String
}
/**
@@ -39,6 +40,7 @@
override val row: Int,
override val width: Int,
override val span: GridItemSpan = GridItemSpan(width),
+ override val s: String = "${tile.tileSpec.spec}-$row-$width",
) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile {
val key: String = "${tile.tileSpec.spec}-$row"
@@ -53,22 +55,30 @@
data class SpacerGridCell(
override val row: Int,
override val span: GridItemSpan = GridItemSpan(1),
+ override val s: String = "spacer",
) : GridCell
+/**
+ * Generates a list of [GridCell] from a list of [SizedTile]
+ *
+ * Builds rows based on the tiles' widths, and fill each hole with a [SpacerGridCell]
+ *
+ * @param startingRow The row index the grid is built from, used in cases where only end rows need
+ * to be regenerated
+ */
fun List<SizedTile<EditTileViewModel>>.toGridCells(
columns: Int,
- includeSpacers: Boolean = false,
+ startingRow: Int = 0,
): List<GridCell> {
return splitInRowsSequence(this, columns)
.flatMapIndexed { rowIndex, sizedTiles ->
- val row: List<GridCell> = sizedTiles.map { TileGridCell(it, rowIndex) }
+ val correctedRowIndex = rowIndex + startingRow
+ val row: List<GridCell> = sizedTiles.map { TileGridCell(it, correctedRowIndex) }
- if (includeSpacers) {
- // Fill the incomplete rows with spacers
- val numSpacers = columns - sizedTiles.sumOf { it.width }
- row.toMutableList().apply { repeat(numSpacers) { add(SpacerGridCell(rowIndex)) } }
- } else {
- row
+ // Fill the incomplete rows with spacers
+ val numSpacers = columns - sizedTiles.sumOf { it.width }
+ row.toMutableList().apply {
+ repeat(numSpacers) { add(SpacerGridCell(correctedRowIndex)) }
}
}
.toList()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
index b604e18..4e698ed 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
@@ -27,7 +27,7 @@
fun isIconTile(spec: TileSpec): Boolean
- fun resize(spec: TileSpec)
+ fun resize(spec: TileSpec, toIcon: Boolean)
}
@SysUISingleton
@@ -37,5 +37,5 @@
override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec)
- override fun resize(spec: TileSpec) = interactor.resize(spec)
+ override fun resize(spec: TileSpec, toIcon: Boolean) = interactor.resize(spec, toIcon)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 9b96931..6907eef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -106,7 +106,8 @@
@VisibleForTesting
public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
= new ArraySet<>();
- private boolean mIsExpanded;
+ private boolean mIsShadeOrQsExpanded;
+ private boolean mIsQsExpanded;
private int mStatusBarState;
private AnimationStateHandler mAnimationStateHandler;
@@ -178,6 +179,10 @@
});
javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
this::onShadeOrQsExpanded);
+ if (SceneContainerFlag.isEnabled()) {
+ javaAdapter.alwaysCollectFlow(shadeInteractor.isQsExpanded(),
+ this::onQsExpanded);
+ }
if (NotificationThrottleHun.isEnabled()) {
mVisualStabilityProvider.addPersistentReorderingBannedListener(
mOnReorderingBannedListener);
@@ -287,14 +292,19 @@
}
private void onShadeOrQsExpanded(Boolean isExpanded) {
- if (isExpanded != mIsExpanded) {
- mIsExpanded = isExpanded;
+ if (isExpanded != mIsShadeOrQsExpanded) {
+ mIsShadeOrQsExpanded = isExpanded;
if (!SceneContainerFlag.isEnabled() && isExpanded) {
mHeadsUpAnimatingAway.setValue(false);
}
}
}
+ private void onQsExpanded(Boolean isQsExpanded) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ if (isQsExpanded != mIsQsExpanded) mIsQsExpanded = isQsExpanded;
+ }
+
/**
* Set that we are exiting the headsUp pinned mode, but some notifications might still be
* animating out. This is used to keep the touchable regions in a reasonable state.
@@ -490,7 +500,10 @@
@Override
protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
- boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded;
+ boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsShadeOrQsExpanded;
+ if (SceneContainerFlag.isEnabled()) {
+ pin |= mIsQsExpanded;
+ }
if (mBypassController.getBypassEnabled()) {
pin |= mStatusBarState == StatusBarState.KEYGUARD;
}
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 48796d8..b108388 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
@@ -634,8 +634,10 @@
}
private View inflateDivider() {
- return LayoutInflater.from(mContext).inflate(
+ View divider = LayoutInflater.from(mContext).inflate(
R.layout.notification_children_divider, this, false);
+ divider.setAlpha(0f);
+ return divider;
}
/**
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 e7c67f9..3c6962a 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
@@ -1260,6 +1260,7 @@
@Override
public void setHeadsUpBottom(float headsUpBottom) {
mAmbientState.setHeadsUpBottom(headsUpBottom);
+ mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom));
}
@Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index 6423d25..8d060e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -60,13 +60,13 @@
onSetTiles: (List<TileSpec>) -> Unit,
) {
DefaultEditTileGrid(
- currentListState = listState,
+ listState = listState,
otherTiles = listOf(),
columns = 4,
modifier = Modifier.fillMaxSize(),
onRemoveTile = {},
onSetTiles = onSetTiles,
- onResize = {},
+ onResize = { _, _ -> },
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index 682ed92..ee1c0e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -25,7 +25,11 @@
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performCustomAccessibilityActionWithLabel
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -43,15 +47,19 @@
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalTestApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ResizingTest : SysuiTestCase() {
@get:Rule val composeRule = createComposeRule()
@Composable
- private fun EditTileGridUnderTest(listState: EditTileListState, onResize: (TileSpec) -> Unit) {
+ private fun EditTileGridUnderTest(
+ listState: EditTileListState,
+ onResize: (TileSpec, Boolean) -> Unit,
+ ) {
DefaultEditTileGrid(
- currentListState = listState,
+ listState = listState,
otherTiles = listOf(),
columns = 4,
modifier = Modifier.fillMaxSize(),
@@ -61,22 +69,12 @@
)
}
- @OptIn(ExperimentalTestApi::class)
@Test
- fun resizedIcon_shouldBeLarge() {
+ fun toggleIconTile_shouldBeLarge() {
var tiles by mutableStateOf(TestEditTiles)
val listState = EditTileListState(tiles, 4)
composeRule.setContent {
- EditTileGridUnderTest(listState) { spec ->
- tiles =
- tiles.map {
- if (it.tile.tileSpec == spec) {
- toggleWidth(it)
- } else {
- it
- }
- }
- }
+ EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
}
composeRule.waitForIdle()
@@ -87,22 +85,12 @@
assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(2)
}
- @OptIn(ExperimentalTestApi::class)
@Test
- fun resizedLarge_shouldBeIcon() {
+ fun toggleLargeTile_shouldBeIcon() {
var tiles by mutableStateOf(TestEditTiles)
val listState = EditTileListState(tiles, 4)
composeRule.setContent {
- EditTileGridUnderTest(listState) { spec ->
- tiles =
- tiles.map {
- if (it.tile.tileSpec == spec) {
- toggleWidth(it)
- } else {
- it
- }
- }
- }
+ EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
}
composeRule.waitForIdle()
@@ -113,9 +101,58 @@
assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1)
}
+ @Test
+ fun resizedLarge_shouldBeIcon() {
+ var tiles by mutableStateOf(TestEditTiles)
+ val listState = EditTileListState(tiles, 4)
+ composeRule.setContent {
+ EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
+ }
+ composeRule.waitForIdle()
+
+ composeRule
+ .onNodeWithContentDescription("tileA")
+ .performClick() // Select
+ .performTouchInput { // Resize down
+ swipeRight()
+ }
+ composeRule.waitForIdle()
+
+ assertThat(tiles.find { it.tile.tileSpec.spec == "tileA" }?.width).isEqualTo(1)
+ }
+
+ @Test
+ fun resizedIcon_shouldBeLarge() {
+ var tiles by mutableStateOf(TestEditTiles)
+ val listState = EditTileListState(tiles, 4)
+ composeRule.setContent {
+ EditTileGridUnderTest(listState) { spec, toIcon -> tiles = tiles.resize(spec, toIcon) }
+ }
+ composeRule.waitForIdle()
+
+ composeRule
+ .onNodeWithContentDescription("tileD_large")
+ .performClick() // Select
+ .performTouchInput { // Resize down
+ swipeLeft()
+ }
+ composeRule.waitForIdle()
+
+ assertThat(tiles.find { it.tile.tileSpec.spec == "tileD_large" }?.width).isEqualTo(1)
+ }
+
companion object {
- private fun toggleWidth(tile: SizedTile<EditTileViewModel>): SizedTile<EditTileViewModel> {
- return SizedTileImpl(tile.tile, width = if (tile.isIcon) 2 else 1)
+ private fun List<SizedTile<EditTileViewModel>>.resize(
+ spec: TileSpec,
+ toIcon: Boolean,
+ ): List<SizedTile<EditTileViewModel>> {
+ return map {
+ if (it.tile.tileSpec == spec) {
+ SizedTileImpl(it.tile, width = if (toIcon) 1 else 2)
+ } else {
+ it
+ }
+ }
}
private fun createEditTile(tileSpec: String): SizedTile<EditTileViewModel> {
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index d4d188d..86246e2 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -34,15 +34,18 @@
},
{
"name": "CarLibHostUnitTest",
- "host": true
+ "host": true,
+ "keywords": ["automotive_code_coverage"]
},
{
"name": "CarServiceHostUnitTest",
- "host": true
+ "host": true,
+ "keywords": ["automotive_code_coverage"]
},
{
"name": "CarSystemUIRavenTests",
- "host": true
+ "host": true,
+ "keywords": ["automotive_code_coverage"]
},
{
"name": "CtsAccountManagerTestCasesRavenwood",
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 0e19347..210301e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -133,6 +133,7 @@
import com.android.server.am.nano.VMInfo;
import com.android.server.compat.PlatformCompat;
import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.AnrTimer;
import com.android.server.utils.Slogf;
import dalvik.annotation.optimization.NeverCompile;
@@ -285,6 +286,8 @@
return -1;
case "trace-ipc":
return runTraceIpc(pw);
+ case "trace-timer":
+ return runTraceTimer(pw);
case "profile":
return runProfile(pw);
case "dumpheap":
@@ -1062,6 +1065,23 @@
return 0;
}
+ // Update AnrTimer tracing.
+ private int runTraceTimer(PrintWriter pw) throws RemoteException {
+ if (!AnrTimer.traceFeatureEnabled()) return -1;
+
+ // Delegate all argument parsing to the AnrTimer method.
+ try {
+ final String result = AnrTimer.traceTimers(peekRemainingArgs());
+ if (result != null) {
+ pw.println(result);
+ }
+ return 0;
+ } catch (IllegalArgumentException e) {
+ getErrPrintWriter().println("Error: bad trace-timer command: " + e);
+ return -1;
+ }
+ }
+
// NOTE: current profiles can only be started on default display (even on automotive builds with
// passenger displays), so there's no need to pass a display-id
private int runProfile(PrintWriter pw) throws RemoteException {
@@ -4352,6 +4372,7 @@
pw.println(" start: start tracing IPC transactions.");
pw.println(" stop: stop tracing IPC transactions and dump the results to file.");
pw.println(" --dump-file <FILE>: Specify the file the trace should be dumped to.");
+ anrTimerHelp(pw);
pw.println(" profile start [--user <USER_ID> current]");
pw.println(" [--clock-type <TYPE>]");
pw.println(" [" + PROFILER_OUTPUT_VERSION_FLAG + " VERSION]");
@@ -4605,4 +4626,19 @@
Intent.printIntentArgsHelp(pw, "");
}
}
+
+ static void anrTimerHelp(PrintWriter pw) {
+ // Return silently if tracing is not feature-enabled.
+ if (!AnrTimer.traceFeatureEnabled()) return;
+
+ String h = AnrTimer.traceTimers(new String[]{"help"});
+ if (h == null) {
+ return;
+ }
+
+ pw.println(" trace-timer <cmd>");
+ for (String s : h.split("\n")) {
+ pw.println(" " + s);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index f7085b4..15f1085 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -57,6 +57,7 @@
import android.app.ApplicationThreadConstants;
import android.app.BackgroundStartPrivileges;
import android.app.BroadcastOptions;
+import android.app.BroadcastStickyCache;
import android.app.IApplicationThread;
import android.app.compat.CompatChanges;
import android.appwidget.AppWidgetManager;
@@ -685,6 +686,7 @@
boolean serialized, boolean sticky, int userId) {
mService.enforceNotIsolatedCaller("broadcastIntent");
+ int result;
synchronized (mService) {
intent = verifyBroadcastLocked(intent);
@@ -706,7 +708,7 @@
final long origId = Binder.clearCallingIdentity();
try {
- return broadcastIntentLocked(callerApp,
+ result = broadcastIntentLocked(callerApp,
callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
@@ -717,6 +719,10 @@
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
}
+ if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+ BroadcastStickyCache.incrementVersion(intent.getAction());
+ }
+ return result;
}
// Not the binder call surface
@@ -727,6 +733,7 @@
boolean serialized, boolean sticky, int userId,
BackgroundStartPrivileges backgroundStartPrivileges,
@Nullable int[] broadcastAllowList) {
+ int result;
synchronized (mService) {
intent = verifyBroadcastLocked(intent);
@@ -734,7 +741,7 @@
String[] requiredPermissions = requiredPermission == null ? null
: new String[] {requiredPermission};
try {
- return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
+ result = broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
resultToApp, resultTo, resultCode, resultData, resultExtras,
requiredPermissions, null, null, OP_NONE, bOptions, serialized, sticky, -1,
uid, realCallingUid, realCallingPid, userId,
@@ -744,6 +751,10 @@
Binder.restoreCallingIdentity(origId);
}
}
+ if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+ BroadcastStickyCache.incrementVersion(intent.getAction());
+ }
+ return result;
}
@GuardedBy("mService")
@@ -1442,6 +1453,7 @@
list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive,
callingUid, callerAppProcessState, resolvedType));
}
+ BroadcastStickyCache.incrementVersion(intent.getAction());
}
}
@@ -1708,6 +1720,7 @@
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
+ final ArrayList<String> changedStickyBroadcasts = new ArrayList<>();
synchronized (mStickyBroadcasts) {
ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
if (stickies != null) {
@@ -1724,12 +1737,16 @@
if (list.size() <= 0) {
stickies.remove(intent.getAction());
}
+ changedStickyBroadcasts.add(intent.getAction());
}
if (stickies.size() <= 0) {
mStickyBroadcasts.remove(userId);
}
}
}
+ for (int i = changedStickyBroadcasts.size() - 1; i >= 0; --i) {
+ BroadcastStickyCache.incrementVersionIfExists(changedStickyBroadcasts.get(i));
+ }
}
void finishReceiver(IBinder caller, int resultCode, String resultData,
@@ -1892,7 +1909,9 @@
private void sendPackageBroadcastLocked(int cmd, String[] packages, int userId) {
mService.mProcessList.sendPackageBroadcastLocked(cmd, packages, userId);
- }private List<ResolveInfo> collectReceiverComponents(
+ }
+
+ private List<ResolveInfo> collectReceiverComponents(
Intent intent, String resolvedType, int callingUid, int callingPid,
int[] users, int[] broadcastAllowList) {
// TODO: come back and remove this assumption to triage all broadcasts
@@ -2108,9 +2127,18 @@
}
void removeStickyBroadcasts(int userId) {
+ final ArrayList<String> changedStickyBroadcasts = new ArrayList<>();
synchronized (mStickyBroadcasts) {
+ final ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
+ mStickyBroadcasts.get(userId);
+ if (stickies != null) {
+ changedStickyBroadcasts.addAll(stickies.keySet());
+ }
mStickyBroadcasts.remove(userId);
}
+ for (int i = changedStickyBroadcasts.size() - 1; i >= 0; --i) {
+ BroadcastStickyCache.incrementVersionIfExists(changedStickyBroadcasts.get(i));
+ }
}
@NeverCompile
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 13e3348b..0fd22c5 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -339,8 +339,8 @@
Log.v(TAG, "setCommunicationDevice, device: " + device + ", uid: " + uid);
}
- synchronized (mDeviceStateLock) {
- if (device == null) {
+ if (device == null) {
+ synchronized (mDeviceStateLock) {
CommunicationRouteClient client = getCommunicationRouteClientForUid(uid);
if (client == null) {
return false;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 101596d..aae7b59 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -261,6 +261,7 @@
.setDisplayName(HdmiUtils.getDefaultDeviceName(source))
.setDeviceType(deviceTypes.get(0))
.setVendorId(Constants.VENDOR_ID_UNKNOWN)
+ .setPortId(mService.getHdmiCecNetwork().physicalAddressToPortId(physicalAddress))
.build();
mService.getHdmiCecNetwork().addCecDevice(newDevice);
}
@@ -1433,6 +1434,7 @@
protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
assertRunOnServiceThread();
mService.unregisterTvInputCallback(mTvInputCallback);
+ mTvInputs.clear();
// Remove any repeated working actions.
// HotplugDetectionAction will be reinstated during the wake up process.
// HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index 19c802b..9cfbfa64 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -16,6 +16,7 @@
package com.android.server.input.debug;
+import android.annotation.AnyThread;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.input.InputManager;
@@ -157,19 +158,28 @@
* @param touchpadHardwareState the hardware state of a touchpad
* @param deviceId the deviceId of the touchpad that is sending the hardware state
*/
+ @AnyThread
public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState,
int deviceId) {
- if (mTouchpadDebugView != null) {
- mTouchpadDebugView.updateHardwareState(touchpadHardwareState, deviceId);
- }
+ mHandler.post(() -> {
+ if (mTouchpadDebugView != null) {
+ mTouchpadDebugView.post(
+ () -> mTouchpadDebugView.updateHardwareState(touchpadHardwareState,
+ deviceId));
+ }
+ });
}
/**
* Notify the TouchpadDebugView of a new touchpad gesture.
*/
+ @AnyThread
public void updateTouchpadGestureInfo(int gestureType, int deviceId) {
- if (mTouchpadDebugView != null) {
- mTouchpadDebugView.updateGestureInfo(gestureType, deviceId);
- }
+ mHandler.post(() -> {
+ if (mTouchpadDebugView != null) {
+ mTouchpadDebugView.post(
+ () -> mTouchpadDebugView.updateGestureInfo(gestureType, deviceId));
+ }
+ });
}
}
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 60056eb..6f50295 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -912,12 +912,13 @@
* available ShareTarget definitions in this package.
*/
public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
- @NonNull final IntentFilter filter) {
- return getMatchingShareTargets(filter, null);
+ @NonNull final IntentFilter filter, final int callingUserId) {
+ return getMatchingShareTargets(filter, null, callingUserId);
}
List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
- @NonNull final IntentFilter filter, @Nullable final String pkgName) {
+ @NonNull final IntentFilter filter, @Nullable final String pkgName,
+ final int callingUserId) {
synchronized (mPackageItemLock) {
final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
for (int i = 0; i < mShareTargets.size(); i++) {
@@ -941,7 +942,7 @@
// included in the result
findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION,
- pkgName, 0, /*getPinnedByAnyLauncher=*/ false);
+ pkgName, callingUserId, /*getPinnedByAnyLauncher=*/ false);
final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
for (int i = 0; i < shortcuts.size(); i++) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index a3ff195..5518bfa 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -2591,7 +2591,8 @@
final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>();
final ShortcutUser user = getUserShortcutsLocked(userId);
user.forAllPackages(p -> shortcutInfoList.addAll(
- p.getMatchingShareTargets(filter, pkg)));
+ p.getMatchingShareTargets(filter, pkg,
+ mUserManagerInternal.getProfileParentId(userId))));
return new ParceledListSlice<>(shortcutInfoList);
}
}
@@ -2623,7 +2624,8 @@
final List<ShortcutManager.ShareShortcutInfo> matchedTargets =
getPackageShortcutsLocked(packageName, userId)
- .getMatchingShareTargets(filter);
+ .getMatchingShareTargets(filter,
+ mUserManagerInternal.getProfileParentId(callingUserId));
final int matchedSize = matchedTargets.size();
for (int i = 0; i < matchedSize; i++) {
if (matchedTargets.get(i).getShortcutInfo().getId().equals(shortcutId)) {
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
index 153bb91..1ba2487 100644
--- a/services/core/java/com/android/server/utils/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -130,22 +130,35 @@
}
/**
- * Return true if freezing is enabled. This has no effect if the service is not enabled.
+ * Return true if freezing is feature-enabled. Freezing must still be enabled on a
+ * per-service basis.
*/
- private static boolean anrTimerFreezerEnabled() {
+ private static boolean freezerFeatureEnabled() {
return Flags.anrTimerFreezer();
}
/**
+ * Return true if tracing is feature-enabled. This has no effect unless tracing is configured.
+ * Note that this does not represent any per-process overrides via an Injector.
+ */
+ public static boolean traceFeatureEnabled() {
+ return anrTimerServiceEnabled() && Flags.anrTimerTrace();
+ }
+
+ /**
* This class allows test code to provide instance-specific overrides.
*/
static class Injector {
- boolean anrTimerServiceEnabled() {
+ boolean serviceEnabled() {
return AnrTimer.anrTimerServiceEnabled();
}
- boolean anrTimerFreezerEnabled() {
- return AnrTimer.anrTimerFreezerEnabled();
+ boolean freezerEnabled() {
+ return AnrTimer.freezerFeatureEnabled();
+ }
+
+ boolean traceEnabled() {
+ return AnrTimer.traceFeatureEnabled();
}
}
@@ -349,7 +362,7 @@
mWhat = what;
mLabel = label;
mArgs = args;
- boolean enabled = args.mInjector.anrTimerServiceEnabled() && nativeTimersSupported();
+ boolean enabled = args.mInjector.serviceEnabled() && nativeTimersSupported();
mFeature = createFeatureSwitch(enabled);
}
@@ -448,7 +461,7 @@
/**
* The FeatureDisabled class bypasses almost all AnrTimer logic. It is used when the AnrTimer
- * service is disabled via Flags.anrTimerServiceEnabled.
+ * service is disabled via Flags.anrTimerService().
*/
private class FeatureDisabled extends FeatureSwitch {
/** Start a timer by sending a message to the client's handler. */
@@ -515,7 +528,7 @@
/**
* The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service
- * is enabled via Flags.anrTimerServiceEnabled.
+ * is enabled via Flags.anrTimerService().
*/
private class FeatureEnabled extends FeatureSwitch {
@@ -533,7 +546,7 @@
FeatureEnabled() {
mNative = nativeAnrTimerCreate(mLabel,
mArgs.mExtend,
- mArgs.mFreeze && mArgs.mInjector.anrTimerFreezerEnabled());
+ mArgs.mFreeze && mArgs.mInjector.freezerEnabled());
if (mNative == 0) throw new IllegalArgumentException("unable to create native timer");
synchronized (sAnrTimerList) {
sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this));
@@ -550,7 +563,7 @@
// exist.
if (cancel(arg)) mTotalRestarted++;
- int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs);
+ final int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs);
if (timerId > 0) {
mTimerIdMap.put(arg, timerId);
mTimerArgMap.put(timerId, arg);
@@ -895,7 +908,7 @@
/** Dumpsys output, allowing for overrides. */
@VisibleForTesting
static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) {
- if (!injector.anrTimerServiceEnabled()) return;
+ if (!injector.serviceEnabled()) return;
final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.println("AnrTimer statistics");
@@ -926,6 +939,18 @@
}
/**
+ * Set a trace specification. The input is a set of strings. On success, the function pushes
+ * the trace specification to all timers, and then returns a response message. On failure,
+ * the function throws IllegalArgumentException and tracing is disabled.
+ *
+ * An empty specification has no effect other than returning the current trace specification.
+ */
+ @Nullable
+ public static String traceTimers(@Nullable String[] spec) {
+ return nativeAnrTimerTrace(spec);
+ }
+
+ /**
* Return true if the native timers are supported. Native timers are supported if the method
* nativeAnrTimerSupported() can be executed and it returns true.
*/
@@ -981,6 +1006,15 @@
*/
private static native boolean nativeAnrTimerRelease(long service, int timerId);
+ /**
+ * Configure tracing. The input array is a set of words pulled from the command line. All
+ * parsing happens inside the native layer. The function returns a string which is either an
+ * error message (so nothing happened) or the current configuration after applying the config.
+ * Passing an null array or an empty array simply returns the current configuration.
+ * The function returns null if the native layer is not implemented.
+ */
+ private static native @Nullable String nativeAnrTimerTrace(@Nullable String[] config);
+
/** Retrieve runtime dump information from the native layer. */
private static native String[] nativeAnrTimerDump(long service);
}
diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig
index 00ebb66..333287f 100644
--- a/services/core/java/com/android/server/utils/flags.aconfig
+++ b/services/core/java/com/android/server/utils/flags.aconfig
@@ -17,3 +17,10 @@
bug: "325594551"
}
+flag {
+ name: "anr_timer_trace"
+ namespace: "system_performance"
+ is_fixed_read_only: true
+ description: "When true, start a trace if an ANR timer reaches 50%"
+ bug: "352085328"
+}
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index cf96114..2836d46 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -19,6 +19,8 @@
#include <sys/timerfd.h>
#include <inttypes.h>
#include <sys/stat.h>
+#include <unistd.h>
+#include <regex.h>
#include <algorithm>
#include <list>
@@ -26,6 +28,7 @@
#include <set>
#include <string>
#include <vector>
+#include <map>
#define LOG_TAG "AnrTimerService"
#define ATRACE_TAG ATRACE_TAG_ACTIVITY_MANAGER
@@ -33,8 +36,8 @@
#include <jni.h>
#include <nativehelper/JNIHelp.h>
-#include "android_runtime/AndroidRuntime.h"
-#include "core_jni_helpers.h"
+#include <android_runtime/AndroidRuntime.h>
+#include <core_jni_helpers.h>
#include <processgroup/processgroup.h>
#include <utils/Log.h>
@@ -109,32 +112,336 @@
// Return the name of the process whose pid is the input. If the process does not exist, the
// name will "notfound".
std::string getProcessName(pid_t pid) {
- char buffer[PATH_MAX];
- snprintf(buffer, sizeof(buffer), "/proc/%d/cmdline", pid);
- int fd = ::open(buffer, O_RDONLY);
- if (fd >= 0) {
- size_t pos = 0;
- ssize_t result;
- while (pos < sizeof(buffer)-1) {
- result = ::read(fd, buffer + pos, (sizeof(buffer) - pos) - 1);
- if (result <= 0) {
- break;
- }
- }
- ::close(fd);
-
- if (result >= 0) {
- buffer[pos] = 0;
+ char path[PATH_MAX];
+ snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
+ FILE* cmdline = fopen(path, "r");
+ if (cmdline != nullptr) {
+ char name[PATH_MAX];
+ char const *retval = fgets(name, sizeof(name), cmdline);
+ fclose(cmdline);
+ if (retval == nullptr) {
+ return std::string("unknown");
} else {
- snprintf(buffer, sizeof(buffer), "err: %s", strerror(errno));
+ return std::string(name);
}
} else {
- snprintf(buffer, sizeof(buffer), "notfound");
+ return std::string("notfound");
}
- return std::string(buffer);
}
/**
+ * Three wrappers of the trace utilities, which hard-code the timer track.
+ */
+void traceBegin(const char* msg, int cookie) {
+ ATRACE_ASYNC_FOR_TRACK_BEGIN(ANR_TIMER_TRACK, msg, cookie);
+}
+
+void traceEnd(int cookie) {
+ ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie);
+}
+
+void traceEvent(const char* msg) {
+ ATRACE_INSTANT_FOR_TRACK(ANR_TIMER_TRACK, msg);
+}
+
+/**
+ * This class captures tracing information for processes tracked by an AnrTimer. A user can
+ * configure tracing to have the AnrTimerService emit extra information for watched processes.
+ * singleton.
+ *
+ * The tracing configuration has two components: process selection and an optional early action.
+ *
+ * Processes are selected in one of three ways:
+ * 1. A list of numeric linux process IDs.
+ * 2. A regular expression, matched against process names.
+ * 3. The keyword "all", to trace every process that uses an AnrTimer.
+ * Perfetto trace events are always emitted for every operation on a traced process.
+ *
+ * An early action occurs before the scheduled timeout. The early timeout is specified as a
+ * percentage (integer value in the range 0:100) of the programmed timeout. The AnrTimer will
+ * execute the early action at the early timeout. The early action may terminate the timer.
+ *
+ * There is one early action:
+ * 1. Expire - consider the AnrTimer expired and report it to the upper layers.
+ */
+class AnrTimerTracer {
+ public:
+ // Actions that can be taken when an early timer expires.
+ enum EarlyAction {
+ // Take no action. This is the value used when tracing is disabled.
+ None,
+ // Trace the timer but take no other action.
+ Trace,
+ // Report timer expiration to the upper layers. This is terminal, in that
+ Expire,
+ };
+
+ // The trace information for a single timer.
+ struct TraceConfig {
+ bool enabled = false;
+ EarlyAction action = None;
+ int earlyTimeout = 0;
+ };
+
+ AnrTimerTracer() {
+ AutoMutex _l(lock_);
+ resetLocked();
+ }
+
+ // Return the TraceConfig for a process.
+ TraceConfig getConfig(int pid) {
+ AutoMutex _l(lock_);
+ // The most likely situation: no tracing is configured.
+ if (!config_.enabled) return {};
+ if (matchAllPids_) return config_;
+ if (watched_.contains(pid)) return config_;
+ if (!matchNames_) return {};
+ if (matchedPids_.contains(pid)) return config_;
+ if (unmatchedPids_.contains(pid)) return {};
+ std::string proc_name = getProcessName(pid);
+ bool matched = regexec(®ex_, proc_name.c_str(), 0, 0, 0) == 0;
+ if (matched) {
+ matchedPids_.insert(pid);
+ return config_;
+ } else {
+ unmatchedPids_.insert(pid);
+ return {};
+ }
+ }
+
+ // Set the trace configuration. The input is a string that contains key/value pairs of the
+ // form "key=value". Pairs are separated by spaces. The function returns a string status.
+ // On success, the normalized config is returned. On failure, the configuration reset the
+ // result contains an error message. As a special case, an empty set of configs, or a
+ // config that contains only the keyword "show", will do nothing except return the current
+ // configuration. On any error, all tracing is disabled.
+ std::pair<bool, std::string> setConfig(const std::vector<std::string>& config) {
+ AutoMutex _l(lock_);
+ if (config.size() == 0) {
+ // Implicit "show"
+ return { true, currentConfigLocked() };
+ } else if (config.size() == 1) {
+ // Process the one-word commands
+ const char* s = config[0].c_str();
+ if (strcmp(s, "show") == 0) {
+ return { true, currentConfigLocked() };
+ } else if (strcmp(s, "off") == 0) {
+ resetLocked();
+ return { true, currentConfigLocked() };
+ } else if (strcmp(s, "help") == 0) {
+ return { true, help() };
+ }
+ } else if (config.size() > 2) {
+ return { false, "unexpected values in config" };
+ }
+
+ // Barring an error in the remaining specification list, tracing will be enabled.
+ resetLocked();
+ // Fetch the process specification. This must be the first configuration entry.
+ {
+ auto result = setTracedProcess(config[0]);
+ if (!result.first) return result;
+ }
+
+ // Process optional actions.
+ if (config.size() > 1) {
+ auto result = setTracedAction(config[1]);
+ if (!result.first) return result;
+ }
+
+ // Accept the result.
+ config_.enabled = true;
+ return { true, currentConfigLocked() };
+ }
+
+ private:
+ // Identify the processes to be traced.
+ std::pair<bool, std::string> setTracedProcess(std::string config) {
+ const char* s = config.c_str();
+ const char* word = nullptr;
+
+ if (strcmp(s, "pid=all") == 0) {
+ matchAllPids_ = true;
+ } else if ((word = startsWith(s, "pid=")) != nullptr) {
+ int p;
+ int n;
+ while (sscanf(word, "%d%n", &p, &n) == 1) {
+ watched_.insert(p);
+ word += n;
+ if (*word == ',') word++;
+ }
+ if (*word != 0) {
+ return { false, "invalid pid list" };
+ }
+ config_.action = Trace;
+ } else if ((word = startsWith(s, "name=")) != nullptr) {
+ if (matchNames_) {
+ regfree(®ex_);
+ matchNames_ = false;
+ }
+ if (regcomp(®ex_, word, REG_EXTENDED) != 0) {
+ return { false, "invalid regex" };
+ }
+ matchNames_ = true;
+ namePattern_ = word;
+ config_.action = Trace;
+ } else {
+ return { false, "no process specified" };
+ }
+ return { true, "" };
+ }
+
+ // Set the action to be taken on a traced process. The incoming default action is Trace;
+ // this method may overwrite that action.
+ std::pair<bool, std::string> setTracedAction(std::string config) {
+ const char* s = config.c_str();
+ const char* word = nullptr;
+ if (sscanf(s, "expire=%d", &config_.earlyTimeout) == 1) {
+ if (config_.earlyTimeout < 0) {
+ return { false, "invalid expire timeout" };
+ }
+ config_.action = Expire;
+ } else {
+ return { false, std::string("cannot parse action ") + s };
+ }
+ return { true, "" };
+ }
+
+ // Return the string value of an action.
+ static const char* toString(EarlyAction action) {
+ switch (action) {
+ case None: return "none";
+ case Trace: return "trace";
+ case Expire: return "expire";
+ }
+ return "unknown";
+ }
+
+ // Return the action represented by the string.
+ static EarlyAction fromString(const char* action) {
+ if (strcmp(action, "expire") == 0) return Expire;
+ return None;
+ }
+
+ // Return the help message. This has everything except the invocation command.
+ static std::string help() {
+ static const char* msg =
+ "help show this message\n"
+ "show report the current configuration\n"
+ "off clear the current configuration, turning off all tracing\n"
+ "spec... configure tracing according to the specification list\n"
+ " action=<action> what to do when a split timer expires\n"
+ " expire expire the timer to the upper levels\n"
+ " event generate extra trace events\n"
+ " pid=<pid>[,<pid>] watch the processes in the pid list\n"
+ " pid=all watch every process in the system\n"
+ " name=<regex> watch the processes whose name matches the regex\n";
+ return msg;
+ }
+
+ // A small convenience function for parsing. If the haystack starts with the needle and the
+ // haystack has at least one more character following, return a pointer to the following
+ // character. Otherwise return null.
+ static const char* startsWith(const char* haystack, const char* needle) {
+ if (strncmp(haystack, needle, strlen(needle)) == 0 && strlen(haystack) + strlen(needle)) {
+ return haystack + strlen(needle);
+ }
+ return nullptr;
+ }
+
+ // Return the currently watched pids. The lock must be held.
+ std::string watchedPidsLocked() const {
+ if (watched_.size() == 0) return "none";
+ bool first = true;
+ std::string result = "";
+ for (auto i = watched_.cbegin(); i != watched_.cend(); i++) {
+ if (first) {
+ result += StringPrintf("%d", *i);
+ } else {
+ result += StringPrintf(",%d", *i);
+ }
+ }
+ return result;
+ }
+
+ // Return the current configuration, in a form that can be consumed by setConfig().
+ std::string currentConfigLocked() const {
+ if (!config_.enabled) return "off";
+ std::string result;
+ if (matchAllPids_) {
+ result = "pid=all";
+ } else if (matchNames_) {
+ result = StringPrintf("name=\"%s\"", namePattern_.c_str());
+ } else {
+ result = std::string("pid=") + watchedPidsLocked();
+ }
+ switch (config_.action) {
+ case None:
+ break;
+ case Trace:
+ // The default action is Trace
+ break;
+ case Expire:
+ result += StringPrintf(" %s=%d", toString(config_.action), config_.earlyTimeout);
+ break;
+ }
+ return result;
+ }
+
+ // Reset the current configuration.
+ void resetLocked() {
+ if (!config_.enabled) return;
+
+ config_.enabled = false;
+ config_.earlyTimeout = 0;
+ config_.action = {};
+ matchAllPids_ = false;
+ watched_.clear();
+ if (matchNames_) regfree(®ex_);
+ matchNames_ = false;
+ namePattern_ = "";
+ matchedPids_.clear();
+ unmatchedPids_.clear();
+ }
+
+ // The lock for all operations
+ mutable Mutex lock_;
+
+ // The current tracing information, when a process matches.
+ TraceConfig config_;
+
+ // A short-hand flag that causes all processes to be tracing without the overhead of
+ // searching any of the maps.
+ bool matchAllPids_;
+
+ // A set of process IDs that should be traced. This is updated directly in setConfig()
+ // and only includes pids that were explicitly called out in the configuration.
+ std::set<pid_t> watched_;
+
+ // Name mapping is a relatively expensive operation, since the process name must be fetched
+ // from the /proc file system and then a regex must be evaluated. However, name mapping is
+ // useful to ensure processes are traced at the moment they start. To make this faster, a
+ // process's name is matched only once, and the result is stored in the matchedPids_ or
+ // unmatchedPids_ set, as appropriate. This can lead to confusion if a process changes its
+ // name after it starts.
+
+ // The global flag that enables name matching. If this is disabled then all name matching
+ // is disabled.
+ bool matchNames_;
+
+ // The regular expression that matches processes to be traced. This is saved for logging.
+ std::string namePattern_;
+
+ // The compiled regular expression.
+ regex_t regex_;
+
+ // The set of all pids that whose process names match (or do not match) the name regex.
+ // There is one set for pids that match and one set for pids that do not match.
+ std::set<pid_t> matchedPids_;
+ std::set<pid_t> unmatchedPids_;
+};
+
+/**
* This class encapsulates the anr timer service. The service manages a list of individual
* timers. A timer is either Running or Expired. Once started, a timer may be canceled or
* accepted. Both actions collect statistics about the timer and then delete it. An expired
@@ -177,7 +484,7 @@
* traditional void* and Java object pointer. The remaining parameters are
* configuration options.
*/
- AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*,
+ AnrTimerService(const char* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*,
bool extend, bool freeze);
// Delete the service and clean up memory.
@@ -211,6 +518,11 @@
// Release a timer. The timer must be in the expired list.
bool release(timer_id_t);
+ // Configure a trace specification to trace selected timers. See AnrTimerTracer for details.
+ static std::pair<bool, std::string> trace(const std::vector<std::string>& spec) {
+ return tracer_.setConfig(spec);
+ }
+
// Return the Java object associated with this instance.
jweak jtimer() const {
return notifierObject_;
@@ -221,7 +533,7 @@
private:
// The service cannot be copied.
- AnrTimerService(AnrTimerService const&) = delete;
+ AnrTimerService(const AnrTimerService&) = delete;
// Insert a timer into the running list. The lock must be held by the caller.
void insertLocked(const Timer&);
@@ -230,7 +542,7 @@
Timer removeLocked(timer_id_t timerId);
// Add a timer to the expired list.
- void addExpiredLocked(Timer const&);
+ void addExpiredLocked(const Timer&);
// Scrub the expired list by removing all entries for non-existent processes. The expired
// lock must be held by the caller.
@@ -240,10 +552,10 @@
static const char* statusString(Status);
// The name of this service, for logging.
- std::string const label_;
+ const std::string label_;
// The callback that is invoked when a timer expires.
- notifier_t const notifier_;
+ const notifier_t notifier_;
// The two cookies passed to the notifier.
void* notifierCookie_;
@@ -289,8 +601,13 @@
// The clock used by this AnrTimerService.
Ticker *ticker_;
+
+ // The global tracing specification.
+ static AnrTimerTracer tracer_;
};
+AnrTimerTracer AnrTimerService::tracer_;
+
class AnrTimerService::ProcessStats {
public:
nsecs_t cpu_time;
@@ -337,14 +654,23 @@
class AnrTimerService::Timer {
public:
// A unique ID assigned when the Timer is created.
- timer_id_t const id;
+ const timer_id_t id;
// The creation parameters. The timeout is the original, relative timeout.
- int const pid;
- int const uid;
- nsecs_t const timeout;
- bool const extend;
- bool const freeze;
+ const int pid;
+ const int uid;
+ const nsecs_t timeout;
+ // True if the timer may be extended.
+ const bool extend;
+ // True if process should be frozen when its timer expires.
+ const bool freeze;
+ // This is a percentage between 0 and 100. If it is non-zero then timer will fire at
+ // timeout*split/100, and the EarlyAction will be invoked. The timer may continue running
+ // or may expire, depending on the action. Thus, this value "splits" the timeout into two
+ // pieces.
+ const int split;
+ // The action to take if split (above) is non-zero, when the timer reaches the split point.
+ const AnrTimerTracer::EarlyAction action;
// The state of this timer.
Status status;
@@ -355,6 +681,9 @@
// The scheduled timeout. This is an absolute time. It may be extended.
nsecs_t scheduled;
+ // True if this timer is split and in its second half
+ bool splitting;
+
// True if this timer has been extended.
bool extended;
@@ -367,22 +696,10 @@
// The default constructor is used to create timers that are Invalid, representing the "not
// found" condition when a collection is searched.
- Timer() :
- id(NOTIMER),
- pid(0),
- uid(0),
- timeout(0),
- extend(false),
- freeze(false),
- status(Invalid),
- started(0),
- scheduled(0),
- extended(false),
- frozen(false) {
- }
+ Timer() : Timer(NOTIMER) { }
- // This constructor creates a timer with the specified id. This can be used as the argument
- // to find().
+ // This constructor creates a timer with the specified id and everything else set to
+ // "empty". This can be used as the argument to find().
Timer(timer_id_t id) :
id(id),
pid(0),
@@ -390,29 +707,37 @@
timeout(0),
extend(false),
freeze(false),
+ split(0),
+ action(AnrTimerTracer::None),
status(Invalid),
started(0),
scheduled(0),
+ splitting(false),
extended(false),
frozen(false) {
}
// Create a new timer. This starts the timer.
- Timer(int pid, int uid, nsecs_t timeout, bool extend, bool freeze) :
+ Timer(int pid, int uid, nsecs_t timeout, bool extend, bool freeze,
+ AnrTimerTracer::TraceConfig trace) :
id(nextId()),
pid(pid),
uid(uid),
timeout(timeout),
extend(extend),
freeze(pid != 0 && freeze),
+ split(trace.earlyTimeout),
+ action(trace.action),
status(Running),
started(now()),
- scheduled(started + timeout),
+ scheduled(started + (split > 0 ? (timeout*split)/100 : timeout)),
+ splitting(false),
extended(false),
frozen(false) {
if (extend && pid != 0) {
initial.fill(pid);
}
+
// A zero-pid is odd but it means the upper layers will never ANR the process. Freezing
// is always disabled. (It won't work anyway, but disabling it avoids error messages.)
ALOGI_IF(DEBUG_ERROR && pid == 0, "error: zero-pid %s", toString().c_str());
@@ -434,6 +759,23 @@
// returns false if the timer is eligible for extension. If the function returns false, the
// scheduled time is updated.
bool expire() {
+ if (split > 0 && !splitting) {
+ scheduled = started + timeout;
+ splitting = true;
+ event("split");
+ switch (action) {
+ case AnrTimerTracer::None:
+ case AnrTimerTracer::Trace:
+ break;
+ case AnrTimerTracer::Expire:
+ status = Expired;
+ maybeFreezeProcess();
+ event("expire");
+ break;
+ }
+ return status == Expired;
+ }
+
nsecs_t extension = 0;
if (extend && !extended) {
// Only one extension is permitted.
@@ -525,15 +867,15 @@
char tag[PATH_MAX];
snprintf(tag, sizeof(tag), "freeze(pid=%d,uid=%d)", pid, uid);
- ATRACE_ASYNC_FOR_TRACK_BEGIN(ANR_TIMER_TRACK, tag, cookie);
+ traceBegin(tag, cookie);
if (SetProcessProfiles(uid, pid, {"Frozen"})) {
ALOGI("freeze %s name=%s", toString().c_str(), getName().c_str());
frozen = true;
- ATRACE_ASYNC_FOR_TRACK_BEGIN(ANR_TIMER_TRACK, "frozen", cookie+1);
+ traceBegin("frozen", cookie+1);
} else {
ALOGE("error: freezing %s name=%s error=%s",
toString().c_str(), getName().c_str(), strerror(errno));
- ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie);
+ traceEnd(cookie);
}
}
@@ -543,7 +885,7 @@
// See maybeFreezeProcess for an explanation of the cookie.
const uint32_t cookie = id << 1;
- ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie+1);
+ traceEnd(cookie+1);
if (SetProcessProfiles(uid, pid, {"Unfrozen"})) {
ALOGI("unfreeze %s name=%s", toString().c_str(), getName().c_str());
frozen = false;
@@ -551,7 +893,7 @@
ALOGE("error: unfreezing %s name=%s error=%s",
toString().c_str(), getName().c_str(), strerror(errno));
}
- ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie);
+ traceEnd(cookie);
}
// Get the next free ID. NOTIMER is never returned.
@@ -564,12 +906,17 @@
}
// Log an event, non-verbose.
- void event(char const* tag) {
+ void event(const char* tag) {
event(tag, false);
}
// Log an event, guarded by the debug flag.
- void event(char const* tag, bool verbose) {
+ void event(const char* tag, bool verbose) {
+ if (action != AnrTimerTracer::None) {
+ char msg[PATH_MAX];
+ snprintf(msg, sizeof(msg), "%s(pid=%d)", tag, pid);
+ traceEvent(msg);
+ }
if (verbose) {
char name[PATH_MAX];
ALOGI_IF(DEBUG_TIMER, "event %s %s name=%s",
@@ -594,12 +941,12 @@
struct Entry {
const nsecs_t scheduled;
const timer_id_t id;
- AnrTimerService* const service;
+ AnrTimerService* service;
Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) :
scheduled(scheduled), id(id), service(service) {};
- bool operator<(const Entry &r) const {
+ bool operator<(const Entry& r) const {
return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled;
}
};
@@ -664,7 +1011,7 @@
}
// Remove every timer associated with the service.
- void remove(AnrTimerService const* service) {
+ void remove(const AnrTimerService* service) {
AutoMutex _l(lock_);
timer_id_t front = headTimerId();
for (auto i = running_.begin(); i != running_.end(); ) {
@@ -746,7 +1093,7 @@
// scheduled expiration time of the first entry.
void restartLocked() {
if (!running_.empty()) {
- Entry const x = *(running_.cbegin());
+ const Entry x = *(running_.cbegin());
nsecs_t delay = x.scheduled - now();
// Force a minimum timeout of 10ns.
if (delay < 10) delay = 10;
@@ -807,7 +1154,7 @@
std::atomic<size_t> AnrTimerService::Ticker::idGen_;
-AnrTimerService::AnrTimerService(char const* label, notifier_t notifier, void* cookie,
+AnrTimerService::AnrTimerService(const char* label, notifier_t notifier, void* cookie,
jweak jtimer, Ticker* ticker, bool extend, bool freeze) :
label_(label),
notifier_(notifier),
@@ -841,7 +1188,7 @@
AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid, nsecs_t timeout) {
AutoMutex _l(lock_);
- Timer t(pid, uid, timeout, extend_, freeze_);
+ Timer t(pid, uid, timeout, extend_, freeze_, tracer_.getConfig(pid));
insertLocked(t);
t.start();
counters_.started++;
@@ -918,7 +1265,7 @@
return okay;
}
-void AnrTimerService::addExpiredLocked(Timer const& timer) {
+void AnrTimerService::addExpiredLocked(const Timer& timer) {
scrubExpiredLocked();
expired_.insert(timer);
}
@@ -1077,7 +1424,7 @@
ScopedUtfChars name(env, jname);
jobject timer = env->NewWeakGlobalRef(jtimer);
AnrTimerService* service = new AnrTimerService(name.c_str(),
- anrNotify, &gAnrArgs, timer, gAnrArgs.ticker, extend, freeze);
+ anrNotify, &gAnrArgs, timer, gAnrArgs.ticker, extend, freeze);
return reinterpret_cast<jlong>(service);
}
@@ -1122,6 +1469,19 @@
return toService(ptr)->release(timerId);
}
+jstring anrTimerTrace(JNIEnv* env, jclass, jobjectArray jconfig) {
+ if (!nativeSupportEnabled) return nullptr;
+ std::vector<std::string> config;
+ const jsize jlen = jconfig == nullptr ? 0 : env->GetArrayLength(jconfig);
+ for (size_t i = 0; i < jlen; i++) {
+ jstring je = static_cast<jstring>(env->GetObjectArrayElement(jconfig, i));
+ ScopedUtfChars e(env, je);
+ config.push_back(e.c_str());
+ }
+ auto r = AnrTimerService::trace(config);
+ return env->NewStringUTF(r.second.c_str());
+}
+
jobjectArray anrTimerDump(JNIEnv *env, jclass, jlong ptr) {
if (!nativeSupportEnabled) return nullptr;
std::vector<std::string> stats = toService(ptr)->getDump();
@@ -1134,22 +1494,23 @@
}
static const JNINativeMethod methods[] = {
- {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported},
- {"nativeAnrTimerCreate", "(Ljava/lang/String;ZZ)J", (void*) anrTimerCreate},
- {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose},
- {"nativeAnrTimerStart", "(JIIJ)I", (void*) anrTimerStart},
- {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel},
- {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept},
- {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard},
- {"nativeAnrTimerRelease", "(JI)Z", (void*) anrTimerRelease},
- {"nativeAnrTimerDump", "(J)[Ljava/lang/String;", (void*) anrTimerDump},
+ {"nativeAnrTimerSupported", "()Z", (void*) anrTimerSupported},
+ {"nativeAnrTimerCreate", "(Ljava/lang/String;ZZ)J", (void*) anrTimerCreate},
+ {"nativeAnrTimerClose", "(J)I", (void*) anrTimerClose},
+ {"nativeAnrTimerStart", "(JIIJ)I", (void*) anrTimerStart},
+ {"nativeAnrTimerCancel", "(JI)Z", (void*) anrTimerCancel},
+ {"nativeAnrTimerAccept", "(JI)Z", (void*) anrTimerAccept},
+ {"nativeAnrTimerDiscard", "(JI)Z", (void*) anrTimerDiscard},
+ {"nativeAnrTimerRelease", "(JI)Z", (void*) anrTimerRelease},
+ {"nativeAnrTimerTrace", "([Ljava/lang/String;)Ljava/lang/String;", (void*) anrTimerTrace},
+ {"nativeAnrTimerDump", "(J)[Ljava/lang/String;", (void*) anrTimerDump},
};
} // anonymous namespace
int register_android_server_utils_AnrTimer(JNIEnv* env)
{
- static const char *className = "com/android/server/utils/AnrTimer";
+ static const char* className = "com/android/server/utils/AnrTimer";
jniRegisterNativeMethods(env, className, methods, NELEM(methods));
nativeSupportEnabled = NATIVE_SUPPORT;
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
index b09e9b1..54282ff 100644
--- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -119,7 +119,7 @@
*/
private class TestInjector extends AnrTimer.Injector {
@Override
- boolean anrTimerServiceEnabled() {
+ boolean serviceEnabled() {
return mEnabled;
}
}
diff --git a/tests/broadcasts/OWNERS b/tests/broadcasts/OWNERS
new file mode 100644
index 0000000..d2e1f81
--- /dev/null
+++ b/tests/broadcasts/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316181
+include platform/frameworks/base:/BROADCASTS_OWNERS
diff --git a/tests/broadcasts/unit/Android.bp b/tests/broadcasts/unit/Android.bp
new file mode 100644
index 0000000..0692477
--- /dev/null
+++ b/tests/broadcasts/unit/Android.bp
@@ -0,0 +1,44 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "BroadcastUnitTests",
+ srcs: ["src/**/*.java"],
+ defaults: [
+ "modules-utils-extended-mockito-rule-defaults",
+ ],
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "mockito-target-extended-minus-junit4",
+ "truth",
+ "flag-junit",
+ "android.app.flags-aconfig-java",
+ ],
+ certificate: "platform",
+ platform_apis: true,
+ test_suites: ["device-tests"],
+}
diff --git a/tests/broadcasts/unit/AndroidManifest.xml b/tests/broadcasts/unit/AndroidManifest.xml
new file mode 100644
index 0000000..e9c5248
--- /dev/null
+++ b/tests/broadcasts/unit/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.broadcasts.unit" >
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.broadcasts.unit"
+ android:label="Broadcasts Unit Tests"/>
+</manifest>
\ No newline at end of file
diff --git a/tests/broadcasts/unit/AndroidTest.xml b/tests/broadcasts/unit/AndroidTest.xml
new file mode 100644
index 0000000..b91e4783
--- /dev/null
+++ b/tests/broadcasts/unit/AndroidTest.xml
@@ -0,0 +1,29 @@
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs Broadcasts tests">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="BroadcastUnitTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="BroadcastUnitTests.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.broadcasts.unit" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/broadcasts/unit/TEST_MAPPING b/tests/broadcasts/unit/TEST_MAPPING
new file mode 100644
index 0000000..0e824c5
--- /dev/null
+++ b/tests/broadcasts/unit/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "postsubmit": [
+ {
+ "name": "BroadcastUnitTests",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
diff --git a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
new file mode 100644
index 0000000..b7c412d
--- /dev/null
+++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.app;
+
+import static android.content.Intent.ACTION_BATTERY_CHANGED;
+import static android.content.Intent.ACTION_DEVICE_STORAGE_LOW;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.ArrayMap;
+
+import androidx.annotation.GuardedBy;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BroadcastStickyCacheTest {
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+ @Rule
+ public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+ .mockStatic(SystemProperties.class)
+ .build();
+
+ private static final String PROP_KEY_BATTERY_CHANGED = BroadcastStickyCache.getKey(
+ ACTION_BATTERY_CHANGED);
+
+ private final TestSystemProps mTestSystemProps = new TestSystemProps();
+
+ @Before
+ public void setUp() {
+ doAnswer(invocation -> {
+ final String name = invocation.getArgument(0);
+ final long value = Long.parseLong(invocation.getArgument(1));
+ mTestSystemProps.add(name, value);
+ return null;
+ }).when(() -> SystemProperties.set(anyString(), anyString()));
+ doAnswer(invocation -> {
+ final String name = invocation.getArgument(0);
+ final TestSystemProps.Handle testHandle = mTestSystemProps.query(name);
+ if (testHandle == null) {
+ return null;
+ }
+ final SystemProperties.Handle handle = Mockito.mock(SystemProperties.Handle.class);
+ doAnswer(handleInvocation -> testHandle.getLong(-1)).when(handle).getLong(anyLong());
+ return handle;
+ }).when(() -> SystemProperties.find(anyString()));
+ }
+
+ @After
+ public void tearDown() {
+ mTestSystemProps.clear();
+ BroadcastStickyCache.clearForTest();
+ }
+
+ @Test
+ public void testUseCache_nullFilter() {
+ assertThat(BroadcastStickyCache.useCache(null)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache_noActions() {
+ final IntentFilter filter = new IntentFilter();
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache_multipleActions() {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_DEVICE_STORAGE_LOW);
+ filter.addAction(ACTION_BATTERY_CHANGED);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache_valueNotSet() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ final Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ BroadcastStickyCache.add(filter, intent);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true);
+ }
+
+ @Test
+ public void testUseCache_versionMismatch() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ final Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ BroadcastStickyCache.add(filter, intent);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testAdd() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ BroadcastStickyCache.add(filter, intent);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true);
+ Intent actualIntent = BroadcastStickyCache.getIntentUnchecked(filter);
+ assertThat(actualIntent).isNotNull();
+ assertEquals(actualIntent, intent);
+
+ intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 99);
+ BroadcastStickyCache.add(filter, intent);
+ actualIntent = BroadcastStickyCache.getIntentUnchecked(filter);
+ assertThat(actualIntent).isNotNull();
+ assertEquals(actualIntent, intent);
+ }
+
+ @Test
+ public void testIncrementVersion_propExists() {
+ SystemProperties.set(PROP_KEY_BATTERY_CHANGED, String.valueOf(100));
+
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(101);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(102);
+ }
+
+ @Test
+ public void testIncrementVersion_propNotExists() {
+ // Verify that the property doesn't exist
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(1);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2);
+ }
+
+ @Test
+ public void testIncrementVersionIfExists_propExists() {
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2);
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(3);
+ }
+
+ @Test
+ public void testIncrementVersionIfExists_propNotExists() {
+ // Verify that the property doesn't exist
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+ // Verify that property is not added as part of the querying.
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+ }
+
+ private void assertEquals(Intent actualIntent, Intent expectedIntent) {
+ assertThat(actualIntent.getAction()).isEqualTo(expectedIntent.getAction());
+ assertEquals(actualIntent.getExtras(), expectedIntent.getExtras());
+ }
+
+ private void assertEquals(Bundle actualExtras, Bundle expectedExtras) {
+ assertWithMessage("Extras expected=%s, actual=%s", expectedExtras, actualExtras)
+ .that(actualExtras.kindofEquals(expectedExtras)).isTrue();
+ }
+
+ private static final class TestSystemProps {
+ @GuardedBy("mSysProps")
+ private final ArrayMap<String, Long> mSysProps = new ArrayMap<>();
+
+ public void add(String name, long value) {
+ synchronized (mSysProps) {
+ mSysProps.put(name, value);
+ }
+ }
+
+ public long get(String name, long defaultValue) {
+ synchronized (mSysProps) {
+ final int idx = mSysProps.indexOfKey(name);
+ return idx >= 0 ? mSysProps.valueAt(idx) : defaultValue;
+ }
+ }
+
+ public Handle query(String name) {
+ synchronized (mSysProps) {
+ return mSysProps.containsKey(name) ? new Handle(name) : null;
+ }
+ }
+
+ public void clear() {
+ synchronized (mSysProps) {
+ mSysProps.clear();
+ }
+ }
+
+ public class Handle {
+ private final String mName;
+
+ Handle(String name) {
+ mName = name;
+ }
+
+ public long getLong(long defaultValue) {
+ return get(mName, defaultValue);
+ }
+ }
+ }
+}
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
index dc0d8a3..cba521e 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -20,7 +20,10 @@
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeSpec
+import java.util.HashMap
+import java.util.Map
import javax.lang.model.element.Modifier
/*
@@ -49,6 +52,7 @@
* public static boolean hasFeatureAutomotive(Context context);
* public static boolean hasFeatureLeanback(Context context);
* public static Boolean maybeHasFeature(String feature, int version);
+ * public static ArrayMap<String, FeatureInfo> getCompileTimeAvailableFeatures();
* }
* </pre>
*/
@@ -58,6 +62,7 @@
private const val READONLY_ARG = "--readonly="
private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
+ private val FEATUREINFO_CLASS = ClassName.get("android.content.pm", "FeatureInfo")
private val ASSUME_TRUE_CLASS =
ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8")
private val ASSUME_FALSE_CLASS =
@@ -142,6 +147,7 @@
addFeatureMethodsToClass(classBuilder, features.values)
addMaybeFeatureMethodToClass(classBuilder, features.values)
+ addGetFeaturesMethodToClass(classBuilder, features.values)
// TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
JavaFile.builder(outputClassName.packageName(), classBuilder.build())
@@ -225,7 +231,7 @@
/*
* Adds a generic query method to the class with the form: {@code public static boolean
* maybeHasFeature(String featureName, int version)}, returning null if the feature version is
- * undefined or not readonly.
+ * undefined or not (compile-time) readonly.
*
* This method is useful for internal usage within the framework, e.g., from the implementation
* of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only
@@ -274,5 +280,41 @@
builder.addMethod(methodBuilder.build())
}
+ /*
+ * Adds a method to get all compile-time enabled features.
+ *
+ * This method is useful for internal usage within the framework to augment
+ * any system features that are parsed from the various partitions.
+ */
+ private fun addGetFeaturesMethodToClass(
+ builder: TypeSpec.Builder,
+ features: Collection<FeatureInfo>,
+ ) {
+ val methodBuilder =
+ MethodSpec.methodBuilder("getCompileTimeAvailableFeatures")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addAnnotation(ClassName.get("android.annotation", "NonNull"))
+ .addJavadoc("Gets features marked as available at compile-time, keyed by name." +
+ "\n\n@hide")
+ .returns(ParameterizedTypeName.get(
+ ClassName.get(Map::class.java),
+ ClassName.get(String::class.java),
+ FEATUREINFO_CLASS))
+
+ val availableFeatures = features.filter { it.readonly && it.version != null }
+ methodBuilder.addStatement("Map<String, FeatureInfo> features = new \$T<>(\$L)",
+ HashMap::class.java, availableFeatures.size)
+ if (!availableFeatures.isEmpty()) {
+ methodBuilder.addStatement("FeatureInfo fi = new FeatureInfo()")
+ }
+ for (feature in availableFeatures) {
+ methodBuilder.addStatement("fi.name = \$T.\$N", PACKAGEMANAGER_CLASS, feature.name)
+ methodBuilder.addStatement("fi.version = \$L", feature.version)
+ methodBuilder.addStatement("features.put(fi.name, new FeatureInfo(fi))")
+ }
+ methodBuilder.addStatement("return features")
+ builder.addMethod(methodBuilder.build())
+ }
+
private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean)
}
diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen
index dfc2937..edbfc42 100644
--- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen
@@ -8,11 +8,15 @@
// --feature-apis=WATCH,PC
package com.android.systemfeatures;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import com.android.aconfig.annotations.AssumeFalseForR8;
import com.android.aconfig.annotations.AssumeTrueForR8;
+import java.util.HashMap;
+import java.util.Map;
/**
* @hide
@@ -83,4 +87,22 @@
}
return null;
}
+
+ /**
+ * Gets features marked as available at compile-time, keyed by name.
+ *
+ * @hide
+ */
+ @NonNull
+ public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() {
+ Map<String, FeatureInfo> features = new HashMap<>(2);
+ FeatureInfo fi = new FeatureInfo();
+ fi.name = PackageManager.FEATURE_WATCH;
+ fi.version = 1;
+ features.put(fi.name, new FeatureInfo(fi));
+ fi.name = PackageManager.FEATURE_WIFI;
+ fi.version = 0;
+ features.put(fi.name, new FeatureInfo(fi));
+ return features;
+ }
}
diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
index 59c5b4e..bf7a006 100644
--- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen
@@ -4,9 +4,13 @@
// --feature-apis=WATCH
package com.android.systemfeatures;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
+import java.util.HashMap;
+import java.util.Map;
/**
* @hide
@@ -32,4 +36,15 @@
public static Boolean maybeHasFeature(String featureName, int version) {
return null;
}
+
+ /**
+ * Gets features marked as available at compile-time, keyed by name.
+ *
+ * @hide
+ */
+ @NonNull
+ public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() {
+ Map<String, FeatureInfo> features = new HashMap<>(0);
+ return features;
+ }
}
diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen
index 89097fb..b20b228 100644
--- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen
@@ -7,9 +7,13 @@
// --feature=AUTO:
package com.android.systemfeatures;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
+import java.util.HashMap;
+import java.util.Map;
/**
* @hide
@@ -62,4 +66,15 @@
public static Boolean maybeHasFeature(String featureName, int version) {
return null;
}
+
+ /**
+ * Gets features marked as available at compile-time, keyed by name.
+ *
+ * @hide
+ */
+ @NonNull
+ public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() {
+ Map<String, FeatureInfo> features = new HashMap<>(0);
+ return features;
+ }
}
diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
index 2111d56..d91f5b6 100644
--- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
+++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen
@@ -3,8 +3,12 @@
// --readonly=false
package com.android.systemfeatures;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.FeatureInfo;
+import java.util.HashMap;
+import java.util.Map;
/**
* @hide
@@ -21,4 +25,15 @@
public static Boolean maybeHasFeature(String featureName, int version) {
return null;
}
+
+ /**
+ * Gets features marked as available at compile-time, keyed by name.
+ *
+ * @hide
+ */
+ @NonNull
+ public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() {
+ Map<String, FeatureInfo> features = new HashMap<>(0);
+ return features;
+ }
}
diff --git a/tools/systemfeatures/tests/src/FeatureInfo.java b/tools/systemfeatures/tests/src/FeatureInfo.java
new file mode 100644
index 0000000..9d57edc
--- /dev/null
+++ b/tools/systemfeatures/tests/src/FeatureInfo.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+/** Stub for testing */
+public final class FeatureInfo {
+ public String name;
+ public int version;
+
+ public FeatureInfo() {}
+
+ public FeatureInfo(FeatureInfo orig) {
+ name = orig.name;
+ version = orig.version;
+ }
+}
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
index c3a23cb..39f8fc4 100644
--- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
+++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java
@@ -25,6 +25,7 @@
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.pm.FeatureInfo;
import android.content.pm.PackageManager;
import org.junit.Before;
@@ -36,6 +37,8 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.Map;
+
@RunWith(JUnit4.class)
public class SystemFeaturesGeneratorTest {
@@ -57,6 +60,7 @@
assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ assertThat(RwNoFeatures.getCompileTimeAvailableFeatures()).isEmpty();
}
@Test
@@ -68,6 +72,7 @@
assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ assertThat(RoNoFeatures.getCompileTimeAvailableFeatures()).isEmpty();
// Also ensure we fall back to the PackageManager for feature APIs without an accompanying
// versioned feature definition.
@@ -101,6 +106,7 @@
assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ assertThat(RwFeatures.getCompileTimeAvailableFeatures()).isEmpty();
}
@Test
@@ -156,5 +162,11 @@
assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull();
assertThat(RoFeatures.maybeHasFeature("", 0)).isNull();
+
+ Map<String, FeatureInfo> compiledFeatures = RoFeatures.getCompileTimeAvailableFeatures();
+ assertThat(compiledFeatures.keySet())
+ .containsExactly(PackageManager.FEATURE_WATCH, PackageManager.FEATURE_WIFI);
+ assertThat(compiledFeatures.get(PackageManager.FEATURE_WATCH).version).isEqualTo(1);
+ assertThat(compiledFeatures.get(PackageManager.FEATURE_WIFI).version).isEqualTo(0);
}
}