Merge "[Satellite] Reading the datagram value from carrierConfig." into main
diff --git a/core/api/current.txt b/core/api/current.txt
index fd61800..b95295c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -10864,6 +10864,7 @@
field public static final String IPSEC_SERVICE = "ipsec";
field public static final String JOB_SCHEDULER_SERVICE = "jobscheduler";
field public static final String KEYGUARD_SERVICE = "keyguard";
+ field @FlaggedApi("android.security.keystore_grant_api") public static final String KEYSTORE_SERVICE = "keystore";
field public static final String LAUNCHER_APPS_SERVICE = "launcherapps";
field @UiContext public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
field public static final String LOCALE_SERVICE = "locale";
@@ -40166,6 +40167,14 @@
method @NonNull public android.security.keystore.KeyProtection.Builder setUserPresenceRequired(boolean);
}
+ @FlaggedApi("android.security.keystore_grant_api") public class KeyStoreManager {
+ method @NonNull public java.util.List<java.security.cert.X509Certificate> getGrantedCertificateChainFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
+ method @NonNull public java.security.Key getGrantedKeyFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
+ method @NonNull public java.security.KeyPair getGrantedKeyPairFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException;
+ method public long grantKeyAccess(@NonNull String, int) throws android.security.KeyStoreException, java.security.UnrecoverableKeyException;
+ method public void revokeKeyAccess(@NonNull String, int) throws android.security.KeyStoreException, java.security.UnrecoverableKeyException;
+ }
+
public class SecureKeyImportUnavailableException extends java.security.ProviderException {
ctor public SecureKeyImportUnavailableException();
ctor public SecureKeyImportUnavailableException(String);
@@ -52648,6 +52657,8 @@
ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int);
ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int, int);
method public void applyTransactionToFrame(@NonNull android.view.SurfaceControl.Transaction);
+ method @FlaggedApi("android.view.flags.surface_view_get_surface_package") public void clearChildSurfacePackage();
+ method @FlaggedApi("android.view.flags.surface_view_get_surface_package") @Nullable public android.view.SurfaceControlViewHost.SurfacePackage getChildSurfacePackage();
method @FlaggedApi("android.view.flags.surface_view_set_composition_order") public int getCompositionOrder();
method public android.view.SurfaceHolder getHolder();
method @Deprecated @Nullable public android.os.IBinder getHostToken();
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index 4fc90ae..d84a4c1 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -81,6 +81,25 @@
*/
private final ArrayList<WeakReference<Object>> mAnimatorRequestors = new ArrayList<>();
+ /**
+ * The callbacks which will invoke {@link Animator#notifyEndListeners(boolean)} on next frame.
+ * It is only used if {@link Animator#setPostNotifyEndListenerEnabled(boolean)} sets true.
+ */
+ private ArrayList<Runnable> mPendingEndAnimationListeners;
+
+ /**
+ * The value of {@link Choreographer#getVsyncId()} at the last animation frame.
+ * It is only used if {@link Animator#setPostNotifyEndListenerEnabled(boolean)} sets true.
+ */
+ private long mLastAnimationFrameVsyncId;
+
+ /**
+ * The value of {@link Choreographer#getVsyncId()} when calling
+ * {@link Animator#notifyEndListeners(boolean)}.
+ * It is only used if {@link Animator#setPostNotifyEndListenerEnabled(boolean)} sets true.
+ */
+ private long mEndAnimationFrameVsyncId;
+
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
@@ -332,6 +351,39 @@
}
}
+ /**
+ * Returns the vsyncId of last animation frame if the given {@param currentVsyncId} matches
+ * the vsyncId from the end callback of animation. Otherwise it returns the given vsyncId.
+ * It only takes effect if {@link #postEndAnimationCallback(Runnable)} is called.
+ */
+ public long getLastAnimationFrameVsyncId(long currentVsyncId) {
+ return currentVsyncId == mEndAnimationFrameVsyncId && mLastAnimationFrameVsyncId != 0
+ ? mLastAnimationFrameVsyncId : currentVsyncId;
+ }
+
+ /** Runs the given callback on next frame to notify the end of the animation. */
+ public void postEndAnimationCallback(Runnable notifyEndAnimation) {
+ if (mPendingEndAnimationListeners == null) {
+ mPendingEndAnimationListeners = new ArrayList<>();
+ }
+ mPendingEndAnimationListeners.add(notifyEndAnimation);
+ if (mPendingEndAnimationListeners.size() > 1) {
+ return;
+ }
+ final Choreographer choreographer = Choreographer.getInstance();
+ mLastAnimationFrameVsyncId = choreographer.getVsyncId();
+ getProvider().postFrameCallback(frame -> {
+ mEndAnimationFrameVsyncId = choreographer.getVsyncId();
+ // The animation listeners can only get vsyncId of last animation frame in this frame
+ // by getLastAnimationFrameVsyncId(currentVsyncId).
+ while (mPendingEndAnimationListeners.size() > 0) {
+ mPendingEndAnimationListeners.remove(0).run();
+ }
+ mEndAnimationFrameVsyncId = 0;
+ mLastAnimationFrameVsyncId = 0;
+ });
+ }
+
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
diff --git a/core/java/android/animation/Animator.java b/core/java/android/animation/Animator.java
index c58624e..d1eb8e8 100644
--- a/core/java/android/animation/Animator.java
+++ b/core/java/android/animation/Animator.java
@@ -16,6 +16,7 @@
package android.animation;
+import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -23,6 +24,7 @@
import android.content.pm.ActivityInfo.Config;
import android.content.res.ConstantState;
import android.os.Build;
+import android.os.Trace;
import android.util.LongArray;
import java.util.ArrayList;
@@ -73,6 +75,13 @@
private static long sBackgroundPauseDelay = 1000;
/**
+ * If true, when the animation plays normally to the end, the callback
+ * {@link AnimatorListener#onAnimationEnd(Animator)} will be scheduled on the next frame.
+ * It is to avoid the last animation frame being delayed by the implementation of listeners.
+ */
+ static boolean sPostNotifyEndListenerEnabled;
+
+ /**
* A cache of the values in a list. Used so that when calling the list, we have a copy
* of it in case the list is modified while iterating. The array can be reused to avoid
* allocation on every notification.
@@ -124,6 +133,14 @@
}
/**
+ * @see #sPostNotifyEndListenerEnabled
+ * @hide
+ */
+ public static void setPostNotifyEndListenerEnabled(boolean enable) {
+ sPostNotifyEndListenerEnabled = enable;
+ }
+
+ /**
* Starts this animation. If the animation has a nonzero startDelay, the animation will start
* running after that delay elapses. A non-delayed animation will have its initial
* value(s) set immediately, followed by calls to
@@ -635,6 +652,28 @@
}
}
+ void notifyEndListenersFromEndAnimation(boolean isReversing, boolean postNotifyEndListener) {
+ if (postNotifyEndListener) {
+ AnimationHandler.getInstance().postEndAnimationCallback(
+ () -> completeEndAnimation(isReversing, "postNotifyAnimEnd"));
+ } else {
+ completeEndAnimation(isReversing, "notifyAnimEnd");
+ }
+ }
+
+ @CallSuper
+ void completeEndAnimation(boolean isReversing, String notifyListenerTraceName) {
+ final boolean useTrace = mListeners != null && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW);
+ if (useTrace) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, notifyListenerTraceName
+ + "-" + getClass().getSimpleName());
+ }
+ notifyEndListeners(isReversing);
+ if (useTrace) {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
/**
* Calls <code>call</code> for every item in <code>list</code> with <code>animator</code> and
* <code>isReverse</code> as parameters.
diff --git a/core/java/android/animation/AnimatorSet.java b/core/java/android/animation/AnimatorSet.java
index ac37113..76098db 100644
--- a/core/java/android/animation/AnimatorSet.java
+++ b/core/java/android/animation/AnimatorSet.java
@@ -1442,6 +1442,8 @@
}
private void endAnimation() {
+ final boolean postNotifyEndListener = sPostNotifyEndListenerEnabled && mListeners != null
+ && mLastFrameTime > 0;
mStarted = false;
mLastFrameTime = -1;
mFirstFrame = -1;
@@ -1453,7 +1455,12 @@
// No longer receive callbacks
removeAnimationCallback();
- notifyEndListeners(mReversing);
+ notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
+ }
+
+ @Override
+ void completeEndAnimation(boolean isReversing, String notifyListenerTraceName) {
+ super.completeEndAnimation(isReversing, notifyListenerTraceName);
removeAnimationEndListener();
mSelfPulse = true;
mReversing = false;
diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java
index 5de7f38..e849aba 100644
--- a/core/java/android/animation/ValueAnimator.java
+++ b/core/java/android/animation/ValueAnimator.java
@@ -1289,6 +1289,8 @@
if (mAnimationEndRequested) {
return;
}
+ final boolean postNotifyEndListener = sPostNotifyEndListenerEnabled && mListeners != null
+ && mLastFrameTime > 0;
removeAnimationCallback();
mAnimationEndRequested = true;
@@ -1303,15 +1305,20 @@
mStartTime = -1;
mRunning = false;
mStarted = false;
- notifyEndListeners(mReversing);
- // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
- mReversing = false;
+ notifyEndListenersFromEndAnimation(mReversing, postNotifyEndListener);
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
System.identityHashCode(this));
}
}
+ @Override
+ void completeEndAnimation(boolean isReversing, String notifyListenerTraceName) {
+ super.completeEndAnimation(isReversing, notifyListenerTraceName);
+ // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
+ mReversing = false;
+ }
+
/**
* Called internally to start an animation by adding it to the active animations list. Must be
* called on the UI thread.
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index ba71afb..6e4c28f 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -3,50 +3,54 @@
per-file ContextImpl.java = *
# ActivityManager
-per-file ActivityManager* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *ApplicationStartInfo* = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationErrorReport* = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationExitInfo* = file:/services/core/java/com/android/server/am/OWNERS
-per-file Application.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationLoaders.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file ApplicationThreadConstants.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file ContentProviderHolder* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *ForegroundService* = file:/services/core/java/com/android/server/am/OWNERS
-per-file IActivityController.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IActivityManager.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IApplicationThread.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IAppTraceRetriever.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IForegroundServiceObserver.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IInstrumentationWatcher.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IntentService.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IServiceConnection.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IStopUserCallback.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file IUidObserver.aidl = file:/services/core/java/com/android/server/am/OWNERS
-per-file LoadedApk.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file LocalActivityManager.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file PendingIntent* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *Process* = file:/services/core/java/com/android/server/am/OWNERS
-per-file ProfilerInfo* = file:/services/core/java/com/android/server/am/OWNERS
-per-file Service* = file:/services/core/java/com/android/server/am/OWNERS
-per-file SystemServiceRegistry.java = file:/services/core/java/com/android/server/am/OWNERS
-per-file *UserSwitchObserver* = file:/services/core/java/com/android/server/am/OWNERS
+per-file ActivityManager* = file:/ACTIVITY_MANAGER_OWNERS
+per-file Application.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file ApplicationErrorReport* = file:/ACTIVITY_MANAGER_OWNERS
+per-file ApplicationLoaders.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file ApplicationThreadConstants.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file ContentProviderHolder* = file:/ACTIVITY_MANAGER_OWNERS
+per-file *ForegroundService* = file:/ACTIVITY_MANAGER_OWNERS
+per-file IActivityController.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IActivityManager.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IApplicationThread.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IAppTraceRetriever.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IForegroundServiceObserver.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IInstrumentationWatcher.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IntentService.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IServiceConnection.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IStopUserCallback.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file IUidObserver.aidl = file:/ACTIVITY_MANAGER_OWNERS
+per-file LoadedApk.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file LocalActivityManager.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file PendingIntent* = file:/ACTIVITY_MANAGER_OWNERS
+per-file *Process* = file:/ACTIVITY_MANAGER_OWNERS
+per-file ProfilerInfo* = file:/ACTIVITY_MANAGER_OWNERS
+per-file Service* = file:/ACTIVITY_MANAGER_OWNERS
+per-file SystemServiceRegistry.java = file:/ACTIVITY_MANAGER_OWNERS
+per-file *UserSwitchObserver* = file:/ACTIVITY_MANAGER_OWNERS
+
+# UI Automation
per-file *UiAutomation* = file:/services/accessibility/OWNERS
per-file *UiAutomation* = file:/core/java/android/permission/OWNERS
+
+# Game Manager
per-file GameManager* = file:/GAME_MANAGER_OWNERS
per-file GameMode* = file:/GAME_MANAGER_OWNERS
per-file GameState* = file:/GAME_MANAGER_OWNERS
per-file IGameManager* = file:/GAME_MANAGER_OWNERS
per-file IGameMode* = file:/GAME_MANAGER_OWNERS
+
+# Background Starts
per-file BackgroundStartPrivileges.java = file:/BAL_OWNERS
per-file activity_manager.aconfig = file:/ACTIVITY_MANAGER_OWNERS
# ActivityThread
-per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
+per-file ActivityThread.java = file:/ACTIVITY_MANAGER_OWNERS
per-file ActivityThread.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file ActivityThread.java = file:RESOURCES_OWNERS
# Alarm
-per-file *Alarm* = file:/apex/jobscheduler/OWNERS
+per-file *Alarm* = file:/apex/jobscheduler/ALARM_OWNERS
# AppOps
per-file *AppOp* = file:/core/java/android/permission/OWNERS
@@ -97,6 +101,8 @@
# Performance
per-file PropertyInvalidatedCache.java = file:/PERFORMANCE_OWNERS
+per-file *ApplicationStartInfo* = file:/PERFORMANCE_OWNERS
+per-file ApplicationExitInfo* = file:/PERFORMANCE_OWNERS
per-file performance.aconfig = file:/PERFORMANCE_OWNERS
# Pinner
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index c17da24..e4d3baa 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -16,6 +16,8 @@
package android.app;
+import static android.text.TextUtils.formatSimple;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
@@ -30,11 +32,10 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.FastPrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -42,12 +43,14 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
@@ -224,12 +227,24 @@
}
/**
- * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note
- * that all values cause the cache to be skipped.
+ * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note that all
+ * reserved values cause the cache to be skipped.
*/
+ // This is the initial value of all cache keys. It is changed when a cache is invalidated.
private static final int NONCE_UNSET = 0;
+ // This value is used in two ways. First, it is used internally to indicate that the cache is
+ // disabled for the current query. Secondly, it is used to globally disable the cache across
+ // the entire system. Once a cache is disabled, there is no way to enable it again. The
+ // global behavior is unused and will likely be removed in the future.
private static final int NONCE_DISABLED = 1;
+ // The cache is corked, which means that clients must act as though the cache is always
+ // invalid. This is used when the server is processing updates that continuously invalidate
+ // caches. Rather than issuing individual invalidations (which has a performance penalty),
+ // the server corks the caches at the start of the process and uncorks at the end of the
+ // process.
private static final int NONCE_CORKED = 2;
+ // The cache is bypassed for the current query. Unlike UNSET and CORKED, this value is never
+ // written to global store.
private static final int NONCE_BYPASS = 3;
private static boolean isReservedNonce(long n) {
@@ -237,15 +252,27 @@
}
/**
- * The names of the nonces
+ * The names of the reserved nonces.
*/
private static final String[] sNonceName =
new String[]{ "unset", "disabled", "corked", "bypass" };
+ // The standard tag for logging.
private static final String TAG = "PropertyInvalidatedCache";
+
+ // Set this true to enable very chatty logging. Never commit this true.
private static final boolean DEBUG = false;
+
+ // Set this true to enable cache verification. On every cache hit, the cache will compare the
+ // cached value to a value pulled directly from the source. This completely negates any
+ // performance advantage of the cache. Enable it only to test if a particular cache is not
+ // being properly invalidated.
private static final boolean VERIFY = false;
+ // The test mode. This is only used to ensure that the test functions setTestMode() and
+ // testPropertyName() are used correctly.
+ private static boolean sTestMode = false;
+
/**
* The object-private lock.
*/
@@ -277,32 +304,17 @@
private static final Object sCorkLock = new Object();
/**
- * Record the number of invalidate or cork calls that were nops because the cache was already
- * corked. This is static because invalidation is done in a static context. Entries are
- * indexed by the cache property.
- */
- @GuardedBy("sCorkLock")
- private static final HashMap<String, Long> sCorkedInvalidates = new HashMap<>();
-
- /**
- * A map of cache keys that we've "corked". (The values are counts.) When a cache key is
- * corked, we skip the cache invalidate when the cache key is in the unset state --- that
- * is, when a cache key is corked, an invalidation does not enable the cache if somebody
- * else hasn't disabled it.
- */
- @GuardedBy("sCorkLock")
- private static final HashMap<String, Integer> sCorks = new HashMap<>();
-
- /**
* A lock for the global list of caches and cache keys. This must never be taken inside mLock
* or sCorkLock.
*/
private static final Object sGlobalLock = new Object();
/**
- * A map of cache keys that have been disabled in the local process. When a key is
- * disabled locally, existing caches are disabled and the key is saved in this map.
- * Future cache instances that use the same key will be disabled in their constructor.
+ * A map of cache keys that have been disabled in the local process. When a key is disabled
+ * locally, existing caches are disabled and the key is saved in this map. Future cache
+ * instances that use the same key will be disabled in their constructor. Note that "disabled"
+ * means the cache is not used in this process. Invalidation still proceeds normally, because
+ * the cache may be used in other processes.
*/
@GuardedBy("sGlobalLock")
private static final HashSet<String> sDisabledKeys = new HashSet<>();
@@ -315,14 +327,6 @@
private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches = new WeakHashMap<>();
/**
- * Counts of the number of times a cache key was invalidated. Invalidation occurs in a static
- * context with no cache object available, so this is a static map. Entries are indexed by
- * the cache property.
- */
- @GuardedBy("sGlobalLock")
- private static final HashMap<String, Long> sInvalidates = new HashMap<>();
-
- /**
* If sEnabled is false then all cache operations are stubbed out. Set
* it to false inside test processes.
*/
@@ -334,12 +338,6 @@
private final String mPropertyName;
/**
- * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the
- * property exists on the system.
- */
- private volatile SystemProperties.Handle mPropertyHandle;
-
- /**
* The name by which this cache is known. This should normally be the
* binder call that is being cached, but the constructors default it to
* the property name.
@@ -369,7 +367,13 @@
private final LinkedHashMap<Query, Result> mCache;
/**
- * The last value of the {@code mPropertyHandle} that we observed.
+ * The nonce handler for this cache.
+ */
+ @GuardedBy("mLock")
+ private final NonceHandler mNonce;
+
+ /**
+ * The last nonce value that was observed.
*/
@GuardedBy("mLock")
private long mLastSeenNonce = NONCE_UNSET;
@@ -385,6 +389,358 @@
private final int mMaxEntries;
/**
+ * A class to manage cache keys. There is a single instance of this class for each unique key
+ * that is shared by all cache instances that use that key. This class is abstract; subclasses
+ * use different storage mechanisms for the nonces.
+ */
+ private static abstract class NonceHandler {
+ // The name of the nonce.
+ final String mName;
+
+ // A lock to synchronize corking and invalidation.
+ protected final Object mLock = new Object();
+
+ // Count the number of times the property name was invalidated.
+ @GuardedBy("mLock")
+ private int mInvalidated = 0;
+
+ // Count the number of times invalidate or cork calls were nops because the cache was
+ // already corked.
+ @GuardedBy("mLock")
+ private int mCorkedInvalidates = 0;
+
+ // Count the number of corks against this property name. This is not a statistic. It
+ // increases when the property is corked and decreases when the property is uncorked.
+ // Invalidation requests are ignored when the cork count is greater than zero.
+ @GuardedBy("mLock")
+ private int mCorks = 0;
+
+ // True if this handler is in test mode. If it is in test mode, then nonces are stored
+ // and retrieved from mTestNonce.
+ @GuardedBy("mLock")
+ private boolean mTestMode = false;
+
+ /**
+ * The local value of the handler, used during testing but also used directly by the
+ * NonceLocal handler.
+ */
+ @GuardedBy("mLock")
+ protected long mTestNonce = NONCE_UNSET;
+
+ /**
+ * The methods to get and set a nonce from whatever storage is being used. mLock may be
+ * held when these methods are called. Implementations that take locks must behave as
+ * though mLock could be held.
+ */
+ abstract long getNonceInternal();
+ abstract void setNonceInternal(long value);
+
+ NonceHandler(@NonNull String name) {
+ mName = name;
+ }
+
+ /**
+ * Get a nonce from storage. If the handler is in test mode, the nonce is returned from
+ * the local mTestNonce.
+ */
+ long getNonce() {
+ synchronized (mLock) {
+ if (mTestMode) return mTestNonce;
+ }
+ return getNonceInternal();
+ }
+
+ /**
+ * Write a nonce to storage. If the handler is in test mode, the nonce is written to the
+ * local mTestNonce and storage is not affected.
+ */
+ void setNonce(long val) {
+ synchronized (mLock) {
+ if (mTestMode) {
+ mTestNonce = val;
+ return;
+ }
+ }
+ setNonceInternal(val);
+ }
+
+ /**
+ * Write the invalidation nonce for the property.
+ */
+ void invalidate() {
+ if (!sEnabled) {
+ if (DEBUG) {
+ Log.d(TAG, formatSimple("cache invalidate %s suppressed", mName));
+ }
+ return;
+ }
+
+ synchronized (mLock) {
+ if (mCorks > 0) {
+ if (DEBUG) {
+ Log.d(TAG, "ignoring invalidation due to cork: " + mName);
+ }
+ mCorkedInvalidates++;
+ return;
+ }
+
+ final long nonce = getNonce();
+ if (nonce == NONCE_DISABLED) {
+ if (DEBUG) {
+ Log.d(TAG, "refusing to invalidate disabled cache: " + mName);
+ }
+ return;
+ }
+
+ long newValue;
+ do {
+ newValue = NoPreloadHolder.next();
+ } while (isReservedNonce(newValue));
+ if (DEBUG) {
+ Log.d(TAG, formatSimple(
+ "invalidating cache [%s]: [%s] -> [%s]",
+ mName, nonce, Long.toString(newValue)));
+ }
+ // There is a small race with concurrent disables here. A compare-and-exchange
+ // property operation would be required to eliminate the race condition.
+ setNonce(newValue);
+ mInvalidated++;
+ }
+ }
+
+ void cork() {
+ if (!sEnabled) {
+ if (DEBUG) {
+ Log.d(TAG, formatSimple("cache corking %s suppressed", mName));
+ }
+ return;
+ }
+
+ synchronized (mLock) {
+ int numberCorks = mCorks;
+ if (DEBUG) {
+ Log.d(TAG, formatSimple(
+ "corking %s: numberCorks=%s", mName, numberCorks));
+ }
+
+ // If we're the first ones to cork this cache, set the cache to the corked state so
+ // existing caches talk directly to their services while we've corked updates.
+ // Make sure we don't clobber a disabled cache value.
+
+ // TODO: we can skip this property write and leave the cache enabled if the
+ // caller promises not to make observable changes to the cache backing state before
+ // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
+ // Implement this more dangerous mode of operation if necessary.
+ if (numberCorks == 0) {
+ final long nonce = getNonce();
+ if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
+ setNonce(NONCE_CORKED);
+ }
+ } else {
+ mCorkedInvalidates++;
+ }
+ mCorks++;
+ if (DEBUG) {
+ Log.d(TAG, "corked: " + mName);
+ }
+ }
+ }
+
+ void uncork() {
+ if (!sEnabled) {
+ if (DEBUG) {
+ Log.d(TAG, formatSimple("cache uncorking %s suppressed", mName));
+ }
+ return;
+ }
+
+ synchronized (mLock) {
+ int numberCorks = --mCorks;
+ if (DEBUG) {
+ Log.d(TAG, formatSimple(
+ "uncorking %s: numberCorks=%s", mName, numberCorks));
+ }
+
+ if (numberCorks < 0) {
+ throw new AssertionError("cork underflow: " + mName);
+ }
+ if (numberCorks == 0) {
+ // The property is fully uncorked and can be invalidated normally.
+ invalidate();
+ if (DEBUG) {
+ Log.d(TAG, "uncorked: " + mName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Globally (that is, system-wide) disable all caches that use this key. There is no way
+ * to re-enable these caches.
+ */
+ void disable() {
+ if (!sEnabled) {
+ return;
+ }
+ synchronized (mLock) {
+ setNonce(NONCE_DISABLED);
+ }
+ }
+
+ /**
+ * Put this handler in or out of test mode. Regardless of the current and next mode, the
+ * test nonce variable is reset to UNSET.
+ */
+ void setTestMode(boolean mode) {
+ synchronized (mLock) {
+ mTestMode = mode;
+ mTestNonce = NONCE_UNSET;
+ }
+ }
+
+ /**
+ * Return the statistics associated with the key. These statistics are not associated
+ * with any individual cache.
+ */
+ record Stats(int invalidated, int corkedInvalidates) {}
+ Stats getStats() {
+ synchronized (mLock) {
+ return new Stats(mInvalidated, mCorkedInvalidates);
+ }
+ }
+ }
+
+ /**
+ * Manage nonces that are stored in a system property.
+ */
+ private static final class NonceSysprop extends NonceHandler {
+ // A handle to the property, for fast lookups.
+ private volatile SystemProperties.Handle mHandle;
+
+ NonceSysprop(@NonNull String name) {
+ super(name);
+ }
+
+ /**
+ * Retrieve the nonce from the system property. If the handle is null, this method
+ * attempts to create a handle. If handle creation fails, the method returns UNSET. If
+ * the handle is not null, the method returns a value read via the handle. This read
+ * occurs outside any lock.
+ */
+ @Override
+ long getNonceInternal() {
+ if (mHandle == null) {
+ synchronized (mLock) {
+ if (mHandle == null) {
+ mHandle = SystemProperties.find(mName);
+ if (mHandle == null) {
+ return NONCE_UNSET;
+ }
+ }
+ }
+ }
+ return mHandle.getLong(NONCE_UNSET);
+ }
+
+ /**
+ * Write a nonce to a system property.
+ */
+ @Override
+ void setNonceInternal(long value) {
+ // Failing to set the nonce is a fatal error. Failures setting a system property have
+ // been reported; given that the failure is probably transient, this function includes
+ // a retry.
+ final String str = Long.toString(value);
+ RuntimeException failure = null;
+ for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
+ try {
+ SystemProperties.set(mName, str);
+ if (attempt > 0) {
+ // This log is not guarded. Based on known bug reports, it should
+ // occur once a week or less. The purpose of the log message is to
+ // identify the retries as a source of delay that might be otherwise
+ // be attributed to the cache itself.
+ Log.w(TAG, "Nonce set after " + attempt + " tries");
+ }
+ return;
+ } catch (RuntimeException e) {
+ if (failure == null) {
+ failure = e;
+ }
+ try {
+ Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
+ } catch (InterruptedException x) {
+ // Ignore this exception. The desired delay is only approximate and
+ // there is no issue if the sleep sometimes terminates early.
+ }
+ }
+ }
+ // This point is reached only if SystemProperties.set() fails at least once.
+ // Rethrow the first exception that was received.
+ throw failure;
+ }
+ }
+
+ /**
+ * SystemProperties and shared storage are protected and cannot be written by random
+ * processes. So, for testing purposes, the NonceLocal handler stores the nonce locally. The
+ * NonceLocal uses the mTestNonce in the superclass, regardless of test mode.
+ */
+ private static class NonceLocal extends NonceHandler {
+ // The saved nonce.
+ private long mValue;
+
+ NonceLocal(@NonNull String name) {
+ super(name);
+ }
+
+ @Override
+ long getNonceInternal() {
+ return mTestNonce;
+ }
+
+ @Override
+ void setNonceInternal(long value) {
+ mTestNonce = value;
+ }
+ }
+
+ /**
+ * Complete key prefixes.
+ */
+ private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + ".";
+
+ /**
+ * A static list of nonce handlers, indexed by name. NonceHandlers can be safely shared by
+ * multiple threads, and can therefore be shared by multiple instances of the same cache, and
+ * with static calls (see {@link #invalidateCache}. Addition and removal are guarded by the
+ * global lock, to ensure that duplicates are not created.
+ */
+ private static final ConcurrentHashMap<String, NonceHandler> sHandlers
+ = new ConcurrentHashMap<>();
+
+ /**
+ * Return the proper nonce handler, based on the property name.
+ */
+ private static NonceHandler getNonceHandler(@NonNull String name) {
+ NonceHandler h = sHandlers.get(name);
+ if (h == null) {
+ synchronized (sGlobalLock) {
+ h = sHandlers.get(name);
+ if (h == null) {
+ if (name.startsWith(PREFIX_TEST)) {
+ h = new NonceLocal(name);
+ } else {
+ h = new NonceSysprop(name);
+ }
+ sHandlers.put(name, h);
+ }
+ }
+ }
+ return h;
+ }
+
+ /**
* Make a new property invalidated cache. This constructor names the cache after the
* property name. New clients should prefer the constructor that takes an explicit
* cache name.
@@ -417,6 +773,7 @@
mPropertyName = propertyName;
validateCacheKey(mPropertyName);
mCacheName = cacheName;
+ mNonce = getNonceHandler(mPropertyName);
mMaxEntries = maxEntries;
mComputer = new DefaultComputer<>(this);
mCache = createMap();
@@ -441,6 +798,7 @@
mPropertyName = createPropertyName(module, api);
validateCacheKey(mPropertyName);
mCacheName = cacheName;
+ mNonce = getNonceHandler(mPropertyName);
mMaxEntries = maxEntries;
mComputer = computer;
mCache = createMap();
@@ -484,130 +842,69 @@
}
/**
- * SystemProperties are protected and cannot be written (or read, usually) by random
- * processes. So, for testing purposes, the methods have a bypass mode that reads and
- * writes to a HashMap and does not go out to the SystemProperties at all.
- */
-
- // If true, the cache might be under test. If false, there is no testing in progress.
- private static volatile boolean sTesting = false;
-
- // If sTesting is true then keys that are under test are in this map.
- private static final HashMap<String, Long> sTestingPropertyMap = new HashMap<>();
-
- /**
- * Enable or disable testing. The testing property map is cleared every time this
- * method is called.
+ * Enable or disable testing. The protocol requires that the mode toggle: for instance, it is
+ * illegal to clear the test mode if the test mode is already off. The purpose is solely to
+ * ensure that test clients do not forget to use the test mode properly, even though the
+ * current logic does not care.
* @hide
*/
@TestApi
public static void setTestMode(boolean mode) {
- sTesting = mode;
- synchronized (sTestingPropertyMap) {
- sTestingPropertyMap.clear();
+ synchronized (sGlobalLock) {
+ if (sTestMode == mode) {
+ throw new IllegalStateException("cannot set test mode redundantly: mode=" + mode);
+ }
+ sTestMode = mode;
+ if (mode) {
+ // No action when testing begins.
+ } else {
+ resetAfterTestLocked();
+ }
}
}
/**
- * Enable testing the specific cache key. Only keys in the map are subject to testing.
- * There is no method to stop testing a property name. Just disable the test mode.
+ * Clean up when testing ends. All handlers are reset out of test mode. NonceLocal handlers
+ * (MODULE_TEST) are reset to the NONCE_UNSET state. This has no effect on any other handlers
+ * that were not originally in test mode.
*/
- private static void testPropertyName(@NonNull String name) {
- synchronized (sTestingPropertyMap) {
- sTestingPropertyMap.put(name, (long) NONCE_UNSET);
+ @GuardedBy("sGlobalLock")
+ private static void resetAfterTestLocked() {
+ for (Iterator<String> e = sHandlers.keys().asIterator(); e.hasNext(); ) {
+ String s = e.next();
+ final NonceHandler h = sHandlers.get(s);
+ h.setTestMode(false);
}
}
/**
- * Enable testing the specific cache key. Only keys in the map are subject to testing.
- * There is no method to stop testing a property name. Just disable the test mode.
+ * Enable testing the specific cache key. This API allows a test process to invalidate caches
+ * for which it would not otherwise have permission. Caches in test mode do NOT write their
+ * values to the system properties. The effect is local to the current process. Test mode
+ * must be true when this method is called.
* @hide
*/
@TestApi
public void testPropertyName() {
- testPropertyName(mPropertyName);
+ synchronized (sGlobalLock) {
+ if (sTestMode == false) {
+ throw new IllegalStateException("cannot test property name with test mode off");
+ }
+ mNonce.setTestMode(true);
+ }
}
- // Read the system property associated with the current cache. This method uses the
- // handle for faster reading.
+ // Read the nonce associated with the current cache.
+ @GuardedBy("mLock")
private long getCurrentNonce() {
- if (sTesting) {
- synchronized (sTestingPropertyMap) {
- Long n = sTestingPropertyMap.get(mPropertyName);
- if (n != null) {
- return n;
- }
- }
- }
-
- SystemProperties.Handle handle = mPropertyHandle;
- if (handle == null) {
- handle = SystemProperties.find(mPropertyName);
- if (handle == null) {
- return NONCE_UNSET;
- }
- mPropertyHandle = handle;
- }
- return handle.getLong(NONCE_UNSET);
- }
-
- // Write the nonce in a static context. No handle is available.
- private static void setNonce(String name, long val) {
- if (sTesting) {
- synchronized (sTestingPropertyMap) {
- Long n = sTestingPropertyMap.get(name);
- if (n != null) {
- sTestingPropertyMap.put(name, val);
- return;
- }
- }
- }
- RuntimeException failure = null;
- for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
- try {
- SystemProperties.set(name, Long.toString(val));
- if (attempt > 0) {
- // This log is not guarded. Based on known bug reports, it should
- // occur once a week or less. The purpose of the log message is to
- // identify the retries as a source of delay that might be otherwise
- // be attributed to the cache itself.
- Log.w(TAG, "Nonce set after " + attempt + " tries");
- }
- return;
- } catch (RuntimeException e) {
- if (failure == null) {
- failure = e;
- }
- try {
- Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
- } catch (InterruptedException x) {
- // Ignore this exception. The desired delay is only approximate and
- // there is no issue if the sleep sometimes terminates early.
- }
- }
- }
- // This point is reached only if SystemProperties.set() fails at least once.
- // Rethrow the first exception that was received.
- throw failure;
- }
-
- // Set the nonce in a static context. No handle is available.
- private static long getNonce(String name) {
- if (sTesting) {
- synchronized (sTestingPropertyMap) {
- Long n = sTestingPropertyMap.get(name);
- if (n != null) {
- return n;
- }
- }
- }
- return SystemProperties.getLong(name, NONCE_UNSET);
+ return mNonce.getNonce();
}
/**
- * Forget all cached values.
- * TODO(216112648) remove this as a public API. Clients should invalidate caches, not clear
- * them.
+ * Forget all cached values. This is used by a client when the server exits. Since the
+ * server has exited, the cache values are no longer valid, but the server is no longer
+ * present to invalidate the cache. Note that this is not necessary if the server is
+ * system_server, because the entire operating system reboots if that process exits.
* @hide
*/
public final void clear() {
@@ -674,7 +971,7 @@
}
/**
- * Disable the use of this cache in this process. This method is using internally and during
+ * Disable the use of this cache in this process. This method is used internally and during
* testing. To disable a cache in normal code, use disableLocal(). A disabled cache cannot
* be re-enabled.
* @hide
@@ -783,7 +1080,7 @@
if (DEBUG) {
if (!mDisabled) {
- Log.d(TAG, TextUtils.formatSimple(
+ Log.d(TAG, formatSimple(
"cache %s %s for %s",
cacheName(), sNonceName[(int) currentNonce], queryToString(query)));
}
@@ -798,7 +1095,7 @@
if (cachedResult != null) mHits++;
} else {
if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple(
+ Log.d(TAG, formatSimple(
"clearing cache %s of %d entries because nonce changed [%s] -> [%s]",
cacheName(), mCache.size(),
mLastSeenNonce, currentNonce));
@@ -824,7 +1121,7 @@
if (currentNonce != afterRefreshNonce) {
currentNonce = afterRefreshNonce;
if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple(
+ Log.d(TAG, formatSimple(
"restarting %s %s because nonce changed in refresh",
cacheName(),
queryToString(query)));
@@ -895,20 +1192,18 @@
* @param name Name of the cache-key property to invalidate
*/
private static void disableSystemWide(@NonNull String name) {
- if (!sEnabled) {
- return;
- }
- setNonce(name, NONCE_DISABLED);
+ getNonceHandler(name).disable();
}
/**
- * Non-static convenience version of invalidateCache() for situations in which only a single
- * PropertyInvalidatedCache is keyed on a particular property value.
+ * Non-static version of invalidateCache() for situations in which a cache instance is
+ * available. This is slightly faster than than the static versions because it does not have
+ * to look up the NonceHandler for a given property name.
* @hide
*/
@TestApi
public void invalidateCache() {
- invalidateCache(mPropertyName);
+ mNonce.invalidate();
}
/**
@@ -931,59 +1226,7 @@
* @hide
*/
public static void invalidateCache(@NonNull String name) {
- if (!sEnabled) {
- if (DEBUG) {
- Log.w(TAG, TextUtils.formatSimple(
- "cache invalidate %s suppressed", name));
- }
- return;
- }
-
- // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't
- // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork.
- // The property service is single-threaded anyway, so we don't lose any concurrency by
- // taking the cork lock around cache invalidations. If we see contention on this lock,
- // we're invalidating too often.
- synchronized (sCorkLock) {
- Integer numberCorks = sCorks.get(name);
- if (numberCorks != null && numberCorks > 0) {
- if (DEBUG) {
- Log.d(TAG, "ignoring invalidation due to cork: " + name);
- }
- final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
- sCorkedInvalidates.put(name, count + 1);
- return;
- }
- invalidateCacheLocked(name);
- }
- }
-
- @GuardedBy("sCorkLock")
- private static void invalidateCacheLocked(@NonNull String name) {
- // There's no race here: we don't require that values strictly increase, but instead
- // only that each is unique in a single runtime-restart session.
- final long nonce = getNonce(name);
- if (nonce == NONCE_DISABLED) {
- if (DEBUG) {
- Log.d(TAG, "refusing to invalidate disabled cache: " + name);
- }
- return;
- }
-
- long newValue;
- do {
- newValue = NoPreloadHolder.next();
- } while (isReservedNonce(newValue));
- if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple(
- "invalidating cache [%s]: [%s] -> [%s]",
- name, nonce, Long.toString(newValue)));
- }
- // There is a small race with concurrent disables here. A compare-and-exchange
- // property operation would be required to eliminate the race condition.
- setNonce(name, newValue);
- long invalidateCount = sInvalidates.getOrDefault(name, (long) 0);
- sInvalidates.put(name, ++invalidateCount);
+ getNonceHandler(name).invalidate();
}
/**
@@ -1000,43 +1243,7 @@
* @hide
*/
public static void corkInvalidations(@NonNull String name) {
- if (!sEnabled) {
- if (DEBUG) {
- Log.w(TAG, TextUtils.formatSimple(
- "cache cork %s suppressed", name));
- }
- return;
- }
-
- synchronized (sCorkLock) {
- int numberCorks = sCorks.getOrDefault(name, 0);
- if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple(
- "corking %s: numberCorks=%s", name, numberCorks));
- }
-
- // If we're the first ones to cork this cache, set the cache to the corked state so
- // existing caches talk directly to their services while we've corked updates.
- // Make sure we don't clobber a disabled cache value.
-
- // TODO(dancol): we can skip this property write and leave the cache enabled if the
- // caller promises not to make observable changes to the cache backing state before
- // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
- // Implement this more dangerous mode of operation if necessary.
- if (numberCorks == 0) {
- final long nonce = getNonce(name);
- if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
- setNonce(name, NONCE_CORKED);
- }
- } else {
- final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
- sCorkedInvalidates.put(name, count + 1);
- }
- sCorks.put(name, numberCorks + 1);
- if (DEBUG) {
- Log.d(TAG, "corked: " + name);
- }
- }
+ getNonceHandler(name).cork();
}
/**
@@ -1048,34 +1255,7 @@
* @hide
*/
public static void uncorkInvalidations(@NonNull String name) {
- if (!sEnabled) {
- if (DEBUG) {
- Log.w(TAG, TextUtils.formatSimple(
- "cache uncork %s suppressed", name));
- }
- return;
- }
-
- synchronized (sCorkLock) {
- int numberCorks = sCorks.getOrDefault(name, 0);
- if (DEBUG) {
- Log.d(TAG, TextUtils.formatSimple(
- "uncorking %s: numberCorks=%s", name, numberCorks));
- }
-
- if (numberCorks < 1) {
- throw new AssertionError("cork underflow: " + name);
- }
- if (numberCorks == 1) {
- sCorks.remove(name);
- invalidateCacheLocked(name);
- if (DEBUG) {
- Log.d(TAG, "uncorked: " + name);
- }
- } else {
- sCorks.put(name, numberCorks - 1);
- }
- }
+ getNonceHandler(name).uncork();
}
/**
@@ -1104,6 +1284,8 @@
@GuardedBy("mLock")
private Handler mHandler;
+ private NonceHandler mNonce;
+
public AutoCorker(@NonNull String propertyName) {
this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS);
}
@@ -1117,31 +1299,35 @@
}
public void autoCork() {
+ synchronized (mLock) {
+ if (mNonce == null) {
+ mNonce = getNonceHandler(mPropertyName);
+ }
+ }
+
if (getLooper() == null) {
// We're not ready to auto-cork yet, so just invalidate the cache immediately.
if (DEBUG) {
Log.w(TAG, "invalidating instead of autocorking early in init: "
+ mPropertyName);
}
- PropertyInvalidatedCache.invalidateCache(mPropertyName);
+ mNonce.invalidate();
return;
}
synchronized (mLock) {
boolean alreadyQueued = mUncorkDeadlineMs >= 0;
if (DEBUG) {
- Log.w(TAG, TextUtils.formatSimple(
+ Log.d(TAG, formatSimple(
"autoCork %s mUncorkDeadlineMs=%s", mPropertyName,
mUncorkDeadlineMs));
}
mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs;
if (!alreadyQueued) {
getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
- PropertyInvalidatedCache.corkInvalidations(mPropertyName);
+ mNonce.cork();
} else {
- synchronized (sCorkLock) {
- final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
- sCorkedInvalidates.put(mPropertyName, count + 1);
- }
+ // Count this as a corked invalidation.
+ mNonce.invalidate();
}
}
}
@@ -1149,7 +1335,7 @@
private void handleMessage(Message msg) {
synchronized (mLock) {
if (DEBUG) {
- Log.w(TAG, TextUtils.formatSimple(
+ Log.d(TAG, formatSimple(
"handleMsesage %s mUncorkDeadlineMs=%s",
mPropertyName, mUncorkDeadlineMs));
}
@@ -1161,7 +1347,7 @@
if (mUncorkDeadlineMs > nowMs) {
mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs;
if (DEBUG) {
- Log.w(TAG, TextUtils.formatSimple(
+ Log.d(TAG, formatSimple(
"scheduling uncork at %s",
mUncorkDeadlineMs));
}
@@ -1169,10 +1355,10 @@
return;
}
if (DEBUG) {
- Log.w(TAG, "automatic uncorking " + mPropertyName);
+ Log.d(TAG, "automatic uncorking " + mPropertyName);
}
mUncorkDeadlineMs = -1;
- PropertyInvalidatedCache.uncorkInvalidations(mPropertyName);
+ mNonce.uncork();
}
}
@@ -1207,7 +1393,7 @@
Result resultToCompare = recompute(query);
boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce);
if (!nonceChanged && !resultEquals(proposedResult, resultToCompare)) {
- Log.e(TAG, TextUtils.formatSimple(
+ Log.e(TAG, formatSimple(
"cache %s inconsistent for %s is %s should be %s",
cacheName(), queryToString(query),
proposedResult, resultToCompare));
@@ -1284,17 +1470,9 @@
/**
* Returns a list of caches alive at the current time.
*/
- @GuardedBy("sGlobalLock")
private static @NonNull ArrayList<PropertyInvalidatedCache> getActiveCaches() {
- return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
- }
-
- /**
- * Returns a list of the active corks in a process.
- */
- private static @NonNull ArrayList<Map.Entry<String, Integer>> getActiveCorks() {
- synchronized (sCorkLock) {
- return new ArrayList<Map.Entry<String, Integer>>(sCorks.entrySet());
+ synchronized (sGlobalLock) {
+ return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
}
}
@@ -1361,32 +1539,27 @@
return;
}
- long invalidateCount;
- long corkedInvalidates;
- synchronized (sCorkLock) {
- invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0);
- corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
- }
+ NonceHandler.Stats stats = mNonce.getStats();
synchronized (mLock) {
- pw.println(TextUtils.formatSimple(" Cache Name: %s", cacheName()));
- pw.println(TextUtils.formatSimple(" Property: %s", mPropertyName));
+ pw.println(formatSimple(" Cache Name: %s", cacheName()));
+ pw.println(formatSimple(" Property: %s", mPropertyName));
final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED]
+ mSkips[NONCE_BYPASS];
- pw.println(TextUtils.formatSimple(
+ pw.println(formatSimple(
" Hits: %d, Misses: %d, Skips: %d, Clears: %d",
mHits, mMisses, skips, mClears));
- pw.println(TextUtils.formatSimple(
+ pw.println(formatSimple(
" Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d",
mSkips[NONCE_CORKED], mSkips[NONCE_UNSET],
mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED]));
- pw.println(TextUtils.formatSimple(
+ pw.println(formatSimple(
" Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d",
- mLastSeenNonce, invalidateCount, corkedInvalidates));
- pw.println(TextUtils.formatSimple(
+ mLastSeenNonce, stats.invalidated, stats.corkedInvalidates));
+ pw.println(formatSimple(
" Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d",
mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow));
- pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true"));
+ pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true"));
pw.println("");
// No specific cache was requested. This is the default, and no details
@@ -1404,23 +1577,7 @@
String key = Objects.toString(entry.getKey());
String value = Objects.toString(entry.getValue());
- pw.println(TextUtils.formatSimple(" Key: %s\n Value: %s\n", key, value));
- }
- }
- }
-
- /**
- * Dump the corking status.
- */
- @GuardedBy("sCorkLock")
- private static void dumpCorkInfo(PrintWriter pw) {
- ArrayList<Map.Entry<String, Integer>> activeCorks = getActiveCorks();
- if (activeCorks.size() > 0) {
- pw.println(" Corking Status:");
- for (int i = 0; i < activeCorks.size(); i++) {
- Map.Entry<String, Integer> entry = activeCorks.get(i);
- pw.println(TextUtils.formatSimple(" Property Name: %s Count: %d",
- entry.getKey(), entry.getValue()));
+ pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value));
}
}
}
@@ -1441,14 +1598,7 @@
// then only that cache is reported.
boolean detail = anyDetailed(args);
- ArrayList<PropertyInvalidatedCache> activeCaches;
- synchronized (sGlobalLock) {
- activeCaches = getActiveCaches();
- if (!detail) {
- dumpCorkInfo(pw);
- }
- }
-
+ ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches();
for (int i = 0; i < activeCaches.size(); i++) {
PropertyInvalidatedCache currentCache = activeCaches.get(i);
currentCache.dumpContents(pw, detail, args);
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index cc90ce5..bd26db5 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -236,6 +236,7 @@
import android.security.advancedprotection.IAdvancedProtectionService;
import android.security.attestationverification.AttestationVerificationManager;
import android.security.attestationverification.IAttestationVerificationManagerService;
+import android.security.keystore.KeyStoreManager;
import android.service.oemlock.IOemLockService;
import android.service.oemlock.OemLockManager;
import android.service.persistentdata.IPersistentDataBlockService;
@@ -1705,6 +1706,17 @@
}
});
+ registerService(Context.KEYSTORE_SERVICE, KeyStoreManager.class,
+ new StaticServiceFetcher<KeyStoreManager>() {
+ @Override
+ public KeyStoreManager createService()
+ throws ServiceNotFoundException {
+ if (!android.security.Flags.keystoreGrantApi()) {
+ throw new ServiceNotFoundException("KeyStoreManager is not supported");
+ }
+ return KeyStoreManager.getInstance();
+ }});
+
registerService(Context.CONTACT_KEYS_SERVICE, E2eeContactKeysManager.class,
new CachedServiceFetcher<E2eeContactKeysManager>() {
@Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index ffa3375..07106e8 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4770,6 +4770,18 @@
/**
* Use with {@link #getSystemService(String)} to retrieve a {@link
+ * android.security.keystore.KeyStoreManager} for accessing
+ * <a href="/privacy-and-security/keystore">Android Keystore</a>
+ * functions.
+ *
+ * @see #getSystemService(String)
+ * @see android.security.keystore.KeyStoreManager
+ */
+ @FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API)
+ public static final String KEYSTORE_SERVICE = "keystore";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.storage.StorageManager} for accessing system storage
* functions.
*
diff --git a/core/java/android/content/OWNERS b/core/java/android/content/OWNERS
index 6d9dc45..392f62a 100644
--- a/core/java/android/content/OWNERS
+++ b/core/java/android/content/OWNERS
@@ -1,11 +1,11 @@
# Remain no owner because multiple modules may touch this file.
per-file Context.java = *
per-file ContextWrapper.java = *
-per-file *Content* = file:/services/core/java/com/android/server/am/OWNERS
-per-file *Sync* = file:/services/core/java/com/android/server/am/OWNERS
+per-file *Content* = varunshah@google.com, yamasani@google.com
+per-file *Sync* = file:/apex/jobscheduler/JOB_OWNERS
per-file IntentFilter.java = file:/PACKAGE_MANAGER_OWNERS
per-file UriRelativeFilter* = file:/PACKAGE_MANAGER_OWNERS
-per-file IntentFilter.java = file:/services/core/java/com/android/server/am/OWNERS
+per-file IntentFilter.java = file:/INTENT_OWNERS
per-file Intent.java = file:/INTENT_OWNERS
per-file AutofillOptions* = file:/core/java/android/service/autofill/OWNERS
per-file ContentCaptureOptions* = file:/core/java/android/service/contentcapture/OWNERS
diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig
index efddd1f..5b30624 100644
--- a/core/java/android/net/vcn/flags.aconfig
+++ b/core/java/android/net/vcn/flags.aconfig
@@ -14,11 +14,4 @@
namespace: "vcn"
description: "Feature flag for adjustable safe mode timeout"
bug: "317406085"
-}
-
-flag{
- name: "network_metric_monitor"
- namespace: "vcn"
- description: "Feature flag for enabling network metric monitor"
- bug: "282996138"
}
\ No newline at end of file
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index a698b9d..ddcb5c6 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -1031,7 +1031,10 @@
return this;
}
- private long getStatsDuration() {
+ /**
+ * Returns the duration of the battery session reflected by these stats.
+ */
+ public long getStatsDuration() {
if (mStatsDurationMs != -1) {
return mStatsDurationMs;
} else {
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index b533225..e68c4ca 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -20,6 +20,8 @@
import android.annotation.NonNull;
import android.util.IntArray;
+import com.android.internal.os.MonotonicClock;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -85,8 +87,11 @@
@NonNull
private final int[] mUserIds;
private final long mMaxStatsAgeMs;
- private final long mFromTimestamp;
- private final long mToTimestamp;
+
+ private final long mAggregatedFromTimestamp;
+ private final long mAggregatedToTimestamp;
+ private long mMonotonicStartTime;
+ private long mMonotonicEndTime;
private final double mMinConsumedPowerThreshold;
private final @BatteryConsumer.PowerComponentId int[] mPowerComponents;
@@ -96,8 +101,10 @@
: new int[]{UserHandle.USER_ALL};
mMaxStatsAgeMs = builder.mMaxStatsAgeMs;
mMinConsumedPowerThreshold = builder.mMinConsumedPowerThreshold;
- mFromTimestamp = builder.mFromTimestamp;
- mToTimestamp = builder.mToTimestamp;
+ mAggregatedFromTimestamp = builder.mAggregateFromTimestamp;
+ mAggregatedToTimestamp = builder.mAggregateToTimestamp;
+ mMonotonicStartTime = builder.mMonotonicStartTime;
+ mMonotonicEndTime = builder.mMonotonicEndTime;
mPowerComponents = builder.mPowerComponents;
}
@@ -163,11 +170,27 @@
}
/**
- * Returns the exclusive lower bound of the stored snapshot timestamps that should be included
- * in the aggregation. Ignored if {@link #getToTimestamp()} is zero.
+ * Returns the exclusive lower bound of the battery history that should be included in
+ * the aggregated battery usage stats.
*/
- public long getFromTimestamp() {
- return mFromTimestamp;
+ public long getMonotonicStartTime() {
+ return mMonotonicStartTime;
+ }
+
+ /**
+ * Returns the inclusive upper bound of the battery history that should be included in
+ * the aggregated battery usage stats.
+ */
+ public long getMonotonicEndTime() {
+ return mMonotonicEndTime;
+ }
+
+ /**
+ * Returns the exclusive lower bound of the stored snapshot timestamps that should be included
+ * in the aggregation. Ignored if {@link #getAggregatedToTimestamp()} is zero.
+ */
+ public long getAggregatedFromTimestamp() {
+ return mAggregatedFromTimestamp;
}
/**
@@ -175,8 +198,8 @@
* be included in the aggregation. The default is to include only the current stats
* accumulated since the latest battery reset.
*/
- public long getToTimestamp() {
- return mToTimestamp;
+ public long getAggregatedToTimestamp() {
+ return mAggregatedToTimestamp;
}
private BatteryUsageStatsQuery(Parcel in) {
@@ -185,8 +208,8 @@
in.readIntArray(mUserIds);
mMaxStatsAgeMs = in.readLong();
mMinConsumedPowerThreshold = in.readDouble();
- mFromTimestamp = in.readLong();
- mToTimestamp = in.readLong();
+ mAggregatedFromTimestamp = in.readLong();
+ mAggregatedToTimestamp = in.readLong();
mPowerComponents = in.createIntArray();
}
@@ -197,8 +220,8 @@
dest.writeIntArray(mUserIds);
dest.writeLong(mMaxStatsAgeMs);
dest.writeDouble(mMinConsumedPowerThreshold);
- dest.writeLong(mFromTimestamp);
- dest.writeLong(mToTimestamp);
+ dest.writeLong(mAggregatedFromTimestamp);
+ dest.writeLong(mAggregatedToTimestamp);
dest.writeIntArray(mPowerComponents);
}
@@ -228,8 +251,10 @@
private int mFlags;
private IntArray mUserIds;
private long mMaxStatsAgeMs = DEFAULT_MAX_STATS_AGE_MS;
- private long mFromTimestamp;
- private long mToTimestamp;
+ private long mMonotonicStartTime = MonotonicClock.UNDEFINED;
+ private long mMonotonicEndTime = MonotonicClock.UNDEFINED;
+ private long mAggregateFromTimestamp;
+ private long mAggregateToTimestamp;
private double mMinConsumedPowerThreshold = 0;
private @BatteryConsumer.PowerComponentId int[] mPowerComponents;
@@ -241,6 +266,17 @@
}
/**
+ * Specifies the time range for the requested stats, in terms of MonotonicClock
+ * @param monotonicStartTime Inclusive starting monotonic timestamp
+ * @param monotonicEndTime Exclusive ending timestamp. Can be MonotonicClock.UNDEFINED
+ */
+ public Builder monotonicTimeRange(long monotonicStartTime, long monotonicEndTime) {
+ mMonotonicStartTime = monotonicStartTime;
+ mMonotonicEndTime = monotonicEndTime;
+ return this;
+ }
+
+ /**
* Add a user whose battery stats should be included in the battery usage stats.
* {@link UserHandle#USER_ALL} will be used by default if no users are added explicitly.
*/
@@ -345,8 +381,8 @@
*/
// TODO(b/298459065): switch to monotonic clock
public Builder aggregateSnapshots(long fromTimestamp, long toTimestamp) {
- mFromTimestamp = fromTimestamp;
- mToTimestamp = toTimestamp;
+ mAggregateFromTimestamp = fromTimestamp;
+ mAggregateToTimestamp = toTimestamp;
return this;
}
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index b2d9260..6afb8e0 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -754,7 +754,7 @@
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
- && mNextPollTimeoutMillis != 0) {
+ && isIdle()) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index a1b75034..590ddb4 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -122,3 +122,6 @@
per-file StatsBootstrapAtomValue.aidl = file:/services/core/java/com/android/server/stats/OWNERS
per-file StatsBootstrapAtomService.java = file:/services/core/java/com/android/server/stats/OWNERS
per-file StatsServiceManager.java = file:/services/core/java/com/android/server/stats/OWNERS
+
+# Dropbox
+per-file DropBoxManager* = mwachens@google.com
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 80c24a9..02335972 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -712,7 +712,7 @@
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
- && mNextPollTimeoutMillis != 0) {
+ && isIdle()) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index 7f7ef04..f893739 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -109,9 +109,11 @@
* Returns the amount of time in milliseconds this UID spent in the specified process state.
*/
public long getTimeInProcessStateMs(@ProcessState int state) {
- Key key = getKey(POWER_COMPONENT_BASE, state);
- if (key != null) {
- return getUsageDurationMillis(key);
+ if (state != BatteryConsumer.PROCESS_STATE_UNSPECIFIED) {
+ Key key = getKey(POWER_COMPONENT_BASE, state);
+ if (key != null) {
+ return getUsageDurationMillis(key);
+ }
}
return 0;
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index acf4a2f..fa99f35 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5602,14 +5602,30 @@
android.Manifest.permission.MANAGE_USERS,
android.Manifest.permission.INTERACT_ACROSS_USERS
})
+ @CachedProperty(api = "user_manager_users")
public @Nullable UserHandle getProfileParent(@NonNull UserHandle user) {
- UserInfo info = getProfileParent(user.getIdentifier());
-
- if (info == null) {
- return null;
+ if (android.multiuser.Flags.cacheProfileParentReadOnly()) {
+ final UserHandle userHandle = UserManagerCache.getProfileParent(
+ (UserHandle query) -> {
+ UserInfo info = getProfileParent(query.getIdentifier());
+ // TODO: Remove when b/372923336 is fixed
+ if (info == null) {
+ return UserHandle.of(UserHandle.USER_NULL);
+ }
+ return UserHandle.of(info.id);
+ },
+ user);
+ if (userHandle.getIdentifier() == UserHandle.USER_NULL) {
+ return null;
+ }
+ return userHandle;
+ } else {
+ UserInfo info = getProfileParent(user.getIdentifier());
+ if (info == null) {
+ return null;
+ }
+ return UserHandle.of(info.id);
}
-
- return UserHandle.of(info.id);
}
/**
@@ -6424,6 +6440,9 @@
*/
public static final void invalidateCacheOnUserListChange() {
UserManagerCache.invalidateUserSerialNumber();
+ if (android.multiuser.Flags.cacheProfileParentReadOnly()) {
+ UserManagerCache.invalidateProfileParent();
+ }
}
/**
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index aedf8e0..1d35344 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -113,3 +113,10 @@
description: "AFL feature"
bug: "365994454"
}
+
+flag {
+ name: "keystore_grant_api"
+ namespace: "hardware_backed_security"
+ description: "Feature flag for exposing KeyStore grant APIs"
+ bug: "351158708"
+}
diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java
index 14d5800..5295b60 100644
--- a/core/java/android/telephony/TelephonyCallback.java
+++ b/core/java/android/telephony/TelephonyCallback.java
@@ -33,6 +33,7 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
import com.android.internal.telephony.IPhoneStateListener;
import com.android.internal.telephony.flags.Flags;
@@ -70,6 +71,7 @@
* its manifest file. Where permissions apply, they are noted in the
* appropriate sub-interfaces.
*/
+@WeaklyReferencedCallback
public class TelephonyCallback {
private static final String LOG_TAG = "TelephonyCallback";
/**
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index 18080e4..fc7a65d 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -24,6 +24,7 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
import dalvik.annotation.optimization.FastNative;
@@ -40,6 +41,7 @@
*
* @hide
*/
+@WeaklyReferencedCallback
public abstract class DisplayEventReceiver {
/**
diff --git a/core/java/android/view/LetterboxScrollProcessor.java b/core/java/android/view/LetterboxScrollProcessor.java
index dc736d6..1364a82 100644
--- a/core/java/android/view/LetterboxScrollProcessor.java
+++ b/core/java/android/view/LetterboxScrollProcessor.java
@@ -16,6 +16,8 @@
package android.view;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
@@ -23,6 +25,8 @@
import androidx.annotation.NonNull;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -35,6 +39,7 @@
*
* @hide
*/
+@VisibleForTesting(visibility = PACKAGE)
public class LetterboxScrollProcessor {
private enum LetterboxScrollState {
@@ -53,6 +58,7 @@
/** IDs of events generated from this class */
private final Set<Integer> mGeneratedEventIds = new HashSet<>();
+ @VisibleForTesting(visibility = PACKAGE)
public LetterboxScrollProcessor(@NonNull Context context, @Nullable Handler handler) {
mContext = context;
mScrollDetector = new GestureDetector(context, new ScrollListener(), handler);
@@ -69,7 +75,9 @@
* @return The list of adjusted events, or null if no adjustments are needed. The list is empty
* if the event should be ignored. Do not keep a reference to the output as the list is reused.
*/
- public List<MotionEvent> processMotionEvent(MotionEvent motionEvent) {
+ @Nullable
+ @VisibleForTesting(visibility = PACKAGE)
+ public List<MotionEvent> processMotionEvent(@NonNull MotionEvent motionEvent) {
mProcessedEvents.clear();
final Rect appBounds = getAppBounds();
@@ -124,11 +132,9 @@
mState = LetterboxScrollState.AWAITING_GESTURE_START;
}
- if (makeNoAdjustments) return null;
- return mProcessedEvents;
+ return makeNoAdjustments ? null : mProcessedEvents;
}
-
/**
* Processes the InputEvent for compatibility before it is finished by calling
* InputEventReceiver#finishInputEvent().
@@ -136,21 +142,33 @@
* @param motionEvent The MotionEvent to process.
* @return The motionEvent to finish, or null if it should not be finished.
*/
- public InputEvent processMotionEventBeforeFinish(MotionEvent motionEvent) {
- if (mGeneratedEventIds.remove(motionEvent.getId())) return null;
- return motionEvent;
+ @Nullable
+ @VisibleForTesting(visibility = PACKAGE)
+ public InputEvent processMotionEventBeforeFinish(@NonNull MotionEvent motionEvent) {
+ return mGeneratedEventIds.remove(motionEvent.getId()) ? null : motionEvent;
}
+ @NonNull
private Rect getAppBounds() {
return mContext.getResources().getConfiguration().windowConfiguration.getBounds();
}
- private boolean isOutsideAppBounds(MotionEvent motionEvent, Rect appBounds) {
- return motionEvent.getX() < 0 || motionEvent.getX() >= appBounds.width()
- || motionEvent.getY() < 0 || motionEvent.getY() >= appBounds.height();
+ /** Checks whether the gesture is located on the letterbox area. */
+ private boolean isOutsideAppBounds(@NonNull MotionEvent motionEvent, @NonNull Rect appBounds) {
+ // The events are in the coordinate system of the ViewRootImpl (window). The window might
+ // not have the same dimensions as the app bounds - for example in case of Dialogs - thus
+ // `getRawX()` and `getRawY()` are used, with the absolute bounds (left, top, etc) instead
+ // of width and height.
+ // The event should be passed to the app if it has happened anywhere in the app area,
+ // irrespective of the current window size, therefore the app bounds are used instead of the
+ // current window.
+ return motionEvent.getRawX() < appBounds.left
+ || motionEvent.getRawX() >= appBounds.right
+ || motionEvent.getRawY() < appBounds.top
+ || motionEvent.getRawY() >= appBounds.bottom;
}
- private void applyOffset(MotionEvent event, Rect appBounds) {
+ private void applyOffset(@NonNull MotionEvent event, @NonNull Rect appBounds) {
float horizontalOffset = calculateOffset(event.getX(), appBounds.width());
float verticalOffset = calculateOffset(event.getY(), appBounds.height());
// Apply the offset to the motion event so it is over the app's view.
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index 1ea58bc..80484a6 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -88,6 +88,7 @@
per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS
per-file ContentInfo.java = file:/core/java/android/service/autofill/OWNERS
per-file ContentInfo.java = file:/core/java/android/widget/OWNERS
+per-file view_flags.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
# WindowManager
per-file ContentRecordingSession.aidl = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 82235d2..9cad3e5 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.view.flags.Flags.FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE;
import static android.view.flags.Flags.FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER;
import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
@@ -27,6 +28,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.CompatibilityInfo.Translator;
@@ -2112,7 +2114,7 @@
}
/**
- * Display the view-hierarchy embedded within a {@link SurfaceControlViewHost.SurfacePackage}
+ * Displays the view-hierarchy embedded within a {@link SurfaceControlViewHost.SurfacePackage}
* within this SurfaceView.
*
* This can be called independently of the SurfaceView lifetime callbacks. SurfaceView
@@ -2132,6 +2134,8 @@
* SurfaceView the underlying {@link SurfaceControlViewHost} remains managed by it's original
* remote-owner.
*
+ * Users can call {@link SurfaceView#clearChildSurfacePackage} to clear the package.
+ *
* @param p The SurfacePackage to embed.
*/
public void setChildSurfacePackage(@NonNull SurfaceControlViewHost.SurfacePackage p) {
@@ -2155,6 +2159,46 @@
invalidate();
}
+ /**
+ * Returns the {@link SurfaceControlViewHost.SurfacePackage} that was set on this SurfaceView.
+ *
+ * Note: This method will return {@code null} if
+ * {@link #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)}
+ * has not been called or if {@link #clearChildSurfacePackage()} has been called.
+ *
+ * @see #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)
+ */
+ @SuppressLint("GetterSetterNullability")
+ @FlaggedApi(FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE)
+ public @Nullable SurfaceControlViewHost.SurfacePackage getChildSurfacePackage() {
+ return mSurfacePackage;
+ }
+
+ /**
+ * Clears the {@link SurfaceControlViewHost.SurfacePackage} that was set on this SurfaceView.
+ * This hides any content rendered by the provided
+ * {@link SurfaceControlViewHost.SurfacePackage}.
+ *
+ * @see #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)
+ */
+ @FlaggedApi(FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE)
+ public void clearChildSurfacePackage() {
+ if (mSurfacePackage != null) {
+ mSurfaceControlViewHostParent.detach();
+ mEmbeddedWindowParams.clear();
+
+ // Reparent the SurfaceControl to remove the content on screen.
+ final SurfaceControl sc = mSurfacePackage.getSurfaceControl();
+ final SurfaceControl.Transaction transaction = new Transaction();
+ transaction.reparent(sc, null);
+ mSurfacePackage.release();
+ applyTransactionOnVriDraw(transaction);
+
+ mSurfacePackage = null;
+ invalidate();
+ }
+ }
+
private void reparentSurfacePackage(SurfaceControl.Transaction t,
SurfaceControlViewHost.SurfacePackage p) {
final SurfaceControl sc = p.getSurfaceControl();
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index bb61ae4..1b86f96 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -119,6 +119,14 @@
}
flag {
+ name: "surface_view_get_surface_package"
+ namespace: "window_surfaces"
+ description: "Add APIs to manage SurfacePackage of the parent SurfaceView."
+ bug: "341021569"
+ is_fixed_read_only: true
+}
+
+flag {
name: "use_refactored_round_scrollbar"
namespace: "wear_frameworks"
description: "Use refactored round scrollbar."
diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java
index 59639d0..6cefc4d 100644
--- a/core/java/android/window/BackNavigationInfo.java
+++ b/core/java/android/window/BackNavigationInfo.java
@@ -16,6 +16,8 @@
package android.window;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
import android.annotation.AnimRes;
import android.annotation.ColorInt;
import android.annotation.IntDef;
@@ -118,6 +120,7 @@
private final Rect mTouchableRegion;
private final boolean mAppProgressGenerationAllowed;
+ private final int mFocusedTaskId;
/**
* Create a new {@link BackNavigationInfo} instance.
@@ -135,7 +138,8 @@
@Nullable CustomAnimationInfo customAnimationInfo,
int letterboxColor,
@Nullable Rect touchableRegion,
- boolean appProgressGenerationAllowed) {
+ boolean appProgressGenerationAllowed,
+ int focusedTaskId) {
mType = type;
mOnBackNavigationDone = onBackNavigationDone;
mOnBackInvokedCallback = onBackInvokedCallback;
@@ -145,6 +149,7 @@
mLetterboxColor = letterboxColor;
mTouchableRegion = new Rect(touchableRegion);
mAppProgressGenerationAllowed = appProgressGenerationAllowed;
+ mFocusedTaskId = focusedTaskId;
}
private BackNavigationInfo(@NonNull Parcel in) {
@@ -157,6 +162,7 @@
mLetterboxColor = in.readInt();
mTouchableRegion = in.readTypedObject(Rect.CREATOR);
mAppProgressGenerationAllowed = in.readBoolean();
+ mFocusedTaskId = in.readInt();
}
/** @hide */
@@ -171,6 +177,7 @@
dest.writeInt(mLetterboxColor);
dest.writeTypedObject(mTouchableRegion, flags);
dest.writeBoolean(mAppProgressGenerationAllowed);
+ dest.writeInt(mFocusedTaskId);
}
/**
@@ -238,6 +245,14 @@
}
/**
+ * @return The focused task id when back gesture start.
+ * @hide
+ */
+ public int getFocusedTaskId() {
+ return mFocusedTaskId;
+ }
+
+ /**
* Callback to be called when the back preview is finished in order to notify the server that
* it can clean up the resources created for the animation.
* @hide
@@ -435,6 +450,7 @@
private int mLetterboxColor = Color.TRANSPARENT;
private Rect mTouchableRegion;
private boolean mAppProgressGenerationAllowed;
+ private int mFocusedTaskId = INVALID_TASK_ID;
/**
* @see BackNavigationInfo#getType()
@@ -527,6 +543,14 @@
}
/**
+ * @param focusedTaskId The current focused taskId when back gesture start.
+ */
+ public Builder setFocusedTaskId(int focusedTaskId) {
+ mFocusedTaskId = focusedTaskId;
+ return this;
+ }
+
+ /**
* Builds and returns an instance of {@link BackNavigationInfo}
*/
public BackNavigationInfo build() {
@@ -537,7 +561,8 @@
mCustomAnimationInfo,
mLetterboxColor,
mTouchableRegion,
- mAppProgressGenerationAllowed);
+ mAppProgressGenerationAllowed,
+ mFocusedTaskId);
}
}
}
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index 9a7bce0..5397da1 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -151,7 +151,9 @@
@VisibleForTesting
public void setFrames(Rect frame, Rect systemBarInsets) {
mFrame.set(frame);
- mSizeMismatch = (mFrame.width() != mSnapshotW || mFrame.height() != mSnapshotH);
+ final Rect letterboxInsets = mSnapshot.getLetterboxInsets();
+ mSizeMismatch = (mFrame.width() != mSnapshotW || mFrame.height() != mSnapshotH)
+ || letterboxInsets.left != 0 || letterboxInsets.top != 0;
if (!Flags.drawSnapshotAspectRatioMatch() && systemBarInsets != null) {
mSystemBarInsets.set(systemBarInsets);
mSystemBarBackgroundPainter.setInsets(systemBarInsets);
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 0d235ff..d15f52c 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -9,6 +9,16 @@
}
flag {
+ name: "reset_draw_state_on_client_invisible"
+ namespace: "windowing_frontend"
+ description: "Reset draw state if the client is notified to be invisible"
+ bug: "373023636"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "wait_for_transition_on_display_switch"
namespace: "windowing_frontend"
description: "Waits for Shell transition to start before unblocking the screen after display switch"
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index d474c6d..6448f10 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -30,6 +30,7 @@
import static com.android.internal.jank.InteractionJankMonitor.ACTION_SESSION_END;
import static com.android.internal.jank.InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT;
+import android.animation.AnimationHandler;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -344,7 +345,8 @@
@UiThread
public boolean end(@Reasons int reason) {
if (mCancelled || mEndVsyncId != INVALID_ID) return false;
- mEndVsyncId = mChoreographer.getVsyncId();
+ mEndVsyncId = AnimationHandler.getInstance().getLastAnimationFrameVsyncId(
+ mChoreographer.getVsyncId());
// Cancel the session if:
// 1. The session begins and ends at the same vsync id.
// 2. The session never begun.
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index d61785e..07aa720 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -83,7 +83,7 @@
private static final String TAG = "BatteryStatsHistory";
// Current on-disk Parcel version. Must be updated when the format of the parcelable changes
- private static final int VERSION = 210;
+ private static final int VERSION = 211;
private static final String HISTORY_DIR = "battery-history";
private static final String FILE_SUFFIX = ".bh";
@@ -210,6 +210,8 @@
private final MonotonicClock mMonotonicClock;
// Monotonic time when we started writing to the history buffer
private long mHistoryBufferStartTime;
+ // Monotonically increasing size of written history
+ private long mMonotonicHistorySize;
private final ArraySet<PowerStats.Descriptor> mWrittenPowerStatsDescriptors = new ArraySet<>();
private byte mLastHistoryStepLevel = 0;
private boolean mMutable = true;
@@ -909,6 +911,8 @@
}
// skip monotonic time field.
p.readLong();
+ // skip monotonic size field
+ p.readLong();
final int bufSize = p.readInt();
final int curPos = p.dataPosition();
@@ -964,6 +968,8 @@
}
// skip monotonic time field.
out.readLong();
+ // skip monotonic size field
+ out.readLong();
return true;
}
@@ -987,6 +993,7 @@
p.setDataPosition(0);
p.readInt(); // Skip the version field
long monotonicTime = p.readLong();
+ p.readLong(); // Skip monotonic size field
p.setDataPosition(pos);
return monotonicTime;
}
@@ -1819,6 +1826,7 @@
// as long as no bit has changed both between now and the last entry, as
// well as the last entry and the one before it (so we capture any toggles).
if (DEBUG) Slog.i(TAG, "ADD: rewinding back to " + mHistoryBufferLastPos);
+ mMonotonicHistorySize -= (mHistoryBuffer.dataSize() - mHistoryBufferLastPos);
mHistoryBuffer.setDataSize(mHistoryBufferLastPos);
mHistoryBuffer.setDataPosition(mHistoryBufferLastPos);
mHistoryBufferLastPos = -1;
@@ -1934,6 +1942,7 @@
}
mHistoryLastWritten.tagsFirstOccurrence = hasTags;
writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten);
+ mMonotonicHistorySize += (mHistoryBuffer.dataSize() - mHistoryBufferLastPos);
cur.wakelockTag = null;
cur.wakeReasonTag = null;
cur.eventCode = HistoryItem.EVENT_NONE;
@@ -2344,6 +2353,8 @@
}
mHistoryBufferStartTime = in.readLong();
+ mMonotonicHistorySize = in.readLong();
+
mHistoryBuffer.setDataSize(0);
mHistoryBuffer.setDataPosition(0);
@@ -2370,6 +2381,7 @@
private void writeHistoryBuffer(Parcel out) {
out.writeInt(BatteryStatsHistory.VERSION);
out.writeLong(mHistoryBufferStartTime);
+ out.writeLong(mMonotonicHistorySize);
out.writeInt(mHistoryBuffer.dataSize());
if (DEBUG) {
Slog.i(TAG, "***************** WRITING HISTORY: "
@@ -2457,6 +2469,14 @@
}
/**
+ * Returns the monotonically increasing size of written history, including the buffers
+ * that have already been discarded.
+ */
+ public long getMonotonicHistorySize() {
+ return mMonotonicHistorySize;
+ }
+
+ /**
* Prints battery stats history for debugging.
*/
public void dump(PrintWriter pw, long startTimeMs, long endTimeMs) {
diff --git a/core/proto/android/app/OWNERS b/core/proto/android/app/OWNERS
index a137ea9..519bf9a 100644
--- a/core/proto/android/app/OWNERS
+++ b/core/proto/android/app/OWNERS
@@ -1,3 +1,3 @@
-per-file appstartinfo.proto = file:/services/core/java/com/android/server/am/OWNERS
+per-file appstartinfo.proto = file:/PERFORMANCE_OWNERS
per-file location_time_zone_manager.proto = file:platform/frameworks/base:/services/core/java/com/android/server/timezonedetector/OWNERS
per-file time_zone_detector.proto = file:platform/frameworks/base:/services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/core/res/OWNERS b/core/res/OWNERS
index d109cee..faed4d8 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -53,7 +53,7 @@
per-file res/values/dimens_car.xml = file:/platform/packages/services/Car:/OWNERS
# Device Idle
-per-file res/values/config_device_idle.xml = file:/apex/jobscheduler/OWNERS
+per-file res/values/config_device_idle.xml = file:/apex/jobscheduler/DEVICE_IDLE_OWNERS
# Display Manager
per-file res/values/config_display.xml = file:/services/core/java/com/android/server/display/OWNERS
diff --git a/core/res/res/values/config_battery_stats.xml b/core/res/res/values/config_battery_stats.xml
index 80cf088..9498273 100644
--- a/core/res/res/values/config_battery_stats.xml
+++ b/core/res/res/values/config_battery_stats.xml
@@ -45,7 +45,12 @@
<integer name="config_powerStatsAggregationPeriod">14400000</integer>
<!-- PowerStats aggregation span duration in milliseconds. This is the length of battery
- history time for every aggregated power stats span that is stored stored in PowerStatsStore.
+ history time for every aggregated power stats span that is stored in PowerStatsStore.
It should not be larger than config_powerStatsAggregationPeriod (but it can be the same) -->
<integer name="config_aggregatedPowerStatsSpanDuration">3600000</integer>
+
+ <!-- BatteryUsageStats accumulation period as determined by the size of accumulated
+ battery history, in bytes. -->
+ <integer name="config_accumulatedBatteryUsageStatsSpanSize">32768</integer>
+
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b424955..0b2b345 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5301,6 +5301,7 @@
<java-symbol type="string" name="config_powerStatsThrottlePeriods" />
<java-symbol type="integer" name="config_powerStatsAggregationPeriod" />
<java-symbol type="integer" name="config_aggregatedPowerStatsSpanDuration" />
+ <java-symbol type="integer" name="config_accumulatedBatteryUsageStatsSpanSize" />
<java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/>
<java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/>
diff --git a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
index eb463fd..0469846 100644
--- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
+++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java
@@ -18,6 +18,8 @@
import static android.test.MoreAsserts.assertNotEqual;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
@@ -892,6 +894,34 @@
}
@Test
+ public void testPostNotifyEndListener() throws Throwable {
+ ValueAnimator.setPostNotifyEndListenerEnabled(true);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final long[] lastAnimFrameId = new long[1];
+ final long[] endAnimCallbackId = new long[1];
+ try {
+ a1.addUpdateListener(animator -> {
+ if (animator.getAnimatedFraction() == 1f) {
+ lastAnimFrameId[0] = Choreographer.getInstance().getVsyncId();
+ }
+ });
+ a1.addListener(new MyListener() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ endAnimCallbackId[0] = Choreographer.getInstance().getVsyncId();
+ latch.countDown();
+ }
+ });
+ mActivityRule.runOnUiThread(() -> a1.start());
+ assertTrue(latch.await(1, TimeUnit.SECONDS));
+ assertThat(endAnimCallbackId[0]).isGreaterThan(lastAnimFrameId[0]);
+ } finally {
+ ValueAnimator.setPostNotifyEndListenerEnabled(false);
+ }
+ }
+
+ @Test
public void testZeroDuration() throws Throwable {
// Run two animators with zero duration, with one running forward and the other one
// backward. Check that the animations start and finish with the correct end fractions.
diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
index b5ee130..dcea5b2 100644
--- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
+++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java
@@ -19,6 +19,8 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
@@ -26,6 +28,7 @@
import androidx.test.filters.SmallTest;
import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -84,14 +87,20 @@
public Boolean apply(Integer x) {
return mServer.query(x);
}
+
@Override
public boolean shouldBypassCache(Integer x) {
return x % 13 == 0;
}
}
- // Clear the test mode after every test, in case this process is used for other
- // tests. This also resets the test property map.
+ // Prepare for testing.
+ @Before
+ public void setUp() throws Exception {
+ PropertyInvalidatedCache.setTestMode(true);
+ }
+
+ // Ensure all test configurations are cleared.
@After
public void tearDown() throws Exception {
PropertyInvalidatedCache.setTestMode(false);
@@ -111,9 +120,6 @@
new PropertyInvalidatedCache<>(4, MODULE, API, "cache1",
new ServerQuery(tester));
- PropertyInvalidatedCache.setTestMode(true);
- testCache.testPropertyName();
-
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -223,22 +229,16 @@
TestCache(String module, String api) {
this(module, api, new TestQuery());
- setTestMode(true);
- testPropertyName();
}
TestCache(String module, String api, TestQuery query) {
super(4, module, api, api, query);
mQuery = query;
- setTestMode(true);
- testPropertyName();
}
public int getRecomputeCount() {
return mQuery.getRecomputeCount();
}
-
-
}
@Test
@@ -375,4 +375,52 @@
PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState");
assertEquals(n1, "cache_key.bluetooth.get_state");
}
+
+ // Verify that test mode works properly.
+ @Test
+ public void testTestMode() {
+ // Create a cache that will write a system nonce.
+ TestCache sysCache = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode1");
+ try {
+ // Invalidate the cache, which writes the system property. There must be a permission
+ // failure.
+ sysCache.invalidateCache();
+ fail("expected permission failure");
+ } catch (RuntimeException e) {
+ // The expected exception is a bare RuntimeException. The test does not attempt to
+ // validate the text of the exception message.
+ }
+
+ sysCache.testPropertyName();
+ // Invalidate the cache. This must succeed because the property has been marked for
+ // testing.
+ sysCache.invalidateCache();
+
+ // Create a cache that uses MODULE_TEST. Invalidation succeeds whether or not the
+ // property is tagged as being tested.
+ TestCache testCache = new TestCache(PropertyInvalidatedCache.MODULE_TEST, "mode2");
+ testCache.invalidateCache();
+ testCache.testPropertyName();
+ testCache.invalidateCache();
+
+ // Clear test mode. This fails if test mode is not enabled.
+ PropertyInvalidatedCache.setTestMode(false);
+ try {
+ PropertyInvalidatedCache.setTestMode(false);
+ fail("expected an IllegalStateException");
+ } catch (IllegalStateException e) {
+ // The expected exception.
+ }
+ // Configuring a property for testing must fail if test mode is false.
+ TestCache cache2 = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode3");
+ try {
+ cache2.testPropertyName();
+ fail("expected an IllegalStateException");
+ } catch (IllegalStateException e) {
+ // The expected exception.
+ }
+
+ // Re-enable test mode (so that the cleanup for the test does not throw).
+ PropertyInvalidatedCache.setTestMode(true);
+ }
}
diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
index 64f77b3..5c56fdc 100644
--- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java
+++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
@@ -17,6 +17,7 @@
package android.os;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
import android.multiuser.Flags;
import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -26,6 +27,7 @@
import androidx.test.filters.SmallTest;
import org.junit.After;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -92,14 +94,20 @@
public Boolean apply(Integer x) {
return mServer.query(x);
}
+
@Override
public boolean shouldBypassCache(Integer x) {
return x % 13 == 0;
}
}
- // Clear the test mode after every test, in case this process is used for other
- // tests. This also resets the test property map.
+ // Prepare for testing.
+ @Before
+ public void setUp() throws Exception {
+ IpcDataCache.setTestMode(true);
+ }
+
+ // Ensure all test configurations are cleared.
@After
public void tearDown() throws Exception {
IpcDataCache.setTestMode(false);
@@ -119,9 +127,6 @@
new IpcDataCache<>(4, MODULE, API, "testCache1",
new ServerQuery(tester));
- IpcDataCache.setTestMode(true);
- testCache.testPropertyName();
-
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -165,9 +170,6 @@
IpcDataCache<Integer, Boolean> testCache =
new IpcDataCache<>(config, (x) -> tester.query(x, x % 10 == 9));
- IpcDataCache.setTestMode(true);
- testCache.testPropertyName();
-
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -205,9 +207,6 @@
IpcDataCache<Integer, Boolean> testCache =
new IpcDataCache<>(config, (x) -> tester.query(x), (x) -> x % 9 == 0);
- IpcDataCache.setTestMode(true);
- testCache.testPropertyName();
-
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
@@ -313,8 +312,6 @@
TestCache(String module, String api, TestQuery query) {
super(4, module, api, "testCache7", query);
mQuery = query;
- setTestMode(true);
- testPropertyName();
}
TestCache(IpcDataCache.Config c) {
@@ -324,8 +321,6 @@
TestCache(IpcDataCache.Config c, TestQuery query) {
super(c, query);
mQuery = query;
- setTestMode(true);
- testPropertyName();
}
int getRecomputeCount() {
@@ -456,4 +451,52 @@
TestCache ec = new TestCache(e);
assertEquals(ec.isDisabled(), true);
}
+
+ // Verify that test mode works properly.
+ @Test
+ public void testTestMode() {
+ // Create a cache that will write a system nonce.
+ TestCache sysCache = new TestCache(IpcDataCache.MODULE_SYSTEM, "mode1");
+ try {
+ // Invalidate the cache, which writes the system property. There must be a permission
+ // failure.
+ sysCache.invalidateCache();
+ fail("expected permission failure");
+ } catch (RuntimeException e) {
+ // The expected exception is a bare RuntimeException. The test does not attempt to
+ // validate the text of the exception message.
+ }
+
+ sysCache.testPropertyName();
+ // Invalidate the cache. This must succeed because the property has been marked for
+ // testing.
+ sysCache.invalidateCache();
+
+ // Create a cache that uses MODULE_TEST. Invalidation succeeds whether or not the
+ // property is tagged as being tested.
+ TestCache testCache = new TestCache(IpcDataCache.MODULE_TEST, "mode2");
+ testCache.invalidateCache();
+ testCache.testPropertyName();
+ testCache.invalidateCache();
+
+ // Clear test mode. This fails if test mode is not enabled.
+ IpcDataCache.setTestMode(false);
+ try {
+ IpcDataCache.setTestMode(false);
+ fail("expected an IllegalStateException");
+ } catch (IllegalStateException e) {
+ // The expected exception.
+ }
+ // Configuring a property for testing must fail if test mode is false.
+ TestCache cache2 = new TestCache(IpcDataCache.MODULE_SYSTEM, "mode3");
+ try {
+ cache2.testPropertyName();
+ fail("expected an IllegalStateException");
+ } catch (IllegalStateException e) {
+ // The expected exception.
+ }
+
+ // Re-enable test mode (so that the cleanup for the test does not throw).
+ IpcDataCache.setTestMode(true);
+ }
}
diff --git a/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java b/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java
index f8ec9f4..235625d 100644
--- a/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java
+++ b/core/tests/coretests/src/android/view/LetterboxScrollProcessorTest.java
@@ -31,13 +31,13 @@
import android.os.Looper;
import android.platform.test.annotations.Presubmit;
+import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
-
import java.util.ArrayList;
import java.util.List;
@@ -54,30 +54,32 @@
private LetterboxScrollProcessor mLetterboxScrollProcessor;
private Context mContext;
- // Constant delta used when comparing coordinates (floats)
+ // Constant delta used when comparing coordinates (floats).
private static final float EPSILON = 0.1f;
+ private static final Rect APP_BOUNDS =
+ new Rect(/* left= */ 200, /* top= */ 200, /* right= */ 600, /* bottom= */ 1000);
+
@Before
public void setUp() {
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Set app bounds as if it was letterboxed.
- mContext.getResources().getConfiguration().windowConfiguration
- .setBounds(new Rect(200, 200, 600, 1000));
-
- Handler handler = new Handler(Looper.getMainLooper());
+ mContext.getResources().getConfiguration().windowConfiguration.setBounds(APP_BOUNDS);
// Recreate to reset LetterboxScrollProcessor state.
- mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext, handler);
+ mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext,
+ new Handler(Looper.getMainLooper()));
}
@Test
public void testGestureInBoundsHasNoAdjustments() {
// Tap-like gesture in bounds (non-scroll).
- List<MotionEvent> tapGestureEvents = createTapGestureEvents(0f, 0f);
+ final List<MotionEvent> tapGestureEvents = createTapGestureEvents(
+ /* startX= */ 0f, /* startY= */ 0f);
// Get processed events from Letterbox Scroll Processor.
- List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
+ final List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
// Ensure no changes are made to events after processing - event locations should not be
// adjusted because the gesture started in the app's bounds (for all gestures).
@@ -87,13 +89,32 @@
}
@Test
- public void testGestureOutsideBoundsIsIgnored() {
- // Tap-like gesture outside bounds (non-scroll).
- List<MotionEvent> tapGestureEvents = createTapGestureEvents(-100f, -100f);
+ public void testGestureInAppBoundsButOutsideTopWindowAlsoForwardedToTheApp() {
+ final Rect dialogBounds =
+ new Rect(/* left= */ 300, /* top= */ 500, /* right= */ 500, /* bottom= */ 700);
+ // Tap-like gesture outside the dialog, but in app bounds.
+ List<MotionEvent> tapGestureEvents = createTapGestureEventsWithCoordinateSystem(0f, 0f,
+ dialogBounds);
// Get processed events from Letterbox Scroll Processor.
List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
+ // Ensure no changes are made to events after processing - the event should be forwarded as
+ // normal.
+ assertEventLocationsAreNotAdjusted(tapGestureEvents, processedEvents);
+ // Ensure all of these events should be finished (expect no generated events).
+ assertMotionEventsShouldBeFinished(processedEvents);
+ }
+
+ @Test
+ public void testGestureOutsideBoundsIsIgnored() {
+ // Tap-like gesture outside bounds (non-scroll).
+ final List<MotionEvent> tapGestureEvents = createTapGestureEvents(
+ /* startX= */ -100f, /* startY= */ -100f);
+
+ // Get processed events from Letterbox Scroll Processor.
+ final List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents);
+
// All events should be ignored since it was a non-scroll gesture and out of bounds.
assertEquals(0, processedEvents.size());
}
@@ -101,10 +122,11 @@
@Test
public void testScrollGestureInBoundsHasNoAdjustments() {
// Scroll gesture in bounds (non-scroll).
- List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(0f, 0f);
+ final List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+ /* startX= */ 0f, /* startY= */ 0f);
// Get processed events from Letterbox Scroll Processor.
- List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
+ final List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
// Ensure no changes are made to events after processing - event locations should not be
// adjusted because the gesture started in the app's bounds (for all gestures).
@@ -116,10 +138,11 @@
@Test
public void testScrollGestureInBoundsThenLeavesBoundsHasNoAdjustments() {
// Scroll gesture in bounds (non-scroll) that moves out of bounds.
- List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(390f, 790f);
+ final List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+ /* startX= */ 390f, /* startY= */ 790f);
// Get processed events from Letterbox Scroll Processor.
- List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
+ final List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
// Ensure no changes are made to events after processing - event locations should not be
// adjusted because the gesture started in the app's bounds (for all gestures), even if it
@@ -132,7 +155,8 @@
@Test
public void testScrollGestureOutsideBoundsIsStartedInBounds() {
// Scroll gesture outside bounds.
- List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(-100f, 0f);
+ List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+ /* startX= */ -100f, /* startY= */ 0f);
// Get processed events from Letterbox Scroll Processor.
List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
@@ -142,9 +166,9 @@
// Ensure offset ACTION_DOWN is first event received.
MotionEvent firstProcessedEvent = processedEvents.getFirst();
- assertEquals(firstProcessedEvent.getAction(), ACTION_DOWN);
- assertEquals(firstProcessedEvent.getX(), 0, EPSILON);
- assertEquals(firstProcessedEvent.getY(), 0, EPSILON);
+ assertEquals(ACTION_DOWN, firstProcessedEvent.getAction());
+ assertEquals(0, firstProcessedEvent.getX(), EPSILON);
+ assertEquals(0, firstProcessedEvent.getY(), EPSILON);
// Ensure this event is not finished (because it was generated by LetterboxScrollProcessor).
assertNull(mLetterboxScrollProcessor.processMotionEventBeforeFinish(firstProcessedEvent));
}
@@ -152,16 +176,17 @@
@Test
public void testScrollGestureOutsideBoundsIsMovedInBounds() {
// Scroll gesture outside bounds.
- List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(-100f, 0f);
+ final List<MotionEvent> scrollGestureEvents = createScrollGestureEvents(
+ /* startX= */ -100f, /* startY= */ 0f);
// Get processed events from Letterbox Scroll Processor.
- List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
+ final List<MotionEvent> processedEvents = processMotionEvents(scrollGestureEvents);
// When a scroll occurs outside bounds: once detected as a scroll, an offset ACTION_DOWN is
// placed and then the rest of the gesture is offset also. Some ACTION_MOVE events may be
// ignored until the gesture is 'detected as a scroll'.
// For this test, we expect the first ACTION_MOVE event to be ignored:
- scrollGestureEvents.remove(1);
+ scrollGestureEvents.remove(/* index= */ 1);
// Ensure all processed events (that are not ignored) are offset over the app.
assertXCoordinatesAdjustedToZero(scrollGestureEvents, processedEvents);
@@ -170,8 +195,9 @@
assertMotionEventsShouldBeFinished(processedEvents.subList(1, processedEvents.size()));
}
- private List<MotionEvent> processMotionEvents(List<MotionEvent> motionEvents) {
- List<MotionEvent> processedEvents = new ArrayList<>();
+ @NonNull
+ private List<MotionEvent> processMotionEvents(@NonNull List<MotionEvent> motionEvents) {
+ final List<MotionEvent> processedEvents = new ArrayList<>();
for (MotionEvent motionEvent : motionEvents) {
MotionEvent clonedEvent = MotionEvent.obtain(motionEvent);
List<MotionEvent> letterboxScrollCompatEvents =
@@ -187,39 +213,76 @@
return processedEvents;
}
+ /**
+ * Creates and returns a tap gesture with X and Y in reference to the app bounds (top left
+ * corner is x=0, y=0).
+ */
+ @NonNull
private List<MotionEvent> createTapGestureEvents(float startX, float startY) {
+ return createTapGestureEventsWithCoordinateSystem(startX, startY, APP_BOUNDS);
+ }
+
+ /**
+ * @param referenceWindowBounds the amount the event will be translated by.
+ */
+ private List<MotionEvent> createTapGestureEventsWithCoordinateSystem(float startX, float startY,
+ @NonNull Rect referenceWindowBounds) {
// Events for tap-like gesture (non-scroll)
List<MotionEvent> motionEvents = new ArrayList<>();
- motionEvents.add(createBasicMotionEvent(0, ACTION_DOWN, startX, startY));
- motionEvents.add(createBasicMotionEvent(10, ACTION_UP, startX , startY));
+ motionEvents.add(createBasicMotionEventWithCoordinateSystem(0, ACTION_DOWN,
+ startX, startY, referenceWindowBounds));
+ motionEvents.add(createBasicMotionEventWithCoordinateSystem(10, ACTION_UP,
+ startX , startY, referenceWindowBounds));
return motionEvents;
}
+ @NonNull
private List<MotionEvent> createScrollGestureEvents(float startX, float startY) {
- float touchSlop = (float) ViewConfiguration.get(mContext).getScaledTouchSlop();
+ final float touchSlop = (float) ViewConfiguration.get(mContext).getScaledTouchSlop();
- // Events for scroll gesture (starts at (startX, startY) then moves down-right
- List<MotionEvent> motionEvents = new ArrayList<>();
- motionEvents.add(createBasicMotionEvent(0, ACTION_DOWN, startX, startY));
- motionEvents.add(createBasicMotionEvent(10, ACTION_MOVE,
+ // Events for scroll gesture (starts at (startX, startY) then moves down-right.
+ final List<MotionEvent> motionEvents = new ArrayList<>();
+ motionEvents.add(createBasicMotionEvent(/* downTime= */ 0, ACTION_DOWN, startX, startY));
+ motionEvents.add(createBasicMotionEvent(/* downTime= */ 10, ACTION_MOVE,
startX + touchSlop / 2, startY + touchSlop / 2));
- // Below event is first event in the scroll gesture where distance > touchSlop
- motionEvents.add(createBasicMotionEvent(20, ACTION_MOVE,
+ // Below event is first event in the scroll gesture where distance > touchSlop.
+ motionEvents.add(createBasicMotionEvent(/* downTime= */ 20, ACTION_MOVE,
startX + touchSlop * 2, startY + touchSlop * 2));
- motionEvents.add(createBasicMotionEvent(30, ACTION_MOVE,
+ motionEvents.add(createBasicMotionEvent(/* downTime= */ 30, ACTION_MOVE,
startX + touchSlop * 3, startY + touchSlop * 3));
- motionEvents.add(createBasicMotionEvent(40, ACTION_UP,
+ motionEvents.add(createBasicMotionEvent(/* downTime= */ 40, ACTION_UP,
startX + touchSlop * 3, startY + touchSlop * 3));
return motionEvents;
}
- private MotionEvent createBasicMotionEvent(int downTime, int action, float x, float y) {
- return MotionEvent.obtain(0, downTime, action, x, y, 0);
+ /**
+ * Creates and returns an event with X and Y in reference to the app bounds (top left corner is
+ * x=0, y=0).
+ */
+ @NonNull
+ private MotionEvent createBasicMotionEvent(int eventTime, int action, float x, float y) {
+ return createBasicMotionEventWithCoordinateSystem(eventTime, action, x, y, APP_BOUNDS);
+ }
+
+ /**
+ * @param referenceWindowBounds the amount the event will be translated by.
+ */
+ @NonNull
+ private MotionEvent createBasicMotionEventWithCoordinateSystem(int eventTime, int action,
+ float x, float y, @NonNull Rect referenceWindowBounds) {
+ final float rawX = referenceWindowBounds.left + x;
+ final float rawY = referenceWindowBounds.top + y;
+ // RawX and RawY cannot be changed once the event is created. Therefore, pass rawX and rawY
+ // according to the app's bounds on the display, and then offset to make X and Y relative to
+ // the app's bounds.
+ final MotionEvent event = MotionEvent.obtain(0, eventTime, action, rawX, rawY, 0);
+ event.offsetLocation(-referenceWindowBounds.left, -referenceWindowBounds.top);
+ return event;
}
private void assertEventLocationsAreNotAdjusted(
- List<MotionEvent> originalEvents,
- List<MotionEvent> processedEvents) {
+ @NonNull List<MotionEvent> originalEvents,
+ @NonNull List<MotionEvent> processedEvents) {
assertEquals("MotionEvent arrays are not the same size",
originalEvents.size(), processedEvents.size());
@@ -232,8 +295,8 @@
}
private void assertXCoordinatesAdjustedToZero(
- List<MotionEvent> originalEvents,
- List<MotionEvent> processedEvents) {
+ @NonNull List<MotionEvent> originalEvents,
+ @NonNull List<MotionEvent> processedEvents) {
assertEquals("MotionEvent arrays are not the same size",
originalEvents.size(), processedEvents.size());
@@ -245,7 +308,7 @@
}
}
- private void assertMotionEventsShouldBeFinished(List<MotionEvent> processedEvents) {
+ private void assertMotionEventsShouldBeFinished(@NonNull List<MotionEvent> processedEvents) {
for (MotionEvent processedEvent : processedEvents) {
assertNotNull(mLetterboxScrollProcessor.processMotionEventBeforeFinish(processedEvent));
}
diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
index c3a5b19c94..1cbc7d6 100644
--- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java
@@ -40,7 +40,10 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.animation.AnimationHandler;
+import android.os.ConditionVariable;
import android.os.Handler;
+import android.view.Choreographer;
import android.view.FrameMetrics;
import android.view.SurfaceControl;
import android.view.SurfaceControl.JankData;
@@ -576,6 +579,44 @@
}
@Test
+ public void testEndAnimationWithLastFrameSyncId() {
+ final long[] expectedLastAnimationFrameVsyncId = { 0 };
+ final long[] lastAnimationFrameVsyncId = { 0 };
+ final long[] endAnimationVsyncId = { 0 };
+ final ConditionVariable condition = new ConditionVariable();
+ final FrameTracker tracker = spyFrameTracker(/* surfaceOnly= */ false);
+ mActivity.runOnUiThread(() -> {
+ final AnimationHandler animationHandler = AnimationHandler.getInstance();
+ final Choreographer realChoreographer = Choreographer.getInstance();
+ // 3 v-sync ids:
+ // 1. Begin mocked vsyncId = 0
+ // 2. Current real vsyncId = last animation frame
+ // 3. Posted real vsyncId = end animation
+ when(mChoreographer.getVsyncId()).thenReturn(0L);
+ tracker.begin();
+ mRunnableArgumentCaptor.getValue().run();
+ when(mChoreographer.getVsyncId()).thenAnswer(a -> realChoreographer.getVsyncId());
+ expectedLastAnimationFrameVsyncId[0] = realChoreographer.getVsyncId();
+ // Simulate ending multiple animators. Their callbacks should run in a batch.
+ animationHandler.postEndAnimationCallback(() -> {
+ endAnimationVsyncId[0] = realChoreographer.getVsyncId();
+ lastAnimationFrameVsyncId[0] =
+ animationHandler.getLastAnimationFrameVsyncId(endAnimationVsyncId[0]);
+ });
+ animationHandler.postEndAnimationCallback(() -> {
+ tracker.end(FrameTracker.REASON_END_NORMAL);
+ condition.open();
+ });
+ });
+
+ condition.block(1000L /* timeoutMs */);
+ assertThat(lastAnimationFrameVsyncId[0]).isEqualTo(expectedLastAnimationFrameVsyncId[0]);
+ assertThat(endAnimationVsyncId[0]).isGreaterThan(lastAnimationFrameVsyncId[0]);
+ // Verifies that FrameTracker#mEndVsyncId uses the vsyncId from AnimationHandler.
+ verify(mJankStatsRegistration).removeAfter(eq(lastAnimationFrameVsyncId[0]));
+ }
+
+ @Test
public void testMaxSuccessiveMissedFramesCount() {
FrameTracker tracker = spyFrameTracker(/* surfaceOnly= */ true);
when(mChoreographer.getVsyncId()).thenReturn(100L);
diff --git a/core/xsd/vts/Android.bp b/core/xsd/vts/Android.bp
index 5d8407f..239eed0 100644
--- a/core/xsd/vts/Android.bp
+++ b/core/xsd/vts/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_android_kernel",
// 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"
diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java
index ed17fde..792e248 100644
--- a/graphics/java/android/graphics/text/PositionedGlyphs.java
+++ b/graphics/java/android/graphics/text/PositionedGlyphs.java
@@ -24,6 +24,7 @@
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.fonts.Font;
+import android.os.Build;
import com.android.internal.util.Preconditions;
import com.android.text.flags.Flags;
@@ -53,6 +54,8 @@
Typeface.class.getClassLoader(), nReleaseFunc());
}
+ private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric");
+
private final long mLayoutPtr;
private final float mXOffset;
private final float mYOffset;
@@ -252,7 +255,7 @@
mXOffset = xOffset;
mYOffset = yOffset;
- if (Flags.typefaceRedesign()) {
+ if (!sIsRobolectric && Flags.typefaceRedesign()) {
int fontCount = nGetFontCount(layoutPtr);
mFonts = new ArrayList<>(fontCount);
for (int i = 0; i < fontCount; ++i) {
diff --git a/keystore/java/android/security/OWNERS b/keystore/java/android/security/OWNERS
index ed30587..32759b2 100644
--- a/keystore/java/android/security/OWNERS
+++ b/keystore/java/android/security/OWNERS
@@ -1 +1,2 @@
per-file *.java,*.aidl = eranm@google.com,pgrafov@google.com,rubinxu@google.com
+per-file KeyStoreManager.java = mpgroover@google.com
diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java
new file mode 100644
index 0000000..197aaba
--- /dev/null
+++ b/keystore/java/android/security/keystore/KeyStoreManager.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.security.KeyStore2;
+import android.security.KeyStoreException;
+import android.security.keystore2.AndroidKeyStoreProvider;
+import android.security.keystore2.AndroidKeyStorePublicKey;
+import android.system.keystore2.Domain;
+import android.system.keystore2.KeyDescriptor;
+import android.system.keystore2.KeyPermission;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.ByteArrayInputStream;
+import java.security.Key;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class provides methods for interacting with keys stored within the
+ * <a href="/privacy-and-security/keystore">Android Keystore</a>.
+ */
+@FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API)
+@SystemService(Context.KEYSTORE_SERVICE)
+public class KeyStoreManager {
+ private static final String TAG = "KeyStoreManager";
+
+ private static final Object sInstanceLock = new Object();
+ @GuardedBy("sInstanceLock")
+ private static KeyStoreManager sInstance;
+
+ private final KeyStore2 mKeyStore2;
+
+ /**
+ * Private constructor to ensure only a single instance is created.
+ */
+ private KeyStoreManager() {
+ mKeyStore2 = KeyStore2.getInstance();
+ }
+
+ /**
+ * Returns the single instance of the {@code KeyStoreManager}.
+ *
+ * @hide
+ */
+ public static KeyStoreManager getInstance() {
+ synchronized (sInstanceLock) {
+ if (sInstance == null) {
+ sInstance = new KeyStoreManager();
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Grants access to the key owned by the calling app stored under the specified {@code alias}
+ * to another app on the device with the provided {@code uid}.
+ *
+ * <p>This method supports granting access to instances of both {@link javax.crypto.SecretKey}
+ * and {@link java.security.PrivateKey}. The resulting ID will persist across reboots and can be
+ * used by the grantee app for the life of the key or until access is revoked with {@link
+ * #revokeKeyAccess(String, int)}.
+ *
+ * <p>If the provided {@code alias} does not correspond to a key in the Android KeyStore, then
+ * an {@link UnrecoverableKeyException} is thrown.
+ *
+ * @param alias the alias of the key to be granted to another app
+ * @param uid the uid of the app to which the key should be granted
+ * @return the ID of the granted key; this can be shared with the specified app, and that
+ * app can use {@link #getGrantedKeyFromId(long)} to access the key
+ * @throws UnrecoverableKeyException if the specified key cannot be recovered
+ * @throws KeyStoreException if an error is encountered when attempting to grant access to
+ * the key
+ * @see #getGrantedKeyFromId(long)
+ */
+ public long grantKeyAccess(@NonNull String alias, int uid)
+ throws KeyStoreException, UnrecoverableKeyException {
+ KeyDescriptor keyDescriptor = createKeyDescriptorFromAlias(alias);
+ final int grantAccessVector = KeyPermission.USE | KeyPermission.GET_INFO;
+ // When a key is in the GRANT domain, the nspace field of the KeyDescriptor contains its ID.
+ KeyDescriptor result = null;
+ try {
+ result = mKeyStore2.grant(keyDescriptor, uid, grantAccessVector);
+ } catch (KeyStoreException e) {
+ // If the provided alias does not correspond to a valid key in the KeyStore, then throw
+ // an UnrecoverableKeyException to remain consistent with other APIs in this class.
+ if (e.getNumericErrorCode() == KeyStoreException.ERROR_KEY_DOES_NOT_EXIST) {
+ throw new UnrecoverableKeyException("No key found by the given alias");
+ }
+ throw e;
+ }
+ if (result == null) {
+ Log.e(TAG, "Received a null KeyDescriptor from grant");
+ throw new KeyStoreException(KeyStoreException.ERROR_INTERNAL_SYSTEM_ERROR,
+ "No ID was returned for the grant request for alias " + alias + " to uid "
+ + uid);
+ } else if (result.domain != Domain.GRANT) {
+ Log.e(TAG, "Received a result outside the grant domain: " + result.domain);
+ throw new KeyStoreException(KeyStoreException.ERROR_INTERNAL_SYSTEM_ERROR,
+ "Unable to obtain a grant ID for alias " + alias + " to uid " + uid);
+ }
+ return result.nspace;
+ }
+
+ /**
+ * Revokes access to the key in the app's namespace stored under the specified {@code
+ * alias} that was previously granted to another app on the device with the provided
+ * {@code uid}.
+ *
+ * <p>If the provided {@code alias} does not correspond to a key in the Android KeyStore, then
+ * an {@link UnrecoverableKeyException} is thrown.
+ *
+ * @param alias the alias of the key to be revoked from another app
+ * @param uid the uid of the app from which the key access should be revoked
+ * @throws UnrecoverableKeyException if the specified key cannot be recovered
+ * @throws KeyStoreException if an error is encountered when attempting to revoke access
+ * to the key
+ */
+ public void revokeKeyAccess(@NonNull String alias, int uid)
+ throws KeyStoreException, UnrecoverableKeyException {
+ KeyDescriptor keyDescriptor = createKeyDescriptorFromAlias(alias);
+ try {
+ mKeyStore2.ungrant(keyDescriptor, uid);
+ } catch (KeyStoreException e) {
+ // If the provided alias does not correspond to a valid key in the KeyStore, then throw
+ // an UnrecoverableKeyException to remain consistent with other APIs in this class.
+ if (e.getNumericErrorCode() == KeyStoreException.ERROR_KEY_DOES_NOT_EXIST) {
+ throw new UnrecoverableKeyException("No key found by the given alias");
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Returns the key with the specified {@code id} that was previously shared with the
+ * app.
+ *
+ * <p>This method can return instances of both {@link javax.crypto.SecretKey} and {@link
+ * java.security.PrivateKey}. If a key with the provide {@code id} has not been granted to the
+ * caller, then an {@link UnrecoverableKeyException} is thrown.
+ *
+ * @param id the ID of the key that was shared with the app
+ * @return the {@link Key} that was shared with the app
+ * @throws UnrecoverableKeyException if the specified key cannot be recovered
+ * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
+ * be used if the user has been authenticated and a
+ * change has been made to the users
+ * lockscreen or biometric enrollment that
+ * permanently invalidates the key
+ * @see #grantKeyAccess(String, int)
+ */
+ public @NonNull Key getGrantedKeyFromId(long id)
+ throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+ Key result = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore2, null,
+ id, Domain.GRANT);
+ if (result == null) {
+ throw new UnrecoverableKeyException("No key found by the given alias");
+ }
+ return result;
+ }
+
+ /**
+ * Returns a {@link KeyPair} containing the public and private key associated with
+ * the key that was previously shared with the app under the provided {@code id}.
+ *
+ * <p>If a {@link java.security.PrivateKey} has not been granted to the caller with the
+ * specified {@code id}, then an {@link UnrecoverableKeyException} is thrown.
+ *
+ * @param id the ID of the private key that was shared with the app
+ * @return a KeyPair containing the public and private key shared with the app
+ * @throws UnrecoverableKeyException if the specified key cannot be recovered
+ * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
+ * be used if the user has been authenticated and a
+ * change has been made to the users
+ * lockscreen or biometric enrollment that
+ * permanently invalidates the key
+ */
+ public @NonNull KeyPair getGrantedKeyPairFromId(long id)
+ throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+ KeyDescriptor keyDescriptor = createKeyDescriptorFromId(id, Domain.GRANT);
+ return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(mKeyStore2,
+ keyDescriptor);
+ }
+
+ /**
+ * Returns a {@link List} of {@link X509Certificate} instances representing the certificate
+ * chain for the key that was previously shared with the app under the provided {@code id}.
+ *
+ * <p>If a {@link java.security.PrivateKey} has not been granted to the caller with the
+ * specified {@code id}, then an {@link UnrecoverableKeyException} is thrown.
+ *
+ * @param id the ID of the asymmetric key that was shared with the app
+ * @return a List of X509Certificates with the certificate at index 0 corresponding to
+ * the private key shared with the app
+ * @throws UnrecoverableKeyException if the specified key cannot be recovered
+ * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
+ * be used if the user has been authenticated and a
+ * change has been made to the users
+ * lockscreen or biometric enrollment that
+ * permanently invalidates the key
+ * @see #grantKeyAccess(String, int)
+ */
+ // Java APIs should prefer mutable collection return types with the exception being
+ // Collection.empty return types.
+ @SuppressWarnings("MixedMutabilityReturnType")
+ public @NonNull List<X509Certificate> getGrantedCertificateChainFromId(long id)
+ throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+ KeyDescriptor keyDescriptor = createKeyDescriptorFromId(id, Domain.GRANT);
+ KeyPair keyPair = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(mKeyStore2,
+ keyDescriptor);
+ PublicKey keyStoreKey = keyPair.getPublic();
+ if (keyStoreKey instanceof AndroidKeyStorePublicKey) {
+ AndroidKeyStorePublicKey androidKeyStorePublicKey =
+ (AndroidKeyStorePublicKey) keyStoreKey;
+ byte[] certBytes = androidKeyStorePublicKey.getCertificate();
+ X509Certificate cert = getCertificate(certBytes);
+ // If the leaf certificate is null, then a chain should not exist either
+ if (cert == null) {
+ return Collections.emptyList();
+ }
+ List<X509Certificate> result = new ArrayList<>();
+ result.add(cert);
+ byte[] certificateChain = androidKeyStorePublicKey.getCertificateChain();
+ Collection<X509Certificate> certificates = getCertificates(certificateChain);
+ result.addAll(certificates);
+ return result;
+ } else {
+ Log.e(TAG, "keyStoreKey is not of the expected type: " + keyStoreKey);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns an {@link X509Certificate} instance from the provided {@code certificate} byte
+ * encoding of the certificate, or null if the provided encoding is null.
+ */
+ private static X509Certificate getCertificate(byte[] certificate) {
+ X509Certificate result = null;
+ if (certificate != null) {
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ result = (X509Certificate) certificateFactory.generateCertificate(
+ new ByteArrayInputStream(certificate));
+ } catch (Exception e) {
+ Log.e(TAG, "Caught an exception parsing the certificate: ", e);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns a {@link Collection} of {@link X509Certificate} instances from the provided
+ * {@code certificateChain} byte encoding of the certificates, or null if the provided
+ * encoding is null.
+ */
+ private static Collection<X509Certificate> getCertificates(byte[] certificateChain) {
+ if (certificateChain != null) {
+ try {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
+ Collection<X509Certificate> certificates =
+ (Collection<X509Certificate>) certificateFactory.generateCertificates(
+ new ByteArrayInputStream(certificateChain));
+ if (certificates == null) {
+ Log.e(TAG, "Received null certificates from a non-null certificateChain");
+ return Collections.emptyList();
+ }
+ return certificates;
+ } catch (Exception e) {
+ Log.e(TAG, "Caught an exception parsing the certs: ", e);
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Returns a new {@link KeyDescriptor} instance in the app domain / namespace with the {@code
+ * alias} set to the provided value.
+ */
+ private static KeyDescriptor createKeyDescriptorFromAlias(String alias) {
+ KeyDescriptor keyDescriptor = new KeyDescriptor();
+ keyDescriptor.domain = Domain.APP;
+ keyDescriptor.nspace = KeyProperties.NAMESPACE_APPLICATION;
+ keyDescriptor.alias = alias;
+ keyDescriptor.blob = null;
+ return keyDescriptor;
+ }
+
+ /**
+ * Returns a new {@link KeyDescriptor} instance in the provided {@code domain} with the nspace
+ * field set to the provided {@code id}.
+ */
+ private static KeyDescriptor createKeyDescriptorFromId(long id, int domain) {
+ KeyDescriptor keyDescriptor = new KeyDescriptor();
+ keyDescriptor.domain = domain;
+ keyDescriptor.nspace = id;
+ keyDescriptor.alias = null;
+ keyDescriptor.blob = null;
+ return keyDescriptor;
+ }
+}
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
index 99100de..dcc8844 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
@@ -17,6 +17,7 @@
package android.security.keystore2;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.security.KeyStore2;
import android.security.KeyStoreSecurityLevel;
import android.security.keymaster.KeymasterDefs;
@@ -335,11 +336,11 @@
}
/**
- * Loads an an AndroidKeyStoreKey from the AndroidKeyStore backend.
+ * Loads an AndroidKeyStoreKey from the AndroidKeyStore backend.
*
* @param keyStore The keystore2 backend.
* @param alias The alias of the key in the Keystore database.
- * @param namespace The a Keystore namespace. This is used by system api only to request
+ * @param namespace The Keystore namespace. This is used by system api only to request
* Android system specific keystore namespace, which can be configured
* in the device's SEPolicy. Third party apps and most system components
* set this parameter to -1 to indicate their application specific namespace.
@@ -351,14 +352,40 @@
public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(
@NonNull KeyStore2 keyStore, @NonNull String alias, int namespace)
throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
- KeyDescriptor descriptor = new KeyDescriptor();
+ int descriptorNamespace;
+ int descriptorDomain;
if (namespace == KeyProperties.NAMESPACE_APPLICATION) {
- descriptor.nspace = KeyProperties.NAMESPACE_APPLICATION; // ignored;
- descriptor.domain = Domain.APP;
+ descriptorNamespace = KeyProperties.NAMESPACE_APPLICATION; // ignored;
+ descriptorDomain = Domain.APP;
} else {
- descriptor.nspace = namespace;
- descriptor.domain = Domain.SELINUX;
+ descriptorNamespace = namespace;
+ descriptorDomain = Domain.SELINUX;
}
+ return loadAndroidKeyStoreKeyFromKeystore(keyStore, alias, descriptorNamespace,
+ descriptorDomain);
+ }
+
+ /**
+ * Loads an AndroidKeyStoreKey from the AndroidKeyStore backend.
+ *
+ * @param keyStore The keystore2 backend
+ * @param alias The alias of the key in the Keystore database
+ * @param namespace The Keystore namespace. This is used by system api only to request
+ * Android system specific keystore namespace, which can be configured
+ * in the device's SEPolicy. Third party apps and most system components
+ * set this parameter to -1 to indicate their application specific namespace.
+ * See <a href="https://source.android.com/security/keystore#access-control">
+ * Keystore 2.0 access control</a>
+ * @param domain The Keystore domain
+ * @return an AndroidKeyStoreKey corresponding to the provided values for the KeyDescriptor
+ * @hide
+ */
+ public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(@NonNull KeyStore2 keyStore,
+ @Nullable String alias, long namespace, int domain)
+ throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
+ KeyDescriptor descriptor = new KeyDescriptor();
+ descriptor.nspace = namespace;
+ descriptor.domain = domain;
descriptor.alias = alias;
descriptor.blob = null;
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
index 0b3be32..bcf619b 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
@@ -44,6 +44,22 @@
mEncoded = x509EncodedForm;
}
+ /**
+ * Returns the byte array encoding of the certificate corresponding to this public key.
+ * @hide
+ */
+ public byte[] getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Returns the byte array encoding of the certificate chain for this public key.
+ * @hide
+ */
+ public byte[] getCertificateChain() {
+ return mCertificateChain;
+ }
+
abstract AndroidKeyStorePrivateKey getPrivateKey();
@Override
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 7f11fea..2ab0310 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -57,9 +57,9 @@
*/
private static final int NO_LEVEL_OVERRIDE = -1;
- private static final int EXTENSIONS_VERSION_V7 = 7;
+ private static final int EXTENSIONS_VERSION_V8 = 8;
- private static final int EXTENSIONS_VERSION_V6 = 6;
+ private static final int EXTENSIONS_VERSION_V7 = 7;
private final Object mLock = new Object();
private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer;
@@ -80,12 +80,10 @@
*/
@VisibleForTesting
static int getExtensionsVersionCurrentPlatform() {
- if (Flags.activityEmbeddingAnimationCustomizationFlag()) {
- // Activity Embedding animation customization is the only major feature for v7.
- return EXTENSIONS_VERSION_V7;
- } else {
- return EXTENSIONS_VERSION_V6;
+ if (Flags.aeBackStackRestore()) {
+ return EXTENSIONS_VERSION_V8;
}
+ return EXTENSIONS_VERSION_V7;
}
private String generateLogMessage() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 19b51f1..e4db7b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -164,6 +164,9 @@
*/
private BackTouchTracker mQueuedTracker = new BackTouchTracker();
+ private final BackTransitionObserver mBackTransitionObserver =
+ new BackTransitionObserver();
+
private final Runnable mAnimationTimeoutRunnable = () -> {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
MAX_ANIMATION_DURATION);
@@ -268,6 +271,8 @@
mBackTransitionHandler = new BackTransitionHandler();
mTransitions.addHandler(mBackTransitionHandler);
mHandler = handler;
+ mTransitions.registerObserver(mBackTransitionObserver);
+ mBackTransitionObserver.setBackTransitionHandler(mBackTransitionHandler);
updateTouchableArea();
}
@@ -729,6 +734,13 @@
}
/**
+ * @return Latest task id which back gesture has occurred on it.
+ */
+ public int getLatestTriggerBackTask() {
+ return mBackTransitionObserver.mFocusedTaskId;
+ }
+
+ /**
* Sets to true when the back gesture has passed the triggering threshold, false otherwise.
*/
public void setTriggerBack(boolean triggerBack) {
@@ -792,6 +804,11 @@
boolean triggerBack = activeTouchTracker.getTriggerBack();
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack);
+ if (triggerBack) {
+ mBackTransitionObserver.update(mBackNavigationInfo != null
+ ? mBackNavigationInfo.getFocusedTaskId()
+ : INVALID_TASK_ID);
+ }
// Reset gesture states.
mThresholdCrossed = false;
mPointersPilfered = false;
@@ -1218,6 +1235,7 @@
}
if (shouldCancelAnimation(info)) {
+ mPrepareOpenTransition = null;
return false;
}
@@ -1645,4 +1663,58 @@
private static boolean canBeTransitionTarget(TransitionInfo.Change change) {
return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID;
}
+
+ // Record the latest back gesture happen on which task.
+ static class BackTransitionObserver implements Transitions.TransitionObserver {
+ int mFocusedTaskId = INVALID_TASK_ID;
+ IBinder mFocusTaskMonitorToken;
+ private BackTransitionHandler mBackTransitionHandler;
+ void setBackTransitionHandler(BackTransitionHandler handler) {
+ mBackTransitionHandler = handler;
+ }
+
+ void update(int focusedTaskId) {
+ mFocusedTaskId = focusedTaskId;
+ }
+
+ @Override
+ public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ if (mFocusedTaskId == INVALID_TASK_ID) {
+ return;
+ }
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change c = info.getChanges().get(i);
+ if (c.getTaskInfo() != null && c.getTaskInfo().taskId == mFocusedTaskId) {
+ mFocusTaskMonitorToken = transition;
+ break;
+ }
+ }
+ // Transition happen but the task isn't involved, reset.
+ if (mFocusTaskMonitorToken == null) {
+ mFocusedTaskId = INVALID_TASK_ID;
+ }
+ }
+
+ @Override
+ public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+ if (mFocusTaskMonitorToken == merged) {
+ mFocusTaskMonitorToken = playing;
+ }
+ if (mBackTransitionHandler.mClosePrepareTransition == merged) {
+ mBackTransitionHandler.mClosePrepareTransition = null;
+ }
+ }
+
+ @Override
+ public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+ if (mFocusTaskMonitorToken == transition) {
+ mFocusedTaskId = INVALID_TASK_ID;
+ }
+ if (mBackTransitionHandler.mClosePrepareTransition == transition) {
+ mBackTransitionHandler.mClosePrepareTransition = null;
+ }
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
index 13a805a..e71b4f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt
@@ -95,15 +95,15 @@
override fun addToContainer(menuView: ManageWindowsView) {
val menuPosition = calculateMenuPosition()
menuViewContainer = AdditionalSystemViewContainer(
- windowManagerWrapper,
- callerTaskInfo.taskId,
- menuPosition.x,
- menuPosition.y,
- menuView.menuWidth,
- menuView.menuHeight,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ windowManagerWrapper = windowManagerWrapper,
+ taskId = callerTaskInfo.taskId,
+ x = menuPosition.x,
+ y = menuPosition.y,
+ width = menuView.menuWidth,
+ height = menuView.menuHeight,
+ flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
- menuView.rootView
+ view = menuView.rootView,
)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
index 05391a8..173bc08 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt
@@ -22,14 +22,19 @@
import android.graphics.Point
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
+import android.view.WindowInsets.Type.systemBars
import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.window.TaskConstants
import android.window.TaskSnapshot
import androidx.compose.ui.graphics.toArgb
+import com.android.internal.annotations.VisibleForTesting
+import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
@@ -41,9 +46,12 @@
*/
class DesktopHeaderManageWindowsMenu(
private val callerTaskInfo: RunningTaskInfo,
+ private val x: Int,
+ private val y: Int,
private val displayController: DisplayController,
private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer,
context: Context,
+ private val desktopRepository: DesktopRepository,
private val surfaceControlBuilderSupplier: Supplier<SurfaceControl.Builder>,
private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
snapshotList: List<Pair<Int, TaskSnapshot>>,
@@ -53,7 +61,8 @@
context,
DecorThemeUtil(context).getColorScheme(callerTaskInfo).background.toArgb()
) {
- private var menuViewContainer: AdditionalViewContainer? = null
+ @VisibleForTesting
+ var menuViewContainer: AdditionalViewContainer? = null
init {
show(snapshotList, onIconClickListener, onOutsideClickListener)
@@ -64,8 +73,37 @@
}
override fun addToContainer(menuView: ManageWindowsView) {
- val taskBounds = callerTaskInfo.getConfiguration().windowConfiguration.bounds
- val menuPosition = Point(taskBounds.left, taskBounds.top)
+ val menuPosition = Point(x, y)
+ val flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+ WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+ menuViewContainer = if (Flags.enableFullyImmersiveInDesktop()
+ && desktopRepository.isTaskInFullImmersiveState(callerTaskInfo.taskId)) {
+ // Use system view container so that forcibly shown system bars take effect in
+ // immersive.
+ createAsSystemViewContainer(menuPosition, flags)
+ } else {
+ createAsViewHostContainer(menuPosition, flags)
+ }
+ }
+
+ private fun createAsSystemViewContainer(position: Point, flags: Int): AdditionalViewContainer {
+ return AdditionalSystemViewContainer(
+ windowManagerWrapper = WindowManagerWrapper(
+ context.getSystemService(WindowManager::class.java)
+ ),
+ taskId = callerTaskInfo.taskId,
+ x = position.x,
+ y = position.y,
+ width = menuView.menuWidth,
+ height = menuView.menuHeight,
+ flags = flags,
+ forciblyShownTypes = systemBars(),
+ view = menuView.rootView
+ )
+ }
+
+ private fun createAsViewHostContainer(position: Point, flags: Int): AdditionalViewContainer {
val builder = surfaceControlBuilderSupplier.get()
rootTdaOrganizer.attachToDisplayArea(callerTaskInfo.displayId, builder)
val leash = builder
@@ -73,11 +111,10 @@
.setContainerLayer()
.build()
val lp = WindowManager.LayoutParams(
- menuView.menuWidth, menuView.menuHeight,
+ menuView.menuWidth,
+ menuView.menuHeight,
WindowManager.LayoutParams.TYPE_APPLICATION,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ flags,
PixelFormat.TRANSPARENT
)
val windowManager = WindowlessWindowManager(
@@ -93,11 +130,12 @@
menuView.let { viewHost.setView(it.rootView, lp) }
val t = surfaceControlTransactionSupplier.get()
t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
- .setPosition(leash, menuPosition.x.toFloat(), menuPosition.y.toFloat())
+ .setPosition(leash, position.x.toFloat(), position.y.toFloat())
.show(leash)
t.apply()
- menuViewContainer = AdditionalViewHostViewContainer(
- leash, viewHost,
+ return AdditionalViewHostViewContainer(
+ leash,
+ viewHost,
surfaceControlTransactionSupplier
)
}
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 d3b7ca1..6eb20b9 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
@@ -454,7 +454,13 @@
}
if (isHandleMenuActive()) {
- mHandleMenu.relayout(startT, mResult.mCaptionX);
+ mHandleMenu.relayout(
+ startT,
+ mResult.mCaptionX,
+ // Add top padding to the caption Y so that the menu is shown over what is the
+ // actual contents of the caption, ignoring padding. This is currently relevant
+ // to the Header in desktop immersive.
+ mResult.mCaptionY + mResult.mCaptionTopPadding);
}
if (isOpenByDefaultDialogActive()) {
@@ -1258,6 +1264,8 @@
&& Flags.enableDesktopWindowingMultiInstanceFeatures();
final boolean shouldShowManageWindowsButton = supportsMultiInstance
&& mMinimumInstancesFound;
+ final boolean inDesktopImmersive = mDesktopRepository
+ .isTaskInFullImmersiveState(mTaskInfo.taskId);
mHandleMenu = mHandleMenuFactory.create(
this,
mWindowManagerWrapper,
@@ -1271,7 +1279,11 @@
getBrowserLink(),
mResult.mCaptionWidth,
mResult.mCaptionHeight,
- mResult.mCaptionX
+ mResult.mCaptionX,
+ // Add top padding to the caption Y so that the menu is shown over what is the
+ // actual contents of the caption, ignoring padding. This is currently relevant
+ // to the Header in desktop immersive.
+ mResult.mCaptionY + mResult.mCaptionTopPadding
);
mWindowDecorViewHolder.onHandleMenuOpened();
mHandleMenu.show(
@@ -1302,7 +1314,8 @@
/* onOutsideTouchListener= */ () -> {
closeHandleMenu();
return Unit.INSTANCE;
- }
+ },
+ /* forceShowSystemBars= */ inDesktopImmersive
);
if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
notifyCaptionStateChanged();
@@ -1316,9 +1329,12 @@
if (mTaskInfo.isFreeform()) {
mManageWindowsMenu = new DesktopHeaderManageWindowsMenu(
mTaskInfo,
+ /* x= */ mResult.mCaptionX,
+ /* y= */ mResult.mCaptionY + mResult.mCaptionTopPadding,
mDisplayController,
mRootTaskDisplayAreaOrganizer,
mContext,
+ mDesktopRepository,
mSurfaceControlBuilderSupplier,
mSurfaceControlTransactionSupplier,
snapshotList,
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 2e32703..93bd929 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
@@ -32,6 +32,7 @@
import android.view.MotionEvent.ACTION_OUTSIDE
import android.view.SurfaceControl
import android.view.View
+import android.view.WindowInsets.Type.systemBars
import android.view.WindowManager
import android.widget.Button
import android.widget.ImageButton
@@ -73,7 +74,8 @@
private val openInBrowserIntent: Intent?,
private val captionWidth: Int,
private val captionHeight: Int,
- captionX: Int
+ captionX: Int,
+ captionY: Int
) {
private val context: Context = parentDecor.mDecorWindowContext
private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo
@@ -110,7 +112,7 @@
get() = openInBrowserIntent != null
init {
- updateHandleMenuPillPositions(captionX)
+ updateHandleMenuPillPositions(captionX, captionY)
}
fun show(
@@ -123,6 +125,7 @@
onOpenByDefaultClickListener: () -> Unit,
onCloseMenuClickListener: () -> Unit,
onOutsideTouchListener: () -> Unit,
+ forceShowSystemBars: Boolean = false,
) {
val ssg = SurfaceSyncGroup(TAG)
val t = SurfaceControl.Transaction()
@@ -139,6 +142,7 @@
onOpenByDefaultClickListener = onOpenByDefaultClickListener,
onCloseMenuClickListener = onCloseMenuClickListener,
onOutsideTouchListener = onOutsideTouchListener,
+ forceShowSystemBars = forceShowSystemBars,
)
ssg.addTransaction(t)
ssg.markSyncReady()
@@ -157,7 +161,8 @@
openInBrowserClickListener: (Intent) -> Unit,
onOpenByDefaultClickListener: () -> Unit,
onCloseMenuClickListener: () -> Unit,
- onOutsideTouchListener: () -> Unit
+ onOutsideTouchListener: () -> Unit,
+ forceShowSystemBars: Boolean = false,
) {
val handleMenuView = HandleMenuView(
context = context,
@@ -185,7 +190,7 @@
val x = handleMenuPosition.x.toInt()
val y = handleMenuPosition.y.toInt()
handleMenuViewContainer =
- if (!taskInfo.isFreeform && Flags.enableHandleInputFix()) {
+ if ((!taskInfo.isFreeform && Flags.enableHandleInputFix()) || forceShowSystemBars) {
AdditionalSystemViewContainer(
windowManagerWrapper = windowManagerWrapper,
taskId = taskInfo.taskId,
@@ -196,7 +201,8 @@
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
- view = handleMenuView.rootView
+ view = handleMenuView.rootView,
+ forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 }
)
} else {
parentDecor.addWindow(
@@ -210,15 +216,15 @@
/**
* Updates handle menu's position variables to reflect its next position.
*/
- private fun updateHandleMenuPillPositions(captionX: Int) {
+ private fun updateHandleMenuPillPositions(captionX: Int, captionY: Int) {
val menuX: Int
val menuY: Int
val taskBounds = taskInfo.getConfiguration().windowConfiguration.bounds
- updateGlobalMenuPosition(taskBounds, captionX)
+ updateGlobalMenuPosition(taskBounds, captionX, captionY)
if (layoutResId == R.layout.desktop_mode_app_header) {
// Align the handle menu to the left side of the caption.
menuX = marginMenuStart
- menuY = marginMenuTop
+ menuY = captionY + marginMenuTop
} else {
if (Flags.enableHandleInputFix()) {
// In a focused decor, we use global coordinates for handle menu. Therefore we
@@ -228,26 +234,26 @@
menuY = globalMenuPosition.y
} else {
menuX = (taskBounds.width() / 2) - (menuWidth / 2)
- menuY = marginMenuTop
+ menuY = captionY + marginMenuTop
}
}
// Handle Menu position setup.
handleMenuPosition.set(menuX.toFloat(), menuY.toFloat())
}
- private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int) {
+ private fun updateGlobalMenuPosition(taskBounds: Rect, captionX: Int, captionY: Int) {
val nonFreeformX = captionX + (captionWidth / 2) - (menuWidth / 2)
when {
taskInfo.isFreeform -> {
globalMenuPosition.set(
/* x = */ taskBounds.left + marginMenuStart,
- /* y = */ taskBounds.top + marginMenuTop
+ /* y = */ taskBounds.top + captionY + marginMenuTop
)
}
taskInfo.isFullscreen -> {
globalMenuPosition.set(
/* x = */ nonFreeformX,
- /* y = */ marginMenuTop
+ /* y = */ marginMenuTop + captionY
)
}
taskInfo.isMultiWindow -> {
@@ -261,13 +267,13 @@
SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT -> {
globalMenuPosition.set(
/* x = */ leftOrTopStageBounds.width() + nonFreeformX,
- /* y = */ marginMenuTop
+ /* y = */ captionY + marginMenuTop
)
}
SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT -> {
globalMenuPosition.set(
/* x = */ nonFreeformX,
- /* y = */ marginMenuTop
+ /* y = */ captionY + marginMenuTop
)
}
}
@@ -280,10 +286,11 @@
*/
fun relayout(
t: SurfaceControl.Transaction,
- captionX: Int
+ captionX: Int,
+ captionY: Int,
) {
handleMenuViewContainer?.let { container ->
- updateHandleMenuPillPositions(captionX)
+ updateHandleMenuPillPositions(captionX, captionY)
container.setPosition(t, handleMenuPosition.x, handleMenuPosition.y)
}
}
@@ -675,7 +682,8 @@
openInBrowserIntent: Intent?,
captionWidth: Int,
captionHeight: Int,
- captionX: Int
+ captionX: Int,
+ captionY: Int,
): HandleMenu
}
@@ -694,7 +702,8 @@
openInBrowserIntent: Intent?,
captionWidth: Int,
captionHeight: Int,
- captionX: Int
+ captionX: Int,
+ captionY: Int,
): HandleMenu {
return HandleMenu(
parentDecor,
@@ -709,7 +718,8 @@
openInBrowserIntent,
captionWidth,
captionHeight,
- captionX
+ captionX,
+ captionY,
)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index ce5cfd0..6b3b357 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -256,6 +256,8 @@
outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
+ outResult.mCaptionY = 0;
+ outResult.mCaptionTopPadding = params.mCaptionTopPadding;
updateDecorationContainerSurface(startT, outResult);
updateCaptionContainerSurface(startT, outResult);
@@ -786,6 +788,8 @@
int mCaptionHeight;
int mCaptionWidth;
int mCaptionX;
+ int mCaptionY;
+ int mCaptionTopPadding;
final Region mCustomizableCaptionRegion = Region.obtain();
int mWidth;
int mHeight;
@@ -797,6 +801,8 @@
mCaptionHeight = 0;
mCaptionWidth = 0;
mCaptionX = 0;
+ mCaptionY = 0;
+ mCaptionTopPadding = 0;
mCustomizableCaptionRegion.setEmpty();
mRootView = null;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 1be26f0..8b6aaaf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -23,6 +23,7 @@
import android.view.LayoutInflater
import android.view.SurfaceControl
import android.view.View
+import android.view.WindowInsets
import android.view.WindowManager
import com.android.wm.shell.windowdecor.WindowManagerWrapper
@@ -38,6 +39,7 @@
width: Int,
height: Int,
flags: Int,
+ @WindowInsets.Type.InsetsType forciblyShownTypes: Int = 0,
override val view: View
) : AdditionalViewContainer() {
val lp: WindowManager.LayoutParams = WindowManager.LayoutParams(
@@ -49,6 +51,7 @@
title = "Additional view container of Task=$taskId"
gravity = Gravity.LEFT or Gravity.TOP
setTrustedOverlay()
+ this.forciblyShownTypes = forciblyShownTypes
}
constructor(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
new file mode 100644
index 0000000..f9f760e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+/**
+ * Tests for [DesktopHeaderManageWindowsMenu].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DesktopHeaderManageWindowsMenuTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class DesktopHeaderManageWindowsMenuTest : ShellTestCase() {
+
+ @JvmField
+ @Rule
+ val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+ private lateinit var desktopRepository: DesktopRepository
+ private lateinit var menu: DesktopHeaderManageWindowsMenu
+
+ @Before
+ fun setUp() {
+ desktopRepository = DesktopRepository(
+ context = context,
+ shellInit = ShellInit(TestShellExecutor()),
+ persistentRepository = mock(),
+ mainCoroutineScope = mock()
+ )
+ }
+
+ @After
+ fun tearDown() {
+ menu.close()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testShow_forImmersiveTask_usesSystemViewContainer() {
+ val task = createFreeformTask()
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ menu = createMenu(task)
+
+ assertThat(menu.menuViewContainer).isInstanceOf(AdditionalSystemViewContainer::class.java)
+ }
+
+ private fun createMenu(task: RunningTaskInfo) = DesktopHeaderManageWindowsMenu(
+ callerTaskInfo = task,
+ x = 0,
+ y = 0,
+ displayController = mock(),
+ rootTdaOrganizer = mock(),
+ context = context,
+ desktopRepository = desktopRepository,
+ surfaceControlBuilderSupplier = { SurfaceControl.Builder() },
+ surfaceControlTransactionSupplier = { SurfaceControl.Transaction() },
+ snapshotList = emptyList(),
+ onIconClickListener = {},
+ onOutsideClickListener = {},
+ )
+
+ private fun createFreeformTask(): RunningTaskInfo = TestRunningTaskInfoBuilder()
+ .setToken(MockToken().token())
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
+ .build()
+}
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 1d11d2e..3208872 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
@@ -259,7 +259,8 @@
doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(),
- anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt()))
+ anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt(),
+ anyInt()))
.thenReturn(mMockHandleMenu);
when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false);
when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(),
@@ -1070,7 +1071,8 @@
openInBrowserCaptor.capture(),
any(),
any(),
- any()
+ any(),
+ anyBoolean()
);
openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
@@ -1099,7 +1101,8 @@
openInBrowserCaptor.capture(),
any(),
any(),
- any()
+ any(),
+ anyBoolean()
);
openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1));
@@ -1151,7 +1154,8 @@
any(),
any(),
closeClickListener.capture(),
- any()
+ any(),
+ anyBoolean()
);
closeClickListener.getValue().invoke();
@@ -1161,6 +1165,30 @@
}
@Test
+ public void createHandleMenu_immersiveWindow_forceShowsSystemBars() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo,
+ true /* relayout */);
+ when(mMockDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId))
+ .thenReturn(true);
+
+ createHandleMenu(decoration);
+
+ verify(mMockHandleMenu).show(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ /* forceShowSystemBars= */ eq(true)
+ );
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
public void notifyCaptionStateChanged_flagDisabled_doNoNotify() {
when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
@@ -1301,7 +1329,7 @@
verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(),
any(), anyBoolean(), anyBoolean(), anyBoolean(),
argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)),
- anyInt(), anyInt(), anyInt());
+ anyInt(), anyInt(), anyInt(), anyInt());
}
private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index 1820133..9544fa8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -33,6 +33,7 @@
import android.view.SurfaceControl
import android.view.SurfaceControlViewHost
import android.view.View
+import android.view.WindowInsets.Type.systemBars
import android.view.WindowManager
import androidx.core.graphics.toPointF
import androidx.test.filters.SmallTest
@@ -186,13 +187,35 @@
assertEquals(expected.toPointF(), handleMenu.handleMenuPosition)
}
- private fun createTaskInfo(windowingMode: Int, splitPosition: Int) {
+ @Test
+ fun testCreate_forceShowSystemBars_usesSystemViewContainer() {
+ createTaskInfo(WINDOWING_MODE_FREEFORM)
+
+ handleMenu = createAndShowHandleMenu(forceShowSystemBars = true)
+
+ // Only AdditionalSystemViewContainer supports force showing system bars.
+ assertTrue(handleMenu.handleMenuViewContainer is AdditionalSystemViewContainer)
+ }
+
+ @Test
+ fun testCreate_forceShowSystemBars() {
+ createTaskInfo(WINDOWING_MODE_FREEFORM)
+
+ handleMenu = createAndShowHandleMenu(forceShowSystemBars = true)
+
+ val types = (handleMenu.handleMenuViewContainer as AdditionalSystemViewContainer)
+ .lp.forciblyShownTypes
+ assertTrue((types and systemBars()) != 0)
+ }
+
+ private fun createTaskInfo(windowingMode: Int, splitPosition: Int? = null) {
val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder()
.setBackgroundColor(Color.YELLOW)
val bounds = when (windowingMode) {
WINDOWING_MODE_FULLSCREEN -> DISPLAY_BOUNDS
WINDOWING_MODE_FREEFORM -> FREEFORM_BOUNDS
WINDOWING_MODE_MULTI_WINDOW -> {
+ checkNotNull(splitPosition)
if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
SPLIT_LEFT_BOUNDS
} else {
@@ -208,14 +231,19 @@
.setBounds(bounds)
.setVisible(true)
.build()
- whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition)
- whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer {
- (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS)
- (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS)
+ if (windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+ whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition)
+ whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer {
+ (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS)
+ (it.arguments[1] as Rect).set(SPLIT_RIGHT_BOUNDS)
+ }
}
}
- private fun createAndShowHandleMenu(splitPosition: Int): HandleMenu {
+ private fun createAndShowHandleMenu(
+ splitPosition: Int? = null,
+ forceShowSystemBars: Boolean = false,
+ ): HandleMenu {
val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
R.layout.desktop_mode_app_header
} else {
@@ -225,6 +253,7 @@
WINDOWING_MODE_FULLSCREEN -> (DISPLAY_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
WINDOWING_MODE_FREEFORM -> 0
WINDOWING_MODE_MULTI_WINDOW -> {
+ checkNotNull(splitPosition)
if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
(SPLIT_LEFT_BOUNDS.width() / 2) - (HANDLE_WIDTH / 2)
} else {
@@ -238,9 +267,21 @@
layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true,
shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false,
null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50,
- captionX = captionX
+ captionX = captionX,
+ captionY = 0,
)
- handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock())
+ handleMenu.show(
+ onToDesktopClickListener = mock(),
+ onToFullscreenClickListener = mock(),
+ onToSplitScreenClickListener = mock(),
+ onNewWindowClickListener = mock(),
+ onManageWindowsClickListener = mock(),
+ openInBrowserClickListener = mock(),
+ onOpenByDefaultClickListener = mock(),
+ onCloseMenuClickListener = mock(),
+ onOutsideTouchListener = mock(),
+ forceShowSystemBars = forceShowSystemBars
+ )
return handleMenu
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index bb41e9c..fb17ae9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -515,6 +515,23 @@
}
@Test
+ public void testRelayout_withPadding_setsOnResult() {
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setBounds(TASK_BOUNDS)
+ .setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
+ .setVisible(true)
+ .build();
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ mRelayoutParams.mCaptionTopPadding = 50;
+
+ windowDecor.relayout(taskInfo, false /* applyStartTransactionOnDraw */,
+ true /* hasGlobalFocus */);
+
+ assertEquals(50, mRelayoutResult.mCaptionTopPadding);
+ }
+
+ @Test
public void testRelayout_fluidResizeEnabled_freeformTask_setTaskSurfaceColor() {
StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
DesktopModeStatus.class).strictness(
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 02ca307..c22b674 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -471,6 +471,7 @@
List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes);
+ @EnforcePermission(anyOf = {"MODIFY_AUDIO_ROUTING", "QUERY_AUDIO_STATE"})
void addOnDevicesForAttributesChangedListener(in AudioAttributes attributes,
in IDevicesForAttributesCallback callback);
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 9a9a073..17d1ff6 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -10,3 +10,11 @@
bug: "323008518"
is_fixed_read_only: true
}
+
+flag {
+ name: "media_projection_connected_display"
+ namespace: "virtual_devices"
+ description: "Enable recording connected display"
+ bug: "362720120"
+ is_exported: true
+}
diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
index 06214eb..8ef4c58 100644
--- a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
+++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
@@ -99,15 +99,18 @@
@Suppress("UNCHECKED_CAST")
val clazz = preferenceScreenCreator.fragmentClass() as Class<PreferenceFragmentCompat>
val builder = StringBuilder()
- launchFragmentScenario(clazz).use {
- it.onFragment { fragment ->
- taskFinished.set(true)
- fragment.preferenceScreen.toString(builder)
- }
+ launchFragment(clazz) { fragment ->
+ taskFinished.set(true)
+ fragment.preferenceScreen.toString(builder)
}
return builder.toString()
}
+ protected open fun launchFragment(
+ fragmentClass: Class<PreferenceFragmentCompat>,
+ action: (PreferenceFragmentCompat) -> Unit,
+ ): Unit = launchFragmentScenario(fragmentClass).use { it.onFragment(action) }
+
protected open fun launchFragmentScenario(fragmentClass: Class<PreferenceFragmentCompat>) =
FragmentScenario.launch(fragmentClass)
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 9726ecf..510c9b7 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1139,11 +1139,5 @@
android:name="android.service.dream"
android:resource="@xml/home_controls_dream_metadata" />
</service>
-
- <activity android:name="com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity"
- android:exported="false"
- android:showForAllUsers="true"
- android:theme="@style/ShortcutHelperTheme"
- android:excludeFromRecents="true" />
</application>
</manifest>
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
index 94f8846..0b15d23 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
@@ -22,8 +22,14 @@
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingMode;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode;
+
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
@@ -157,6 +163,9 @@
t.show(wallpapers[i].leash);
t.setAlpha(wallpapers[i].leash, 1.f);
}
+ if (ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()) {
+ resetLauncherAlphaOnDesktopExit(info, launcherTask, leashMap, t);
+ }
} else {
if (launcherTask != null) {
counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash()));
@@ -236,4 +245,33 @@
}
};
}
+
+ /**
+ * Reset the alpha of the Launcher leash to give the Launcher time to hide its Views before the
+ * exit-desktop animation starts.
+ *
+ * This method should only be called if the current transition is opening Launcher, otherwise we
+ * might not be exiting Desktop Mode.
+ */
+ private static void resetLauncherAlphaOnDesktopExit(
+ TransitionInfo info,
+ TransitionInfo.Change launcherChange,
+ ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+ SurfaceControl.Transaction startTransaction
+ ) {
+ checkArgument(isOpeningMode(launcherChange.getMode()));
+ if (!isClosingType(info.getType())) {
+ return;
+ }
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ // skip changes that we didn't wrap
+ if (!leashMap.containsKey(change.getLeash())) continue;
+ // Only make the update if we are closing Desktop tasks.
+ if (change.getTaskInfo().isFreeform() && isClosingMode(change.getMode())) {
+ startTransaction.setAlpha(leashMap.get(launcherChange.getLeash()), 0f);
+ return;
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
index 025c8b9..f426aa5 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -70,6 +70,19 @@
}
"""
+ private const val SIMPLEX_SIMPLE_SHADER =
+ """
+ vec4 main(vec2 p) {
+ vec2 uv = p / in_size.xy;
+ uv.x *= in_aspectRatio;
+
+ // Compute turbulence effect with the uv distorted with simplex noise.
+ vec3 noisePos = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum;
+ float mixFactor = simplex3d(noisePos) * 0.5 + 0.5;
+ return mix(in_color, in_screenColor, mixFactor);
+ }
+ """
+
private const val FRACTAL_SHADER =
"""
vec4 main(vec2 p) {
@@ -155,6 +168,8 @@
return sparkleLayer;
}
"""
+ private const val SIMPLEX_NOISE_SIMPLE_SHADER =
+ ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SIMPLEX_SIMPLE_SHADER
private const val SIMPLEX_NOISE_SHADER =
ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SHADER
private const val FRACTAL_NOISE_SHADER =
@@ -163,17 +178,20 @@
ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SPARKLE_SHADER
enum class Type {
- /** Effect with a simple color noise turbulence. */
+ /** Effect with a color noise turbulence with luma matte. */
SIMPLEX_NOISE,
+ /** Effect with a noise interpolating between foreground and background colors. */
+ SIMPLEX_NOISE_SIMPLE,
/** Effect with a simple color noise turbulence, with fractal. */
SIMPLEX_NOISE_FRACTAL,
/** Effect with color & sparkle turbulence with screen color layer. */
- SIMPLEX_NOISE_SPARKLE
+ SIMPLEX_NOISE_SPARKLE,
}
fun getShader(type: Type): String {
return when (type) {
Type.SIMPLEX_NOISE -> SIMPLEX_NOISE_SHADER
+ Type.SIMPLEX_NOISE_SIMPLE -> SIMPLEX_NOISE_SIMPLE_SHADER
Type.SIMPLEX_NOISE_FRACTAL -> FRACTAL_NOISE_SHADER
Type.SIMPLEX_NOISE_SPARKLE -> SPARKLE_NOISE_SHADER
}
@@ -206,15 +224,15 @@
setFloatUniform("in_pixelDensity", pixelDensity)
}
- /** Sets the noise color of the effect. Alpha is ignored. */
+ /**
+ * Sets the noise color of the effect. Alpha is ignored for all types except
+ * [Type.SIMPLEX_NOISE_SIMPLE].
+ */
fun setColor(color: Int) {
setColorUniform("in_color", color)
}
- /**
- * Sets the color that is used for blending on top of the background color/image. Only relevant
- * to [Type.SIMPLEX_NOISE_SPARKLE].
- */
+ /** Sets the color that is used for blending on top of the background color/image. */
fun setScreenColor(color: Int) {
setColorUniform("in_screenColor", color)
}
@@ -259,7 +277,7 @@
*/
fun setLumaMatteFactors(
lumaMatteBlendFactor: Float = 1f,
- lumaMatteOverallBrightness: Float = 0f
+ lumaMatteOverallBrightness: Float = 0f,
) {
setFloatUniform("in_lumaMatteBlendFactor", lumaMatteBlendFactor)
setFloatUniform("in_lumaMatteOverallBrightness", lumaMatteOverallBrightness)
@@ -279,8 +297,10 @@
/** Current noise movements in x, y, and z axes. */
var noiseOffsetX: Float = 0f
private set
+
var noiseOffsetY: Float = 0f
private set
+
var noiseOffsetZ: Float = 0f
private set
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index c1c3b1f..d08df26 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -371,10 +371,7 @@
.motionTestValues { animatedAlpha(animatedOffset) exportAs MotionTestValues.alpha }
}
- UserSwitcher(
- viewModel = viewModel,
- modifier = Modifier.weight(1f).swappable().testTag("UserSwitcher"),
- )
+ UserSwitcher(viewModel = viewModel, modifier = Modifier.weight(1f).swappable())
FoldAware(
modifier = Modifier.weight(1f).swappable(inversed = true).testTag("FoldAware"),
@@ -738,7 +735,7 @@
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
- modifier = modifier,
+ modifier = modifier.sysuiResTag("UserSwitcher"),
) {
selectedUserImage?.let {
Image(
@@ -781,7 +778,7 @@
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null,
- modifier = Modifier.size(32.dp),
+ modifier = Modifier.size(32.dp).sysuiResTag("user_switcher_anchor"),
)
}
@@ -819,11 +816,11 @@
expanded = isExpanded,
onDismissRequest = onDismissed,
offset = DpOffset(x = 0.dp, y = -UserSwitcherDropdownHeight),
- modifier =
- Modifier.width(UserSwitcherDropdownWidth).sysuiResTag("user_switcher_dropdown"),
+ modifier = Modifier.width(UserSwitcherDropdownWidth).sysuiResTag("user_list_dropdown"),
) {
items.forEach { userSwitcherDropdownItem ->
DropdownMenuItem(
+ modifier = Modifier.sysuiResTag("user_switcher_item"),
leadingIcon = {
Icon(
icon = userSwitcherDropdownItem.icon,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
index 3a9587c..521330f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -32,7 +32,9 @@
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
@@ -49,6 +51,8 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastIsFinite
+import androidx.compose.ui.zIndex
+import com.android.compose.modifiers.thenIf
import com.android.systemui.communal.ui.viewmodel.DragHandle
import com.android.systemui.communal.ui.viewmodel.ResizeInfo
import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
@@ -150,10 +154,9 @@
/**
* Draws a frame around the content with drag handles on the top and bottom of the content.
*
- * @param index The index of this item in the [LazyGridState].
+ * @param key The unique key of this element, must be the same key used in the [LazyGridState].
+ * @param currentSpan The current span size of this item in the grid.
* @param gridState The [LazyGridState] for the grid containing this item.
- * @param minItemSpan The minimum span that an item may occupy. Items are resized in multiples of
- * this span.
* @param gridContentPadding The content padding used for the grid, needed for determining offsets.
* @param verticalArrangement The vertical arrangement of the grid items.
* @param modifier Optional modifier to apply to the frame.
@@ -162,6 +165,10 @@
* @param outlineColor Optional color to make the outline around the content.
* @param cornerRadius Optional radius to give to the outline around the content.
* @param strokeWidth Optional stroke width to draw the outline with.
+ * @param minHeightPx Optional minimum height in pixels that this widget can be resized to.
+ * @param maxHeightPx Optional maximum height in pixels that this widget can be resized to.
+ * @param resizeMultiple Optional number of spans that we allow resizing by. For example, if set to
+ * 3, then we only allow resizing in multiples of 3 spans.
* @param alpha Optional function to provide an alpha value for the outline. Can be used to fade the
* outline in and out. This is wrapped in a function for performance, as the value is only
* accessed during the draw phase.
@@ -196,10 +203,19 @@
}
val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
+ val isDragging by
+ remember(viewModel) {
+ derivedStateOf {
+ val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+ val bottomOffset =
+ viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+ topOffset > 0 || bottomOffset > 0
+ }
+ }
// Draw content surrounded by drag handles at top and bottom. Allow drag handles
// to overlap content.
- Box(modifier) {
+ Box(modifier.thenIf(isDragging) { Modifier.zIndex(1f) }) {
content()
if (enabled) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index a0fed90..339445e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -22,7 +22,7 @@
import androidx.compose.material3.Text
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
@@ -294,15 +294,10 @@
available: Offset,
consumedByScroll: Offset = Offset.Zero,
) {
- val consumedByPreScroll =
- onPreScroll(available = available, source = NestedScrollSource.Drag)
+ val consumedByPreScroll = onPreScroll(available = available, source = UserInput)
val consumed = consumedByPreScroll + consumedByScroll
- onPostScroll(
- consumed = consumed,
- available = available - consumed,
- source = NestedScrollSource.Drag,
- )
+ onPostScroll(consumed = consumed, available = available - consumed, source = UserInput)
}
fun NestedScrollConnection.preFling(
@@ -738,7 +733,7 @@
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
nestedScroll.onPreScroll(
available = downOffset(fractionOfScreen = 0.1f),
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertIdle(currentScene = SceneA)
}
@@ -750,7 +745,7 @@
nestedScroll.onPostScroll(
consumed = Offset.Zero,
available = Offset.Zero,
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertIdle(currentScene = SceneA)
@@ -764,7 +759,7 @@
nestedScroll.onPostScroll(
consumed = Offset.Zero,
available = downOffset(fractionOfScreen = 0.1f),
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertTransition(currentScene = SceneA)
@@ -784,16 +779,12 @@
val consumed =
nestedScroll.onPreScroll(
available = downOffset(fractionOfScreen = 0.1f),
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertThat(progress).isEqualTo(0.2f)
// do nothing on postScroll
- nestedScroll.onPostScroll(
- consumed = consumed,
- available = Offset.Zero,
- source = NestedScrollSource.Drag,
- )
+ nestedScroll.onPostScroll(consumed = consumed, available = Offset.Zero, source = UserInput)
assertThat(progress).isEqualTo(0.2f)
nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
@@ -813,10 +804,7 @@
nestedScroll.preFling(available = Velocity.Zero)
// a pre scroll event, that could be intercepted by DraggableHandlerImpl
- nestedScroll.onPreScroll(
- available = Offset(0f, secondScroll),
- source = NestedScrollSource.Drag,
- )
+ nestedScroll.onPreScroll(available = Offset(0f, secondScroll), source = UserInput)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
similarity index 72%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
index f8e2f47..d1431ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt
@@ -16,25 +16,26 @@
package com.android.systemui.keyboard.shortcut.ui
-import android.content.Intent
+import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts
-import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
-import com.android.systemui.keyboard.shortcut.shortcutHelperActivityStarter
import com.android.systemui.keyboard.shortcut.shortcutHelperAppCategoriesShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperCurrentAppShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperInputShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperMultiTaskingShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSource
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
-import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.shortcutHelperViewModel
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.phone.systemUIDialogFactory
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -46,7 +47,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
-class ShortcutHelperActivityStarterTest : SysuiTestCase() {
+class ShortcutHelperDialogStarterTest : SysuiTestCase() {
private val fakeSystemSource = FakeKeyboardShortcutGroupsSource()
private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource()
@@ -64,8 +65,14 @@
private val testScope = kosmos.testScope
private val testHelper = kosmos.shortcutHelperTestHelper
- private val fakeStartActivity = kosmos.fakeShortcutHelperStartActivity
- private val starter = kosmos.shortcutHelperActivityStarter
+ private val dialogFactory = kosmos.systemUIDialogFactory
+ private val coroutineScope = kosmos.applicationCoroutineScope
+ private val viewModel = kosmos.shortcutHelperViewModel
+
+ private val starter: ShortcutHelperDialogStarter =
+ with(kosmos) {
+ ShortcutHelperDialogStarter(coroutineScope, viewModel, dialogFactory, activityStarter)
+ }
@Before
fun setUp() {
@@ -74,21 +81,22 @@
}
@Test
- fun start_doesNotStartByDefault() =
+ fun start_doesNotShowDialogByDefault() =
testScope.runTest {
starter.start()
- assertThat(fakeStartActivity.startIntents).isEmpty()
+ assertThat(starter.dialog).isNull()
}
@Test
- fun start_onToggle_startsActivity() =
+ @UiThreadTest
+ fun start_onToggle_showsDialog() =
testScope.runTest {
starter.start()
testHelper.toggle(deviceId = 456)
- verifyShortcutHelperActivityStarted()
+ assertThat(starter.dialog?.isShowing).isTrue()
}
@Test
@@ -101,34 +109,18 @@
testHelper.toggle(deviceId = 456)
- assertThat(fakeStartActivity.startIntents).isEmpty()
+ assertThat(starter.dialog).isNull()
}
@Test
- fun start_onToggle_multipleTimesStartsActivityOnlyWhenNotStarted() =
- testScope.runTest {
- starter.start()
-
- // Starts
- testHelper.toggle(deviceId = 456)
- // Stops
- testHelper.toggle(deviceId = 456)
- // Starts again
- testHelper.toggle(deviceId = 456)
- // Stops
- testHelper.toggle(deviceId = 456)
-
- verifyShortcutHelperActivityStarted(numTimes = 2)
- }
-
- @Test
+ @UiThreadTest
fun start_onRequestShowShortcuts_startsActivity() =
testScope.runTest {
starter.start()
testHelper.showFromActivity()
- verifyShortcutHelperActivityStarted()
+ assertThat(starter.dialog?.isShowing).isTrue()
}
@Test
@@ -140,10 +132,11 @@
testHelper.showFromActivity()
- assertThat(fakeStartActivity.startIntents).isEmpty()
+ assertThat(starter.dialog).isNull()
}
@Test
+ @UiThreadTest
fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyOnce() =
testScope.runTest {
starter.start()
@@ -152,40 +145,40 @@
testHelper.showFromActivity()
testHelper.showFromActivity()
- verifyShortcutHelperActivityStarted(numTimes = 1)
+ assertThat(starter.dialog?.isShowing).isTrue()
}
@Test
+ @UiThreadTest
fun start_onRequestShowShortcuts_multipleTimes_startsActivityOnlyWhenNotStarted() =
testScope.runTest {
starter.start()
+ assertThat(starter.dialog).isNull()
// No-op. Already hidden.
testHelper.hideFromActivity()
+ assertThat(starter.dialog).isNull()
// No-op. Already hidden.
testHelper.hideForSystem()
+ assertThat(starter.dialog).isNull()
// Show 1st time.
testHelper.toggle(deviceId = 987)
+ assertThat(starter.dialog).isNotNull()
+ assertThat(starter.dialog?.isShowing).isTrue()
// No-op. Already shown.
testHelper.showFromActivity()
+ assertThat(starter.dialog?.isShowing).isTrue()
// Hidden.
testHelper.hideFromActivity()
+ assertThat(starter.dialog?.isShowing).isFalse()
// No-op. Already hidden.
testHelper.hideForSystem()
+ assertThat(starter.dialog?.isShowing).isFalse()
// Show 2nd time.
testHelper.toggle(deviceId = 456)
+ assertThat(starter.dialog?.isShowing).isTrue()
// No-op. Already shown.
testHelper.showFromActivity()
-
- verifyShortcutHelperActivityStarted(numTimes = 2)
+ assertThat(starter.dialog?.isShowing).isTrue()
}
-
- private fun verifyShortcutHelperActivityStarted(numTimes: Int = 1) {
- assertThat(fakeStartActivity.startIntents).hasSize(numTimes)
- fakeStartActivity.startIntents.forEach { intent ->
- assertThat(intent.flags).isEqualTo(Intent.FLAG_ACTIVITY_NEW_TASK)
- assertThat(intent.filterEquals(Intent(context, ShortcutHelperActivity::class.java)))
- .isTrue()
- }
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index a629b24..5f3668a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -30,7 +30,6 @@
import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
@@ -50,11 +49,10 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
- private lateinit var controller: SystemEventChipAnimationController
+ private lateinit var controller: SystemEventChipAnimationControllerImpl
@get:Rule val animatorTestRule = AnimatorTestRule(this)
@Mock private lateinit var sbWindowController: StatusBarWindowController
- @Mock private lateinit var sbWindowControllerStore: StatusBarWindowControllerStore
@Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
private var testView = TestView(mContext)
@@ -63,7 +61,6 @@
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- whenever(sbWindowControllerStore.defaultDisplay).thenReturn(sbWindowController)
// StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
// ensure that the chip view is added to a parent view
whenever(sbWindowController.addViewToWindow(any(), any())).then {
@@ -76,7 +73,7 @@
)
statusbarFake.addView(
it.arguments[0] as View,
- it.arguments[1] as FrameLayout.LayoutParams
+ it.arguments[1] as FrameLayout.LayoutParams,
)
}
@@ -86,16 +83,16 @@
/* left= */ insets,
/* top= */ insets,
/* right= */ insets,
- /* bottom= */ 0
+ /* bottom= */ 0,
)
)
whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
.thenReturn(portraitArea)
controller =
- SystemEventChipAnimationController(
+ SystemEventChipAnimationControllerImpl(
context = mContext,
- statusBarWindowControllerStore = sbWindowControllerStore,
+ statusBarWindowController = sbWindowController,
contentInsetsProvider = insetsProvider,
)
}
@@ -156,10 +153,7 @@
val lp = it.arguments[1] as FrameLayout.LayoutParams
assertThat(lp.gravity and Gravity.VERTICAL_GRAVITY_MASK).isEqualTo(Gravity.TOP)
- statusbarFake.addView(
- it.arguments[0] as View,
- lp,
- )
+ statusbarFake.addView(it.arguments[0] as View, lp)
}
// GIVEN insets provider gives the correct content area
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
index 97fa6eb..75479ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
@@ -17,7 +17,7 @@
package com.android.systemui.statusbar.notification
import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -58,10 +58,7 @@
contentHeight = COLLAPSED_CONTENT_HEIGHT
val offsetConsumed =
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = -1f),
- source = NestedScrollSource.Drag,
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
assertThat(offsetConsumed).isEqualTo(Offset.Zero)
assertThat(isStarted).isEqualTo(false)
@@ -73,10 +70,7 @@
scrimOffset = MIN_SCRIM_OFFSET
val offsetConsumed =
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = -1f),
- source = NestedScrollSource.Drag,
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
assertThat(offsetConsumed).isEqualTo(Offset.Zero)
assertThat(isStarted).isEqualTo(false)
@@ -88,10 +82,7 @@
val availableOffset = Offset(x = 0f, y = -1f)
val offsetConsumed =
- scrollConnection.onPreScroll(
- available = availableOffset,
- source = NestedScrollSource.Drag,
- )
+ scrollConnection.onPreScroll(available = availableOffset, source = UserInput)
assertThat(offsetConsumed).isEqualTo(availableOffset)
assertThat(isStarted).isEqualTo(true)
@@ -105,10 +96,7 @@
val availableOffset = Offset(x = 0f, y = -2f)
val consumableOffset = Offset(x = 0f, y = -1f)
val offsetConsumed =
- scrollConnection.onPreScroll(
- available = availableOffset,
- source = NestedScrollSource.Drag,
- )
+ scrollConnection.onPreScroll(available = availableOffset, source = UserInput)
assertThat(offsetConsumed).isEqualTo(consumableOffset)
assertThat(isStarted).isEqualTo(true)
@@ -120,7 +108,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset(x = 0f, y = -1f),
- source = NestedScrollSource.Drag,
+ source = UserInput,
)
assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -130,10 +118,7 @@
@Test
fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest {
val offsetConsumed =
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = 1f),
- source = NestedScrollSource.Drag,
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
assertThat(offsetConsumed).isEqualTo(Offset.Zero)
assertThat(isStarted).isEqualTo(false)
@@ -148,7 +133,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = availableOffset,
- source = NestedScrollSource.Drag
+ source = UserInput,
)
assertThat(offsetConsumed).isEqualTo(availableOffset)
@@ -165,7 +150,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = availableOffset,
- source = NestedScrollSource.Drag
+ source = UserInput,
)
assertThat(offsetConsumed).isEqualTo(consumableOffset)
@@ -180,7 +165,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset(x = 0f, y = 1f),
- source = NestedScrollSource.Drag
+ source = UserInput,
)
assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -197,7 +182,7 @@
scrollConnection.onPostScroll(
consumed = Offset.Zero,
available = Offset(x = 0f, y = 1f),
- source = NestedScrollSource.Drag
+ source = UserInput,
)
assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -210,17 +195,11 @@
fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest {
scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f
contentHeight = EXPANDED_CONTENT_HEIGHT
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = -1f),
- source = NestedScrollSource.Drag
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
assertThat(isStarted).isEqualTo(true)
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = 1f),
- source = NestedScrollSource.Drag
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
assertThat(isStarted).isEqualTo(true)
}
@@ -229,17 +208,11 @@
fun canContinueScroll_atMaxOffset_false() = runTest {
scrimOffset = MAX_SCRIM_OFFSET
contentHeight = EXPANDED_CONTENT_HEIGHT
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = -1f),
- source = NestedScrollSource.Drag
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
assertThat(isStarted).isEqualTo(true)
- scrollConnection.onPreScroll(
- available = Offset(x = 0f, y = 1f),
- source = NestedScrollSource.Drag
- )
+ scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
assertThat(isStarted).isEqualTo(false)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt
new file mode 100644
index 0000000..d2a3a19
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotifCollectionCacheTest : SysuiTestCase() {
+ companion object {
+ const val A = "a"
+ const val B = "b"
+ const val C = "c"
+ }
+
+ val systemClock = FakeSystemClock()
+ val underTest =
+ NotifCollectionCache<String>(purgeTimeoutMillis = 200L, systemClock = systemClock)
+
+ @After
+ fun cleanUp() {
+ underTest.clear()
+ }
+
+ @Test
+ fun fetch_isOnlyCalledOncePerEntry() {
+ val fetchList = mutableListOf<String>()
+ val fetch = { key: String ->
+ fetchList.add(key)
+ key
+ }
+
+ // Construct the cache and make sure fetch is called
+ assertThat(underTest.getOrFetch(A, fetch)).isEqualTo(A)
+ assertThat(underTest.getOrFetch(B, fetch)).isEqualTo(B)
+ assertThat(underTest.getOrFetch(C, fetch)).isEqualTo(C)
+ assertThat(fetchList).containsExactly(A, B, C).inOrder()
+
+ // Verify that further calls don't trigger fetch again
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(B, fetch)
+ underTest.getOrFetch(C, fetch)
+ assertThat(fetchList).containsExactly(A, B, C).inOrder()
+
+ // Verify that fetch gets called again if the entries are cleared
+ underTest.clear()
+ underTest.getOrFetch(A, fetch)
+ assertThat(fetchList).containsExactly(A, B, C, A).inOrder()
+ }
+
+ @Test
+ fun purge_beforeTimeout_doesNothing() {
+ // Populate cache
+ val fetch = { key: String -> key }
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(B, fetch)
+ underTest.getOrFetch(C, fetch)
+
+ // B starts off with ♥ ︎♥︎
+ assertThat(underTest.getLives(B)).isEqualTo(2)
+ // First purge run removes a ︎♥︎
+ underTest.purge(listOf(A, C))
+ assertNotNull(underTest.cache[B])
+ assertThat(underTest.getLives(B)).isEqualTo(1)
+ // Second purge run done too early does nothing to B
+ systemClock.advanceTime(100L)
+ underTest.purge(listOf(A, C))
+ assertNotNull(underTest.cache[B])
+ assertThat(underTest.getLives(B)).isEqualTo(1)
+ // Purge done after timeout (200ms) clears B
+ systemClock.advanceTime(100L)
+ underTest.purge(listOf(A, C))
+ assertNull(underTest.cache[B])
+ }
+
+ @Test
+ fun get_resetsLives() {
+ // Populate cache
+ val fetch = { key: String -> key }
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(B, fetch)
+ underTest.getOrFetch(C, fetch)
+
+ // Bring B down to one ︎♥︎
+ underTest.purge(listOf(A, C))
+ assertThat(underTest.getLives(B)).isEqualTo(1)
+
+ // Get should restore B to ♥ ︎♥︎
+ underTest.getOrFetch(B, fetch)
+ assertThat(underTest.getLives(B)).isEqualTo(2)
+
+ // Subsequent purge should remove a life regardless of timing
+ underTest.purge(listOf(A, C))
+ assertThat(underTest.getLives(B)).isEqualTo(1)
+ }
+
+ @Test
+ fun purge_resetsLives() {
+ // Populate cache
+ val fetch = { key: String -> key }
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(B, fetch)
+ underTest.getOrFetch(C, fetch)
+
+ // Bring B down to one ︎♥︎
+ underTest.purge(listOf(A, C))
+ assertThat(underTest.getLives(B)).isEqualTo(1)
+
+ // When B is back to wantedKeys, it is restored to to ♥ ︎♥ ︎︎
+ underTest.purge(listOf(B))
+ assertThat(underTest.getLives(B)).isEqualTo(2)
+ assertThat(underTest.getLives(A)).isEqualTo(1)
+ assertThat(underTest.getLives(C)).isEqualTo(1)
+
+ // Subsequent purge should remove a life regardless of timing
+ underTest.purge(listOf(A, C))
+ assertThat(underTest.getLives(B)).isEqualTo(1)
+ }
+
+ @Test
+ fun purge_worksWithMoreLives() {
+ val multiLivesCache =
+ NotifCollectionCache<String>(
+ retainCount = 3,
+ purgeTimeoutMillis = 100L,
+ systemClock = systemClock,
+ )
+
+ // Populate cache
+ val fetch = { key: String -> key }
+ multiLivesCache.getOrFetch(A, fetch)
+ multiLivesCache.getOrFetch(B, fetch)
+ multiLivesCache.getOrFetch(C, fetch)
+
+ // B starts off with ♥ ︎♥︎ ♥ ︎♥︎
+ assertThat(multiLivesCache.getLives(B)).isEqualTo(4)
+ // First purge run removes a ︎♥︎
+ multiLivesCache.purge(listOf(A, C))
+ assertNotNull(multiLivesCache.cache[B])
+ assertThat(multiLivesCache.getLives(B)).isEqualTo(3)
+ // Second purge run done too early does nothing to B
+ multiLivesCache.purge(listOf(A, C))
+ assertNotNull(multiLivesCache.cache[B])
+ assertThat(multiLivesCache.getLives(B)).isEqualTo(3)
+ // Staggered purge runs remove further ︎♥︎
+ systemClock.advanceTime(100L)
+ multiLivesCache.purge(listOf(A, C))
+ assertNotNull(multiLivesCache.cache[B])
+ assertThat(multiLivesCache.getLives(B)).isEqualTo(2)
+ systemClock.advanceTime(100L)
+ multiLivesCache.purge(listOf(A, C))
+ assertNotNull(multiLivesCache.cache[B])
+ assertThat(multiLivesCache.getLives(B)).isEqualTo(1)
+ systemClock.advanceTime(100L)
+ multiLivesCache.purge(listOf(A, C))
+ assertNull(multiLivesCache.cache[B])
+ }
+
+ @Test
+ fun purge_worksWithNoLives() {
+ val noLivesCache =
+ NotifCollectionCache<String>(
+ retainCount = 0,
+ purgeTimeoutMillis = 0L,
+ systemClock = systemClock,
+ )
+
+ val fetch = { key: String -> key }
+ noLivesCache.getOrFetch(A, fetch)
+ noLivesCache.getOrFetch(B, fetch)
+ noLivesCache.getOrFetch(C, fetch)
+
+ // Purge immediately removes entry
+ noLivesCache.purge(listOf(A, C))
+
+ assertNotNull(noLivesCache.cache[A])
+ assertNull(noLivesCache.cache[B])
+ assertNotNull(noLivesCache.cache[C])
+ }
+
+ @Test
+ fun hitsAndMisses_areAccurate() {
+ val fetch = { key: String -> key }
+
+ // Construct the cache
+ assertThat(underTest.getOrFetch(A, fetch)).isEqualTo(A)
+ assertThat(underTest.getOrFetch(B, fetch)).isEqualTo(B)
+ assertThat(underTest.getOrFetch(C, fetch)).isEqualTo(C)
+ assertThat(underTest.hits.get()).isEqualTo(0)
+ assertThat(underTest.misses.get()).isEqualTo(3)
+
+ // Verify that further calls count as hits
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(A, fetch)
+ underTest.getOrFetch(B, fetch)
+ underTest.getOrFetch(C, fetch)
+ assertThat(underTest.hits.get()).isEqualTo(4)
+ assertThat(underTest.misses.get()).isEqualTo(3)
+
+ // Verify that a miss is counted again if the entries are cleared
+ underTest.clear()
+ underTest.getOrFetch(A, fetch)
+ assertThat(underTest.hits.get()).isEqualTo(4)
+ assertThat(underTest.misses.get()).isEqualTo(4)
+ }
+
+ private fun <V> NotifCollectionCache<V>.getLives(key: String) = this.cache[key]?.lives
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
index 9d990b1..9a6a699 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -26,6 +26,7 @@
import com.android.internal.widget.MessagingGroup
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLinearLayout
+import com.android.internal.widget.NotificationRowIconView
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationTestHelper
@@ -90,7 +91,7 @@
private fun fakeConversationLayout(
mockDrawableGroupMessage: AnimatedImageDrawable,
- mockDrawableImageMessage: AnimatedImageDrawable
+ mockDrawableImageMessage: AnimatedImageDrawable,
): View {
val mockMessagingImageMessage: MessagingImageMessage =
mock<MessagingImageMessage>().apply {
@@ -126,6 +127,7 @@
whenever(requireViewById<CachingIconView>(R.id.conversation_icon))
.thenReturn(mock())
whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock())
+ whenever(requireViewById<NotificationRowIconView>(R.id.icon)).thenReturn(mock())
whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock())
whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock())
whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
index 286f017..dbe90a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt
@@ -20,6 +20,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_FRACTAL
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_SIMPLE
import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_SPARKLE
import org.junit.Test
import org.junit.runner.RunWith
@@ -28,20 +29,23 @@
@RunWith(AndroidJUnit4::class)
class TurbulenceNoiseShaderTest : SysuiTestCase() {
- private lateinit var turbulenceNoiseShader: TurbulenceNoiseShader
-
@Test
fun compilesSimplexNoise() {
- turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE)
+ TurbulenceNoiseShader(baseType = SIMPLEX_NOISE)
+ }
+
+ @Test
+ fun compilesSimplexSimpleNoise() {
+ TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SIMPLE)
}
@Test
fun compilesFractalNoise() {
- turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_FRACTAL)
+ TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_FRACTAL)
}
@Test
fun compilesSparkleNoise() {
- turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SPARKLE)
+ TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SPARKLE)
}
}
diff --git a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
deleted file mode 100644
index 47fd78a..0000000
--- a/packages/SystemUI/res/anim/shortcut_helper_close_anim.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2024 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:interpolator="@android:anim/accelerate_interpolator"
- android:zAdjustment="top">
-
- <translate
- android:fromYDelta="0"
- android:toYDelta="100%"
- android:duration="@android:integer/config_shortAnimTime" />
-</set>
diff --git a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml b/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
deleted file mode 100644
index 77edf58..0000000
--- a/packages/SystemUI/res/anim/shortcut_helper_launch_anim.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2024 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<!-- Animation for when a dock window at the bottom of the screen is entering. -->
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:interpolator="@android:anim/accelerate_decelerate_interpolator"
- android:zAdjustment="top">
-
- <translate android:fromYDelta="100%"
- android:toYDelta="0"
- android:startOffset="@android:integer/config_shortAnimTime"
- android:duration="@android:integer/config_mediumAnimTime"/>
-</set>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 17ba2e5..d5d52e3 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -59,8 +59,4 @@
-->
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
-
- <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
- <item name="android:windowLightNavigationBar">false</item>
- </style>
</resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 7cebac2..bda3453 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1709,32 +1709,4 @@
<style name="Theme.SystemUI.Dialog.StickyKeys" parent="@style/Theme.SystemUI.Dialog">
<item name="android:colorBackground">@color/transparent</item>
</style>
-
- <style name="ShortcutHelperBottomSheet" parent="@style/Widget.Material3.BottomSheet">
- <item name="backgroundTint">?colorSurfaceContainer</item>
- </style>
-
- <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
- <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
- <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
- <item name="android:activityOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
- <item name="android:taskOpenExitAnimation">@anim/shortcut_helper_close_anim</item>
- </style>
-
- <style name="ShortcutHelperThemeCommon" parent="@style/Theme.Material3.DynamicColors.DayNight">
- <item name="android:windowAnimationStyle">@style/ShortcutHelperAnimation</item>
- <item name="android:windowIsTranslucent">true</item>
- <item name="android:windowNoTitle">true</item>
- <item name="android:windowBackground">@android:color/transparent</item>
- <item name="android:backgroundDimEnabled">true</item>
- <item name="android:statusBarColor">@android:color/transparent</item>
- <item name="android:windowContentOverlay">@null</item>
- <item name="android:navigationBarColor">@android:color/transparent</item>
- <item name="android:windowLayoutInDisplayCutoutMode">always</item>
- <item name="enableEdgeToEdge">true</item>
- </style>
-
- <style name="ShortcutHelperTheme" parent="@style/ShortcutHelperThemeCommon">
- <item name="android:windowLightNavigationBar">true</item>
- </style>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
index 6cee28b..0c29466 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionLoader.kt
@@ -22,15 +22,18 @@
import android.graphics.RectF
import android.util.PathParser
import com.android.systemui.res.R
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlin.math.roundToInt
interface CameraProtectionLoader {
fun loadCameraProtectionInfoList(): List<CameraProtectionInfo>
}
-class CameraProtectionLoaderImpl @Inject constructor(private val context: Context) :
- CameraProtectionLoader {
+class CameraProtectionLoaderImpl
+@AssistedInject
+constructor(@Assisted private val context: Context) : CameraProtectionLoader {
override fun loadCameraProtectionInfoList(): List<CameraProtectionInfo> {
val list = mutableListOf<CameraProtectionInfo>()
@@ -76,7 +79,7 @@
computed.left.roundToInt(),
computed.top.roundToInt(),
computed.right.roundToInt(),
- computed.bottom.roundToInt()
+ computed.bottom.roundToInt(),
)
val displayUniqueId = context.getString(displayUniqueIdRes)
return CameraProtectionInfo(
@@ -84,7 +87,7 @@
physicalCameraId,
protectionPath,
protectionBounds,
- displayUniqueId
+ displayUniqueId,
)
}
@@ -95,4 +98,9 @@
throw IllegalArgumentException("Invalid protection path", e)
}
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(context: Context): CameraProtectionLoaderImpl
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
index 58680a8..442a1e4 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraProtectionModule.kt
@@ -16,11 +16,20 @@
package com.android.systemui
-import dagger.Binds
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
import dagger.Module
+import dagger.Provides
@Module
-interface CameraProtectionModule {
+object CameraProtectionModule {
- @Binds fun cameraProtectionLoaderImpl(impl: CameraProtectionLoaderImpl): CameraProtectionLoader
+ @Provides
+ @SysUISingleton
+ fun cameraProtectionLoader(
+ factory: CameraProtectionLoaderImpl.Factory,
+ context: Context,
+ ): CameraProtectionLoader {
+ return factory.create(context)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
index 044312b..6fc50fb 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt
@@ -16,9 +16,14 @@
package com.android.systemui
+import android.content.Context
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.decor.FaceScanningProviderFactory
+import com.android.systemui.decor.FaceScanningProviderFactoryImpl
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import dagger.Binds
import dagger.Module
+import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import dagger.multibindings.IntoSet
@@ -35,4 +40,15 @@
@Binds
@IntoSet
fun bindScreenDecorationsConfigListener(impl: ScreenDecorations): ConfigurationListener
+
+ companion object {
+ @Provides
+ @SysUISingleton
+ fun faceScanningProviderFactory(
+ creator: FaceScanningProviderFactoryImpl.Creator,
+ context: Context,
+ ): FaceScanningProviderFactory {
+ return creator.create(context)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
index 7309599..b4cb103 100644
--- a/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/SysUICutoutProvider.kt
@@ -21,21 +21,12 @@
import android.util.RotationUtils
import android.view.Display
import android.view.DisplayCutout
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.naturalBounds
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
-@SysUISingleton
-class SysUICutoutProvider
-@Inject
-constructor(
- private val context: Context,
- private val cameraProtectionLoader: CameraProtectionLoader,
-) {
-
- private val cameraProtectionList by lazy {
- cameraProtectionLoader.loadCameraProtectionInfoList()
- }
+interface SysUICutoutProvider {
/**
* Returns the [SysUICutoutInformation] for the current display and the current rotation.
@@ -43,7 +34,21 @@
* This means that the bounds of the display cutout and the camera protection will be
* adjusted/rotated for the current rotation.
*/
- fun cutoutInfoForCurrentDisplayAndRotation(): SysUICutoutInformation? {
+ fun cutoutInfoForCurrentDisplayAndRotation(): SysUICutoutInformation?
+}
+
+class SysUICutoutProviderImpl
+@AssistedInject
+constructor(
+ @Assisted private val context: Context,
+ @Assisted private val cameraProtectionLoader: CameraProtectionLoader,
+) : SysUICutoutProvider {
+
+ private val cameraProtectionList by lazy {
+ cameraProtectionLoader.loadCameraProtectionInfoList()
+ }
+
+ override fun cutoutInfoForCurrentDisplayAndRotation(): SysUICutoutInformation? {
val display = context.display
val displayCutout: DisplayCutout = display.cutout ?: return null
return SysUICutoutInformation(displayCutout, getCameraProtectionForDisplay(display))
@@ -72,8 +77,16 @@
/* inOutBounds = */ rotatedBoundsOut,
/* parentWidth = */ displayNaturalBounds.width(),
/* parentHeight = */ displayNaturalBounds.height(),
- /* rotation = */ display.rotation
+ /* rotation = */ display.rotation,
)
return rotatedBoundsOut
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ context: Context,
+ cameraProtectionLoader: CameraProtectionLoader,
+ ): SysUICutoutProviderImpl
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
index 50970a5..8b5fde3 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
@@ -55,11 +55,7 @@
settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
@Background backgroundDispatcher: CoroutineDispatcher,
): AudioSharingRepository =
- if (
- Flags.enableLeAudioSharing() &&
- Flags.audioSharingQsDialogImprovement() &&
- localBluetoothManager != null
- ) {
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
AudioSharingRepositoryImpl(
localBluetoothManager,
settingsLibAudioSharingRepository,
diff --git a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt
index cbed21c..bfd6b5b 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/CutoutDecorProviderFactory.kt
@@ -22,18 +22,15 @@
import android.view.DisplayCutout
import android.view.DisplayInfo
-class CutoutDecorProviderFactory constructor(
- private val res: Resources,
- private val display: Display?,
-) : DecorProviderFactory() {
+class CutoutDecorProviderFactory(private val res: Resources, private val display: Display?) :
+ DecorProviderFactory {
val displayInfo = DisplayInfo()
override val hasProviders: Boolean
get() {
- display?.getDisplayInfo(displayInfo) ?: run {
- Log.w(TAG, "display is null, can't update displayInfo")
- }
+ display?.getDisplayInfo(displayInfo)
+ ?: run { Log.w(TAG, "display is null, can't update displayInfo") }
return DisplayCutout.getFillBuiltInDisplayCutout(res, displayInfo.uniqueId)
}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt
index c60cad8..16e73f5 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/DecorProviderFactory.kt
@@ -16,7 +16,7 @@
package com.android.systemui.decor
-abstract class DecorProviderFactory {
- abstract val providers: List<DecorProvider>
- abstract val hasProviders: Boolean
-}
\ No newline at end of file
+interface DecorProviderFactory {
+ val providers: List<DecorProvider>
+ val hasProviders: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index 3bc4f34..88580cf 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -33,23 +33,32 @@
import com.android.systemui.FaceScanningOverlay
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import java.util.concurrent.Executor
-import javax.inject.Inject
-@SysUISingleton
-class FaceScanningProviderFactory @Inject constructor(
- private val authController: AuthController,
- private val context: Context,
- private val statusBarStateController: StatusBarStateController,
- private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- @Main private val mainExecutor: Executor,
- private val logger: ScreenDecorationsLogger,
- private val facePropertyRepository: FacePropertyRepository,
-) : DecorProviderFactory() {
+interface FaceScanningProviderFactory : DecorProviderFactory {
+
+ fun canShowFaceScanningAnim(): Boolean
+
+ fun shouldShowFaceScanningAnim(): Boolean
+}
+
+class FaceScanningProviderFactoryImpl
+@AssistedInject
+constructor(
+ private val authController: AuthController,
+ @Assisted private val context: Context,
+ private val statusBarStateController: StatusBarStateController,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ @Main private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
+ private val facePropertyRepository: FacePropertyRepository,
+) : FaceScanningProviderFactory {
private val display = context.display
private val displayInfo = DisplayInfo()
@@ -60,11 +69,12 @@
}
// update display info
- display?.getDisplayInfo(displayInfo) ?: run {
- Log.w(TAG, "display is null, can't update displayInfo")
- }
+ display?.getDisplayInfo(displayInfo)
+ ?: run { Log.w(TAG, "display is null, can't update displayInfo") }
return DisplayCutout.getFillBuiltInDisplayCutout(
- context.resources, displayInfo.uniqueId)
+ context.resources,
+ displayInfo.uniqueId,
+ )
}
override val providers: List<DecorProvider>
@@ -81,39 +91,45 @@
// Cutout drawing is updated in ScreenDecorations#updateCutout
for (bound in bounds) {
list.add(
- FaceScanningOverlayProviderImpl(
- bound.baseOnRotation0(displayInfo.rotation),
- authController,
- statusBarStateController,
- keyguardUpdateMonitor,
- mainExecutor,
- logger,
- facePropertyRepository,
- )
+ FaceScanningOverlayProviderImpl(
+ bound.baseOnRotation0(displayInfo.rotation),
+ authController,
+ statusBarStateController,
+ keyguardUpdateMonitor,
+ mainExecutor,
+ logger,
+ facePropertyRepository,
+ )
)
}
}
}
}
- fun canShowFaceScanningAnim(): Boolean {
+ override fun canShowFaceScanningAnim(): Boolean {
return hasProviders && keyguardUpdateMonitor.isFaceEnabledAndEnrolled
}
- fun shouldShowFaceScanningAnim(): Boolean {
+ override fun shouldShowFaceScanningAnim(): Boolean {
return canShowFaceScanningAnim() &&
- (keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing)
+ (keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing)
+ }
+
+ // Using the name "Creator" so that it doesn't become "...FactoryFactory".
+ @AssistedFactory
+ interface Creator {
+ fun create(context: Context): FaceScanningProviderFactoryImpl
}
}
class FaceScanningOverlayProviderImpl(
- override val alignedBound: Int,
- private val authController: AuthController,
- private val statusBarStateController: StatusBarStateController,
- private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val mainExecutor: Executor,
- private val logger: ScreenDecorationsLogger,
- private val facePropertyRepository: FacePropertyRepository,
+ override val alignedBound: Int,
+ private val authController: AuthController,
+ private val statusBarStateController: StatusBarStateController,
+ private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+ private val mainExecutor: Executor,
+ private val logger: ScreenDecorationsLogger,
+ private val facePropertyRepository: FacePropertyRepository,
) : BoundDecorProvider() {
override val viewId: Int = com.android.systemui.res.R.id.face_scanning_anim
@@ -122,7 +138,7 @@
reloadToken: Int,
@Surface.Rotation rotation: Int,
tintColor: Int,
- displayUniqueId: String?
+ displayUniqueId: String?,
) {
(view.layoutParams as FrameLayout.LayoutParams).let {
updateLayoutParams(it, rotation)
@@ -138,9 +154,10 @@
context: Context,
parent: ViewGroup,
@Surface.Rotation rotation: Int,
- tintColor: Int
+ tintColor: Int,
): View {
- val view = FaceScanningOverlay(
+ val view =
+ FaceScanningOverlay(
context,
alignedBound,
statusBarStateController,
@@ -148,43 +165,46 @@
mainExecutor,
logger,
authController,
- )
+ )
view.id = viewId
view.setColor(tintColor)
- FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT).let {
- updateLayoutParams(it, rotation)
- parent.addView(view, it)
- }
+ FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ )
+ .let {
+ updateLayoutParams(it, rotation)
+ parent.addView(view, it)
+ }
return view
}
private fun updateLayoutParams(
layoutParams: FrameLayout.LayoutParams,
- @Surface.Rotation rotation: Int
+ @Surface.Rotation rotation: Int,
) {
layoutParams.let { lp ->
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
lp.height = ViewGroup.LayoutParams.MATCH_PARENT
logger.faceSensorLocation(facePropertyRepository.sensorLocation.value)
- facePropertyRepository.sensorLocation.value?.y?.let {
- faceAuthSensorHeight ->
+ facePropertyRepository.sensorLocation.value?.y?.let { faceAuthSensorHeight ->
val faceScanningHeight = (faceAuthSensorHeight * 2)
when (rotation) {
- Surface.ROTATION_0, Surface.ROTATION_180 ->
- lp.height = faceScanningHeight
- Surface.ROTATION_90, Surface.ROTATION_270 ->
- lp.width = faceScanningHeight
+ Surface.ROTATION_0,
+ Surface.ROTATION_180 -> lp.height = faceScanningHeight
+ Surface.ROTATION_90,
+ Surface.ROTATION_270 -> lp.width = faceScanningHeight
}
}
- lp.gravity = when (rotation) {
- Surface.ROTATION_0 -> Gravity.TOP or Gravity.START
- Surface.ROTATION_90 -> Gravity.LEFT or Gravity.START
- Surface.ROTATION_180 -> Gravity.BOTTOM or Gravity.END
- Surface.ROTATION_270 -> Gravity.RIGHT or Gravity.END
- else -> -1 /* invalid rotation */
- }
+ lp.gravity =
+ when (rotation) {
+ Surface.ROTATION_0 -> Gravity.TOP or Gravity.START
+ Surface.ROTATION_90 -> Gravity.LEFT or Gravity.START
+ Surface.ROTATION_180 -> Gravity.BOTTOM or Gravity.END
+ Surface.ROTATION_270 -> Gravity.RIGHT or Gravity.END
+ else -> -1 /* invalid rotation */
+ }
}
}
}
@@ -209,24 +229,27 @@
fun Int.baseOnRotation0(@DisplayCutout.BoundsPosition currentRotation: Int): Int {
return when (currentRotation) {
Surface.ROTATION_0 -> this
- Surface.ROTATION_90 -> when (this) {
- BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_TOP
- BOUNDS_POSITION_TOP -> BOUNDS_POSITION_RIGHT
- BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_BOTTOM
- else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_LEFT
- }
- Surface.ROTATION_270 -> when (this) {
- BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_BOTTOM
- BOUNDS_POSITION_TOP -> BOUNDS_POSITION_LEFT
- BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_TOP
- else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_RIGHT
- }
- else /* Surface.ROTATION_180 */ -> when (this) {
- BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_RIGHT
- BOUNDS_POSITION_TOP -> BOUNDS_POSITION_BOTTOM
- BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_LEFT
- else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_TOP
- }
+ Surface.ROTATION_90 ->
+ when (this) {
+ BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_TOP
+ BOUNDS_POSITION_TOP -> BOUNDS_POSITION_RIGHT
+ BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_BOTTOM
+ else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_LEFT
+ }
+ Surface.ROTATION_270 ->
+ when (this) {
+ BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_BOTTOM
+ BOUNDS_POSITION_TOP -> BOUNDS_POSITION_LEFT
+ BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_TOP
+ else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_RIGHT
+ }
+ else /* Surface.ROTATION_180 */ ->
+ when (this) {
+ BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_RIGHT
+ BOUNDS_POSITION_TOP -> BOUNDS_POSITION_BOTTOM
+ BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_LEFT
+ else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_TOP
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
index 14ecc66..9aa7fd1 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt
@@ -23,19 +23,18 @@
import android.view.Surface
import android.view.View
import android.view.ViewGroup
-import com.android.systemui.res.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
import javax.inject.Inject
/**
- * Provides privacy dot views for each orientation. The PrivacyDot orientation and visibility
- * of the privacy dot views are controlled by the PrivacyDotViewController.
+ * Provides privacy dot views for each orientation. The PrivacyDot orientation and visibility of the
+ * privacy dot views are controlled by the PrivacyDotViewController.
*/
@SysUISingleton
-open class PrivacyDotDecorProviderFactory @Inject constructor(
- @Main private val res: Resources
-) : DecorProviderFactory() {
+open class PrivacyDotDecorProviderFactory @Inject constructor(@Main private val res: Resources) :
+ DecorProviderFactory {
private val isPrivacyDotEnabled: Boolean
get() = res.getBoolean(R.bool.config_enablePrivacyDot)
@@ -51,22 +50,26 @@
viewId = R.id.privacy_dot_top_left_container,
alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
- layoutId = R.layout.privacy_dot_top_left),
+ layoutId = R.layout.privacy_dot_top_left,
+ ),
PrivacyDotCornerDecorProviderImpl(
viewId = R.id.privacy_dot_top_right_container,
alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
- layoutId = R.layout.privacy_dot_top_right),
+ layoutId = R.layout.privacy_dot_top_right,
+ ),
PrivacyDotCornerDecorProviderImpl(
viewId = R.id.privacy_dot_bottom_left_container,
alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
- layoutId = R.layout.privacy_dot_bottom_left),
+ layoutId = R.layout.privacy_dot_bottom_left,
+ ),
PrivacyDotCornerDecorProviderImpl(
viewId = R.id.privacy_dot_bottom_right_container,
alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
- layoutId = R.layout.privacy_dot_bottom_right)
+ layoutId = R.layout.privacy_dot_bottom_right,
+ ),
)
} else {
emptyList()
@@ -78,7 +81,7 @@
override val viewId: Int,
@DisplayCutout.BoundsPosition override val alignedBound1: Int,
@DisplayCutout.BoundsPosition override val alignedBound2: Int,
- private val layoutId: Int
+ private val layoutId: Int,
) : CornerDecorProvider() {
override fun onReloadResAndMeasure(
@@ -86,7 +89,7 @@
reloadToken: Int,
rotation: Int,
tintColor: Int,
- displayUniqueId: String?
+ displayUniqueId: String?,
) {
// Do nothing here because it is handled inside PrivacyDotViewController
}
@@ -95,7 +98,7 @@
context: Context,
parent: ViewGroup,
@Surface.Rotation rotation: Int,
- tintColor: Int
+ tintColor: Int,
): View {
LayoutInflater.from(context).inflate(layoutId, parent, true)
return parent.getChildAt(parent.childCount - 1 /* latest new added child */)
diff --git a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt
index 2f2c952f..39fd551 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/RoundedCornerDecorProviderFactory.kt
@@ -21,65 +21,74 @@
class RoundedCornerDecorProviderFactory(
private val roundedCornerResDelegate: RoundedCornerResDelegate
-) : DecorProviderFactory() {
+) : DecorProviderFactory {
override val hasProviders: Boolean
- get() = roundedCornerResDelegate.run {
- hasTop || hasBottom
- }
+ get() = roundedCornerResDelegate.run { hasTop || hasBottom }
override val providers: List<DecorProvider>
- get() {
- val hasTop = roundedCornerResDelegate.hasTop
- val hasBottom = roundedCornerResDelegate.hasBottom
- return when {
- hasTop && hasBottom -> listOf(
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_top_left,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
- roundedCornerResDelegate = roundedCornerResDelegate),
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_top_right,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
- roundedCornerResDelegate = roundedCornerResDelegate),
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_bottom_left,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
- roundedCornerResDelegate = roundedCornerResDelegate),
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_bottom_right,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
- roundedCornerResDelegate = roundedCornerResDelegate)
- )
- hasTop -> listOf(
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_top_left,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
- roundedCornerResDelegate = roundedCornerResDelegate),
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_top_right,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
- roundedCornerResDelegate = roundedCornerResDelegate)
- )
- hasBottom -> listOf(
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_bottom_left,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
- roundedCornerResDelegate = roundedCornerResDelegate),
- RoundedCornerDecorProviderImpl(
- viewId = R.id.rounded_corner_bottom_right,
- alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
- alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
- roundedCornerResDelegate = roundedCornerResDelegate)
- )
- else -> emptyList()
+ get() {
+ val hasTop = roundedCornerResDelegate.hasTop
+ val hasBottom = roundedCornerResDelegate.hasBottom
+ return when {
+ hasTop && hasBottom ->
+ listOf(
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_top_left,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_top_right,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_bottom_left,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_bottom_right,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ )
+ hasTop ->
+ listOf(
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_top_left,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_top_right,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_TOP,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ )
+ hasBottom ->
+ listOf(
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_bottom_left,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_LEFT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ RoundedCornerDecorProviderImpl(
+ viewId = R.id.rounded_corner_bottom_right,
+ alignedBound1 = DisplayCutout.BOUNDS_POSITION_BOTTOM,
+ alignedBound2 = DisplayCutout.BOUNDS_POSITION_RIGHT,
+ roundedCornerResDelegate = roundedCornerResDelegate,
+ ),
+ )
+ else -> emptyList()
+ }
}
- }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
index 906f600..7b3380a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.keyboard.shortcut
-import android.app.Activity
import com.android.systemui.CoreStartable
import com.android.systemui.Flags.keyboardShortcutHelperRewrite
import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository
@@ -31,8 +30,7 @@
import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts
import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts
import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts
-import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
-import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
+import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperDialogStarter
import dagger.Binds
import dagger.Lazy
import dagger.Module
@@ -44,11 +42,6 @@
interface ShortcutHelperModule {
@Binds
- @IntoMap
- @ClassKey(ShortcutHelperActivity::class)
- fun activity(impl: ShortcutHelperActivity): Activity
-
- @Binds
@SystemShortcuts
fun systemShortcutsSource(impl: SystemShortcutsSource): KeyboardShortcutGroupsSource
@@ -73,8 +66,8 @@
companion object {
@Provides
@IntoMap
- @ClassKey(ShortcutHelperActivityStarter::class)
- fun starter(implLazy: Lazy<ShortcutHelperActivityStarter>): CoreStartable {
+ @ClassKey(ShortcutHelperDialogStarter::class)
+ fun starter(implLazy: Lazy<ShortcutHelperDialogStarter>): CoreStartable {
return if (keyboardShortcutHelperRewrite()) {
implLazy.get()
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
deleted file mode 100644
index fbf52e7..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarter.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyboard.shortcut.ui
-
-import android.content.Context
-import android.content.Intent
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyboard.shortcut.ui.view.ShortcutHelperActivity
-import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class ShortcutHelperActivityStarter(
- private val context: Context,
- @Application private val applicationScope: CoroutineScope,
- private val viewModel: ShortcutHelperViewModel,
- private val startActivity: (Intent) -> Unit,
-) : CoreStartable {
-
- @Inject
- constructor(
- context: Context,
- @Application applicationScope: CoroutineScope,
- viewModel: ShortcutHelperViewModel,
- ) : this(
- context,
- applicationScope,
- viewModel,
- startActivity = { intent -> context.startActivity(intent) }
- )
-
- override fun start() {
- applicationScope.launch {
- viewModel.shouldShow.collect { shouldShow ->
- if (shouldShow) {
- startShortcutHelperActivity()
- }
- }
- }
- }
-
- private fun startShortcutHelperActivity() {
- startActivity(
- Intent(context, ShortcutHelperActivity::class.java)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- )
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
new file mode 100644
index 0000000..d33ab2a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarter.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.shortcut.ui
+
+import android.app.Dialog
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
+import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelperBottomSheet
+import com.android.systemui.keyboard.shortcut.ui.composable.getWidth
+import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.createBottomSheet
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class ShortcutHelperDialogStarter
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val viewModel: ShortcutHelperViewModel,
+ private val dialogFactory: SystemUIDialogFactory,
+ private val activityStarter: ActivityStarter,
+) : CoreStartable {
+
+ @VisibleForTesting var dialog: Dialog? = null
+
+ override fun start() {
+ viewModel.shouldShow
+ .map { shouldShow ->
+ if (shouldShow) {
+ dialog = createShortcutHelperDialog().also { it.show() }
+ } else {
+ dialog?.dismiss()
+ }
+ }
+ .launchIn(applicationScope)
+ }
+
+ private fun createShortcutHelperDialog(): Dialog {
+ return dialogFactory.createBottomSheet(
+ content = { dialog ->
+ val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
+ ShortcutHelper(
+ modifier = Modifier.width(getWidth()),
+ shortcutsUiState = shortcutsUiState,
+ onKeyboardSettingsClicked = { onKeyboardSettingsClicked(dialog) },
+ onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
+ )
+ dialog.setOnDismissListener { viewModel.onViewClosed() }
+ },
+ maxWidth = ShortcutHelperBottomSheet.LargeScreenWidthLandscape
+ )
+ }
+
+ private fun onKeyboardSettingsClicked(dialog: Dialog) {
+ try {
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK),
+ /* dismissShade= */ true,
+ /* animationController = */ null,
+ /* showOverLockscreenWhenLocked = */ false,
+ UserHandle.CURRENT,
+ )
+ } catch (e: ActivityNotFoundException) {
+ // From the Settings docs: In some cases, a matching Activity may not exist, so ensure
+ // you safeguard against this.
+ e.printStackTrace()
+ return
+ }
+ dialog.dismiss()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
index 1f0d696..e295564 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelperUtils.kt
@@ -16,9 +16,13 @@
package com.android.systemui.keyboard.shortcut.ui.composable
+import android.content.res.Configuration
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
import com.android.compose.windowsizeclass.LocalWindowSizeClass
/**
@@ -29,3 +33,21 @@
fun hasCompactWindowSize() =
LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact ||
LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
+
+@Composable
+fun getWidth(): Dp {
+ return if (hasCompactWindowSize()) {
+ ShortcutHelperBottomSheet.DefaultWidth
+ } else
+ when (LocalConfiguration.current.orientation) {
+ Configuration.ORIENTATION_LANDSCAPE ->
+ ShortcutHelperBottomSheet.LargeScreenWidthLandscape
+ else -> ShortcutHelperBottomSheet.LargeScreenWidthPortrait
+ }
+}
+
+object ShortcutHelperBottomSheet {
+ val DefaultWidth = 412.dp
+ val LargeScreenWidthPortrait = 704.dp
+ val LargeScreenWidthLandscape = 864.dp
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
deleted file mode 100644
index 52263ce..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyboard.shortcut.ui.view
-
-import android.content.ActivityNotFoundException
-import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.content.res.Configuration
-import android.os.Bundle
-import android.provider.Settings
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.Surface
-import androidx.compose.material3.rememberModalBottomSheetState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.key.Key
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.contentDescription
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.flowWithLifecycle
-import androidx.lifecycle.lifecycleScope
-import com.android.compose.theme.PlatformTheme
-import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
-import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
-import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
-import com.android.systemui.res.R
-import com.android.systemui.settings.UserTracker
-import javax.inject.Inject
-import kotlinx.coroutines.launch
-
-/**
- * Activity that hosts the new version of the keyboard shortcut helper. It will be used both for
- * small and large screen devices.
- */
-class ShortcutHelperActivity
-@Inject
-constructor(private val userTracker: UserTracker, private val viewModel: ShortcutHelperViewModel) :
- ComponentActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- setupEdgeToEdge()
- super.onCreate(savedInstanceState)
- setContent { Content() }
- observeFinishRequired()
- viewModel.onViewOpened()
- }
-
- @Composable
- private fun Content() {
- CompositionLocalProvider(LocalContext provides userTracker.userContext) {
- PlatformTheme { BottomSheet { finish() } }
- }
- }
-
- @OptIn(ExperimentalMaterial3Api::class)
- @Composable
- private fun BottomSheet(onDismiss: () -> Unit) {
- ModalBottomSheet(
- onDismissRequest = { onDismiss() },
- modifier =
- Modifier.width(getWidth()).padding(top = getTopPadding()).onKeyEvent {
- if (it.key == Key.Escape) {
- onDismiss()
- true
- } else false
- },
- sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
- dragHandle = { DragHandle() },
- ) {
- val shortcutsUiState by viewModel.shortcutsUiState.collectAsStateWithLifecycle()
- ShortcutHelper(
- shortcutsUiState = shortcutsUiState,
- onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
- onSearchQueryChanged = { viewModel.onSearchQueryChanged(it) },
- )
- }
- }
-
- @Composable
- fun DragHandle() {
- val dragHandleContentDescription =
- stringResource(id = R.string.shortcut_helper_content_description_drag_handle)
- Surface(
- modifier =
- Modifier.padding(top = 16.dp, bottom = 6.dp).semantics {
- contentDescription = dragHandleContentDescription
- },
- color = MaterialTheme.colorScheme.outlineVariant,
- shape = MaterialTheme.shapes.extraLarge,
- ) {
- Box(Modifier.size(width = 32.dp, height = 4.dp))
- }
- }
-
- private fun onKeyboardSettingsClicked() {
- try {
- startActivityAsUser(
- Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS).addFlags(FLAG_ACTIVITY_NEW_TASK),
- userTracker.userHandle,
- )
- } catch (e: ActivityNotFoundException) {
- // From the Settings docs: In some cases, a matching Activity may not exist, so ensure
- // you safeguard against this.
- e.printStackTrace()
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (isFinishing) {
- viewModel.onViewClosed()
- }
- }
-
- private fun observeFinishRequired() {
- lifecycleScope.launch {
- viewModel.shouldShow.flowWithLifecycle(lifecycle).collect { shouldShow ->
- if (!shouldShow) {
- finish()
- }
- }
- }
- }
-
- private fun setupEdgeToEdge() {
- // Draw behind system bars
- window.setDecorFitsSystemWindows(false)
- }
-
- @Composable
- private fun getTopPadding(): Dp {
- return if (hasCompactWindowSize()) DefaultTopPadding else LargeScreenTopPadding
- }
-
- @Composable
- private fun getWidth(): Dp {
- return if (hasCompactWindowSize()) {
- DefaultWidth
- } else
- when (LocalConfiguration.current.orientation) {
- Configuration.ORIENTATION_LANDSCAPE -> LargeScreenWidthLandscape
- else -> LargeScreenWidthPortrait
- }
- }
-
- companion object {
- private val DefaultTopPadding = 64.dp
- private val LargeScreenTopPadding = 72.dp
- private val DefaultWidth = 412.dp
- private val LargeScreenWidthPortrait = 704.dp
- private val LargeScreenWidthLandscape = 864.dp
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index 2961d05..742f435 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -170,12 +170,11 @@
// Set different layout for each device
if (device.isMutingExpectedDevice()
&& !mController.isCurrentConnectedDeviceRemote()) {
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateUnmutedVolumeIcon(device);
mCurrentActivePosition = position;
updateFullItemClickListener(v -> onItemClick(v, device));
setSingleLineLayout(getItemTitle(device));
- initFakeActiveDevice();
+ initFakeActiveDevice(device);
} else if (device.hasSubtext()) {
boolean isActiveWithOngoingSession =
(device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded(
@@ -184,8 +183,7 @@
&& isActiveWithOngoingSession;
if (isActiveWithOngoingSession) {
mCurrentActivePosition = position;
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateUnmutedVolumeIcon(device);
mSubTitleText.setText(device.getSubtextString());
updateTwoLineLayoutContentAlpha(DEVICE_CONNECTED_ALPHA);
updateEndClickAreaAsSessionEditing(device,
@@ -199,9 +197,7 @@
} else {
if (currentlyConnected) {
mCurrentActivePosition = position;
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
- initSeekbar(device, isCurrentSeekbarInvisible);
+ updateUnmutedVolumeIcon(device);
} else {
setUpDeviceIcon(device);
}
@@ -243,8 +239,7 @@
// selected device in group
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateUnmutedVolumeIcon(device);
updateGroupableCheckBox(true, isDeviceDeselectable, device);
updateEndClickArea(device, isDeviceDeselectable);
disableFocusPropertyForView(mContainerLayout);
@@ -264,8 +259,7 @@
setSingleLineLayout(getItemTitle(device));
} else if (device.hasOngoingSession()) {
mCurrentActivePosition = position;
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateUnmutedVolumeIcon(device);
updateEndClickAreaAsSessionEditing(device, device.isHostForOngoingSession()
? R.drawable.media_output_status_edit_session
: R.drawable.ic_sound_bars_anim);
@@ -278,8 +272,7 @@
&& !mController.getSelectableMediaDevice().isEmpty()) {
//If device is connected and there's other selectable devices, layout as
// one of selected devices.
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateUnmutedVolumeIcon(device);
boolean isDeviceDeselectable = isDeviceIncluded(
mController.getDeselectableMediaDevice(), device);
updateGroupableCheckBox(true, isDeviceDeselectable, device);
@@ -291,8 +284,7 @@
true /* showEndTouchArea */);
initSeekbar(device, isCurrentSeekbarInvisible);
} else {
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateUnmutedVolumeIcon(device);
disableFocusPropertyForView(mContainerLayout);
setUpContentDescriptionForView(mSeekBar, device);
mCurrentActivePosition = position;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 63a7e01..574ccee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -47,6 +47,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.res.R;
@@ -321,18 +322,20 @@
// Check if response volume match with the latest request, to ignore obsolete
// response
if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
- updateTitleIcon(currentVolume == 0 ? R.drawable.media_output_icon_volume_off
- : R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ if (currentVolume == 0) {
+ updateMutedVolumeIcon(device);
+ } else {
+ updateUnmutedVolumeIcon(device);
+ }
} else {
if (!mVolumeAnimator.isStarted()) {
int percentage =
(int) ((double) currentVolume * VOLUME_PERCENTAGE_SCALE_SIZE
/ (double) mSeekBar.getMax());
if (percentage == 0) {
- updateMutedVolumeIcon();
+ updateMutedVolumeIcon(device);
} else {
- updateUnmutedVolumeIcon();
+ updateUnmutedVolumeIcon(device);
}
mSeekBar.setVolume(currentVolume);
mLatestUpdateVolume = -1;
@@ -340,7 +343,7 @@
}
} else if (currentVolume == 0) {
mSeekBar.resetVolume();
- updateMutedVolumeIcon();
+ updateMutedVolumeIcon(device);
}
if (currentVolume == mLatestUpdateVolume) {
mLatestUpdateVolume = -1;
@@ -365,7 +368,7 @@
R.string.media_output_dialog_volume_percentage, percentage));
mVolumeValueText.setVisibility(View.VISIBLE);
if (mStartFromMute) {
- updateUnmutedVolumeIcon();
+ updateUnmutedVolumeIcon(device);
mStartFromMute = false;
}
if (progressToVolume != deviceVolume) {
@@ -390,9 +393,9 @@
seekBar.getProgress());
if (currentVolume == 0) {
seekBar.setProgress(0);
- updateMutedVolumeIcon();
+ updateMutedVolumeIcon(device);
} else {
- updateUnmutedVolumeIcon();
+ updateUnmutedVolumeIcon(device);
}
mTitleIcon.setVisibility(View.VISIBLE);
mVolumeValueText.setVisibility(View.GONE);
@@ -402,36 +405,48 @@
});
}
- void updateMutedVolumeIcon() {
+ void updateMutedVolumeIcon(MediaDevice device) {
mIconAreaLayout.setBackground(
mContext.getDrawable(R.drawable.media_output_item_background_active));
- updateTitleIcon(R.drawable.media_output_icon_volume_off,
- mController.getColorItemContent());
+ updateTitleIcon(device, true /* isMutedVolumeIcon */);
}
- void updateUnmutedVolumeIcon() {
+ void updateUnmutedVolumeIcon(MediaDevice device) {
mIconAreaLayout.setBackground(
mContext.getDrawable(R.drawable.media_output_title_icon_area)
);
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateTitleIcon(device, false /* isMutedVolumeIcon */);
}
- void updateTitleIcon(@DrawableRes int id, int color) {
+ void updateTitleIcon(MediaDevice device, boolean isMutedVolumeIcon) {
+ boolean isInputMediaDevice = device instanceof InputMediaDevice;
+ int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon);
mTitleIcon.setImageDrawable(mContext.getDrawable(id));
- mTitleIcon.setImageTintList(ColorStateList.valueOf(color));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(mController.getColorItemContent()));
mIconAreaLayout.setBackgroundTintList(
ColorStateList.valueOf(mController.getColorSeekbarProgress()));
}
+ @VisibleForTesting
+ int getDrawableId(boolean isInputDevice, boolean isMutedVolumeIcon) {
+ // Returns the microphone icon when the flag is enabled and the device is an input
+ // device.
+ if (com.android.media.flags.Flags.enableAudioInputDeviceRoutingAndVolumeControl()
+ && isInputDevice) {
+ return isMutedVolumeIcon ? R.drawable.ic_mic_off : R.drawable.ic_mic_26dp;
+ }
+ return isMutedVolumeIcon
+ ? R.drawable.media_output_icon_volume_off
+ : R.drawable.media_output_icon_volume;
+ }
+
void updateIconAreaClickListener(View.OnClickListener listener) {
mIconAreaLayout.setOnClickListener(listener);
}
- void initFakeActiveDevice() {
+ void initFakeActiveDevice(MediaDevice device) {
disableSeekBar();
- updateTitleIcon(R.drawable.media_output_icon_volume,
- mController.getColorItemContent());
+ updateTitleIcon(device, false /* isMutedIcon */);
final Drawable backgroundDrawable = mContext.getDrawable(
R.drawable.media_output_item_background_active)
.mutate();
@@ -518,13 +533,13 @@
mController.logInteractionUnmuteDevice(device);
mSeekBar.setVolume(UNMUTE_DEFAULT_VOLUME);
mController.adjustVolume(device, UNMUTE_DEFAULT_VOLUME);
- updateUnmutedVolumeIcon();
+ updateUnmutedVolumeIcon(device);
mIconAreaLayout.setOnTouchListener(((iconV, event) -> false));
} else {
mController.logInteractionMuteDevice(device);
mSeekBar.resetVolume();
mController.adjustVolume(device, 0);
- updateMutedVolumeIcon();
+ updateMutedVolumeIcon(device);
mIconAreaLayout.setOnTouchListener(((iconV, event) -> {
mSeekBar.dispatchTouchEvent(event);
return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index 8d3f728..30f564f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar;
+import android.app.Flags;
import android.app.Notification;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
@@ -60,20 +61,6 @@
return row.getEntry().getSbn().getNotification();
}
};
- private static final IconComparator ICON_VISIBILITY_COMPARATOR = new IconComparator() {
- public boolean compare(View parent, View child, Object parentData,
- Object childData) {
- return hasSameIcon(parentData, childData)
- && hasSameColor(parentData, childData);
- }
- };
- private static final IconComparator GREY_COMPARATOR = new IconComparator() {
- public boolean compare(View parent, View child, Object parentData,
- Object childData) {
- return !hasSameIcon(parentData, childData)
- || hasSameColor(parentData, childData);
- }
- };
private static final ResultApplicator GREY_APPLICATOR = new ResultApplicator() {
@Override
public void apply(View parent, View view, boolean apply, boolean reset) {
@@ -90,34 +77,58 @@
public NotificationGroupingUtil(ExpandableNotificationRow row) {
mRow = row;
+
+ final IconComparator iconVisibilityComparator = new IconComparator(mRow) {
+ public boolean compare(View parent, View child, Object parentData,
+ Object childData) {
+ return hasSameIcon(parentData, childData)
+ && hasSameColor(parentData, childData);
+ }
+ };
+ final IconComparator greyComparator = new IconComparator(mRow) {
+ public boolean compare(View parent, View child, Object parentData,
+ Object childData) {
+ if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
+ return false;
+ }
+ return !hasSameIcon(parentData, childData)
+ || hasSameColor(parentData, childData);
+ }
+ };
+
// To hide the icons if they are the same and the color is the same
mProcessors.add(new Processor(mRow,
com.android.internal.R.id.icon,
ICON_EXTRACTOR,
- ICON_VISIBILITY_COMPARATOR,
+ iconVisibilityComparator,
VISIBILITY_APPLICATOR));
- // To grey them out the icons and expand button when the icons are not the same
+ // To grey out the icons when they are not the same, or they have the same color
mProcessors.add(new Processor(mRow,
com.android.internal.R.id.status_bar_latest_event_content,
ICON_EXTRACTOR,
- GREY_COMPARATOR,
+ greyComparator,
GREY_APPLICATOR));
+ // To show the large icon on the left side instead if all the small icons are the same
mProcessors.add(new Processor(mRow,
com.android.internal.R.id.status_bar_latest_event_content,
ICON_EXTRACTOR,
- ICON_VISIBILITY_COMPARATOR,
+ iconVisibilityComparator,
LEFT_ICON_APPLICATOR));
+ // To only show the work profile icon in the group header
mProcessors.add(new Processor(mRow,
com.android.internal.R.id.profile_badge,
null /* Extractor */,
BADGE_COMPARATOR,
VISIBILITY_APPLICATOR));
+ // To hide the app name in group children
mProcessors.add(new Processor(mRow,
com.android.internal.R.id.app_name_text,
null,
APP_NAME_COMPARATOR,
APP_NAME_APPLICATOR));
+ // To hide the header text if it's the same
mProcessors.add(Processor.forTextView(mRow, com.android.internal.R.id.header_text));
+
mDividers.add(com.android.internal.R.id.header_text_divider);
mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
mDividers.add(com.android.internal.R.id.time_divider);
@@ -261,6 +272,7 @@
mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
mApply = !mComparator.isEmpty(mParentView);
}
+
public void compareToGroupParent(ExpandableNotificationRow row) {
if (!mApply) {
return;
@@ -356,12 +368,21 @@
}
private abstract static class IconComparator implements ViewComparator {
+ private final ExpandableNotificationRow mRow;
+
+ IconComparator(ExpandableNotificationRow row) {
+ mRow = row;
+ }
+
@Override
public boolean compare(View parent, View child, Object parentData, Object childData) {
return false;
}
protected boolean hasSameIcon(Object parentData, Object childData) {
+ if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
+ return true;
+ }
Icon parentIcon = getIcon((Notification) parentData);
Icon childIcon = getIcon((Notification) childData);
return parentIcon.sameAs(childIcon);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index f6f4503..f65ae67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -17,8 +17,10 @@
package com.android.systemui.statusbar.dagger
import android.content.Context
+import com.android.systemui.CameraProtectionLoader
import com.android.systemui.CoreStartable
import com.android.systemui.SysUICutoutProvider
+import com.android.systemui.SysUICutoutProviderImpl
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
@@ -114,6 +116,16 @@
@Provides
@SysUISingleton
+ fun sysUiCutoutProvider(
+ factory: SysUICutoutProviderImpl.Factory,
+ context: Context,
+ cameraProtectionLoader: CameraProtectionLoader,
+ ): SysUICutoutProvider {
+ return factory.create(context, cameraProtectionLoader)
+ }
+
+ @Provides
+ @SysUISingleton
fun contentInsetsProvider(
factory: StatusBarContentInsetsProviderImpl.Factory,
context: Context,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
index ed96482..415d990 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
@@ -23,7 +23,7 @@
import dagger.Module
import dagger.Provides
-@Module
+@Module(includes = [SystemEventChipAnimationControllerModule::class])
interface StatusBarEventsModule {
companion object {
@@ -41,4 +41,4 @@
fun bindSystemStatusAnimationScheduler(
systemStatusAnimationSchedulerImpl: SystemStatusAnimationSchedulerImpl
): SystemStatusAnimationScheduler
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index bf7e879..35816c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -31,22 +31,46 @@
import androidx.core.animation.AnimatorSet
import androidx.core.animation.ValueAnimator
import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
-import javax.inject.Inject
+import dagger.Module
+import dagger.Provides
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlin.math.roundToInt
-/**
- * Controls the view for system event animations.
- */
-class SystemEventChipAnimationController @Inject constructor(
- private val context: Context,
- private val statusBarWindowControllerStore: StatusBarWindowControllerStore,
- private val contentInsetsProvider: StatusBarContentInsetsProvider,
-) : SystemStatusAnimationCallback {
+/** Controls the view for system event animations. */
+interface SystemEventChipAnimationController : SystemStatusAnimationCallback {
+
+ /**
+ * Give the chip controller a chance to inflate and configure the chip view before we start
+ * animating
+ */
+ fun prepareChipAnimation(viewCreator: ViewCreator)
+
+ fun init()
+
+ /** Announces [contentDescriptions] for accessibility. */
+ fun announceForAccessibility(contentDescriptions: String)
+
+ override fun onSystemEventAnimationBegin(): Animator
+
+ override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator
+}
+
+class SystemEventChipAnimationControllerImpl
+@AssistedInject
+constructor(
+ @Assisted private val context: Context,
+ @Assisted private val statusBarWindowController: StatusBarWindowController,
+ @Assisted private val contentInsetsProvider: StatusBarContentInsetsProvider,
+) : SystemEventChipAnimationController {
private lateinit var animationWindowView: FrameLayout
private lateinit var themedContext: ContextThemeWrapper
@@ -57,25 +81,27 @@
private var animationDirection = LEFT
@VisibleForTesting var chipBounds = Rect()
- private val chipWidth get() = chipBounds.width()
- private val chipRight get() = chipBounds.right
- private val chipLeft get() = chipBounds.left
- private var chipMinWidth = context.resources.getDimensionPixelSize(
- R.dimen.ongoing_appops_chip_min_animation_width)
+ private val chipWidth
+ get() = chipBounds.width()
- private val dotSize = context.resources.getDimensionPixelSize(
- R.dimen.ongoing_appops_dot_diameter)
+ private val chipRight
+ get() = chipBounds.right
+
+ private val chipLeft
+ get() = chipBounds.left
+
+ private var chipMinWidth =
+ context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_min_animation_width)
+
+ private val dotSize =
+ context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
// Use during animation so that multiple animators can update the drawing rect
private var animRect = Rect()
// TODO: move to dagger
@VisibleForTesting var initialized = false
- /**
- * Give the chip controller a chance to inflate and configure the chip view before we start
- * animating
- */
- fun prepareChipAnimation(viewCreator: ViewCreator) {
+ override fun prepareChipAnimation(viewCreator: ViewCreator) {
if (!initialized) {
init()
}
@@ -83,47 +109,62 @@
// Initialize the animated view
val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
- currentAnimatedView = viewCreator(themedContext).also {
- animationWindowView.addView(
+ currentAnimatedView =
+ viewCreator(themedContext).also {
+ animationWindowView.addView(
it.view,
layoutParamsDefault(
- if (animationWindowView.isLayoutRtl) insets.left
- else insets.right))
- it.view.alpha = 0f
- // For some reason, the window view's measured width is always 0 here, so use the
- // parent (status bar)
- it.view.measure(
+ if (animationWindowView.isLayoutRtl) insets.left else insets.right
+ ),
+ )
+ it.view.alpha = 0f
+ // For some reason, the window view's measured width is always 0 here, so use the
+ // parent (status bar)
+ it.view.measure(
View.MeasureSpec.makeMeasureSpec(
- (animationWindowView.parent as View).width, AT_MOST),
+ (animationWindowView.parent as View).width,
+ AT_MOST,
+ ),
View.MeasureSpec.makeMeasureSpec(
- (animationWindowView.parent as View).height, AT_MOST))
+ (animationWindowView.parent as View).height,
+ AT_MOST,
+ ),
+ )
- updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
- }
+ updateChipBounds(
+ it,
+ contentInsetsProvider.getStatusBarContentAreaForCurrentRotation(),
+ )
+ }
}
override fun onSystemEventAnimationBegin(): Animator {
initializeAnimRect()
- val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
- startDelay = 7.frames
- duration = 5.frames
- interpolator = null
- addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
- }
+ val alphaIn =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ startDelay = 7.frames
+ duration = 5.frames
+ interpolator = null
+ addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
+ }
currentAnimatedView?.contentView?.alpha = 0f
- val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
- startDelay = 10.frames
- duration = 10.frames
- interpolator = null
- addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
- }
- val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
- startDelay = 7.frames
- duration = 23.frames
- interpolator = STATUS_BAR_X_MOVE_IN
- addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
- }
+ val contentAlphaIn =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ startDelay = 10.frames
+ duration = 10.frames
+ interpolator = null
+ addUpdateListener {
+ currentAnimatedView?.contentView?.alpha = animatedValue as Float
+ }
+ }
+ val moveIn =
+ ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
+ startDelay = 7.frames
+ duration = 23.frames
+ interpolator = STATUS_BAR_X_MOVE_IN
+ addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
+ }
val animSet = AnimatorSet()
animSet.playTogether(alphaIn, contentAlphaIn, moveIn)
return animSet
@@ -131,75 +172,80 @@
override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
initializeAnimRect()
- val finish = if (hasPersistentDot) {
- createMoveOutAnimationForDot()
- } else {
- createMoveOutAnimationDefault()
- }
-
- finish.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- animationWindowView.removeView(currentAnimatedView!!.view)
+ val finish =
+ if (hasPersistentDot) {
+ createMoveOutAnimationForDot()
+ } else {
+ createMoveOutAnimationDefault()
}
- })
+
+ finish.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ animationWindowView.removeView(currentAnimatedView!!.view)
+ }
+ }
+ )
return finish
}
private fun createMoveOutAnimationForDot(): Animator {
- val width1 = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
- duration = 9.frames
- interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
- addUpdateListener {
- updateAnimatedViewBoundsWidth(animatedValue as Int)
+ val width1 =
+ ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
+ duration = 9.frames
+ interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
+ addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
}
- }
- val width2 = ValueAnimator.ofInt(chipMinWidth, dotSize).apply {
- startDelay = 9.frames
- duration = 20.frames
- interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
- addUpdateListener {
- updateAnimatedViewBoundsWidth(animatedValue as Int)
+ val width2 =
+ ValueAnimator.ofInt(chipMinWidth, dotSize).apply {
+ startDelay = 9.frames
+ duration = 20.frames
+ interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
+ addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
}
- }
val keyFrame1Height = dotSize * 2
val chipVerticalCenter = chipBounds.top + chipBounds.height() / 2
- val height1 = ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply {
- startDelay = 8.frames
- duration = 6.frames
- interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
- addUpdateListener {
- updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+ val height1 =
+ ValueAnimator.ofInt(chipBounds.height(), keyFrame1Height).apply {
+ startDelay = 8.frames
+ duration = 6.frames
+ interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
+ addUpdateListener {
+ updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+ }
}
- }
- val height2 = ValueAnimator.ofInt(keyFrame1Height, dotSize).apply {
- startDelay = 14.frames
- duration = 15.frames
- interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
- addUpdateListener {
- updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+ val height2 =
+ ValueAnimator.ofInt(keyFrame1Height, dotSize).apply {
+ startDelay = 14.frames
+ duration = 15.frames
+ interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
+ addUpdateListener {
+ updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
+ }
}
- }
// Move the chip view to overlap exactly with the privacy dot. The chip displays by default
// exactly adjacent to the dot, so we can just move over by the diameter of the dot itself
- val moveOut = ValueAnimator.ofInt(0, dotSize).apply {
- startDelay = 3.frames
- duration = 11.frames
- interpolator = STATUS_CHIP_MOVE_TO_DOT
- addUpdateListener {
- // If RTL, we can just invert the move
- val amt = if (animationDirection == LEFT) {
- animatedValue as Int
- } else {
- -(animatedValue as Int)
+ val moveOut =
+ ValueAnimator.ofInt(0, dotSize).apply {
+ startDelay = 3.frames
+ duration = 11.frames
+ interpolator = STATUS_CHIP_MOVE_TO_DOT
+ addUpdateListener {
+ // If RTL, we can just invert the move
+ val amt =
+ if (animationDirection == LEFT) {
+ animatedValue as Int
+ } else {
+ -(animatedValue as Int)
+ }
+ updateAnimatedBoundsX(amt)
}
- updateAnimatedBoundsX(amt)
}
- }
val animSet = AnimatorSet()
animSet.playTogether(width1, width2, height1, height2, moveOut)
@@ -207,71 +253,80 @@
}
private fun createMoveOutAnimationDefault(): Animator {
- val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
- startDelay = 6.frames
- duration = 6.frames
- interpolator = null
- addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
- }
+ val alphaOut =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ startDelay = 6.frames
+ duration = 6.frames
+ interpolator = null
+ addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
+ }
- val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
- duration = 5.frames
- interpolator = null
- addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
- }
-
- val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
- duration = 23.frames
- interpolator = STATUS_BAR_X_MOVE_OUT
- addUpdateListener {
- currentAnimatedView?.apply {
- updateAnimatedViewBoundsWidth(animatedValue as Int)
+ val contentAlphaOut =
+ ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 5.frames
+ interpolator = null
+ addUpdateListener {
+ currentAnimatedView?.contentView?.alpha = animatedValue as Float
}
}
- }
+
+ val moveOut =
+ ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
+ duration = 23.frames
+ interpolator = STATUS_BAR_X_MOVE_OUT
+ addUpdateListener {
+ currentAnimatedView?.apply {
+ updateAnimatedViewBoundsWidth(animatedValue as Int)
+ }
+ }
+ }
val animSet = AnimatorSet()
animSet.playTogether(alphaOut, contentAlphaOut, moveOut)
return animSet
}
- fun init() {
+ override fun init() {
initialized = true
themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
- animationWindowView = LayoutInflater.from(themedContext)
- .inflate(R.layout.system_event_animation_window, null) as FrameLayout
+ animationWindowView =
+ LayoutInflater.from(themedContext).inflate(R.layout.system_event_animation_window, null)
+ as FrameLayout
// Matches status_bar.xml
val height = themedContext.resources.getDimensionPixelSize(R.dimen.status_bar_height)
val lp = FrameLayout.LayoutParams(MATCH_PARENT, height)
lp.gravity = Gravity.END or Gravity.TOP
- statusBarWindowControllerStore.defaultDisplay.addViewToWindow(animationWindowView, lp)
+ statusBarWindowController.addViewToWindow(animationWindowView, lp)
animationWindowView.clipToPadding = false
animationWindowView.clipChildren = false
// Use contentInsetsProvider rather than configuration controller, since we only care
// about status bar dimens
- contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
- override fun onStatusBarContentInsetsChanged() {
- val newContentArea = contentInsetsProvider
- .getStatusBarContentAreaForCurrentRotation()
- updateDimens(newContentArea)
+ contentInsetsProvider.addCallback(
+ object : StatusBarContentInsetsChangedListener {
+ override fun onStatusBarContentInsetsChanged() {
+ val newContentArea =
+ contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
+ updateDimens(newContentArea)
- // If we are currently animating, we have to re-solve for the chip bounds. If we're
- // not animating then [prepareChipAnimation] will take care of it for us
- currentAnimatedView?.let {
- updateChipBounds(it, newContentArea)
- // Since updateCurrentAnimatedView can only be called during an animation, we
- // have to create a dummy animator here to apply the new chip bounds
- val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
- animator.addUpdateListener { updateCurrentAnimatedView() }
- animator.start()
+ // If we are currently animating, we have to re-solve for the chip bounds. If
+ // we're
+ // not animating then [prepareChipAnimation] will take care of it for us
+ currentAnimatedView?.let {
+ updateChipBounds(it, newContentArea)
+ // Since updateCurrentAnimatedView can only be called during an animation,
+ // we
+ // have to create a dummy animator here to apply the new chip bounds
+ val animator = ValueAnimator.ofInt(0, 1).setDuration(0)
+ animator.addUpdateListener { updateCurrentAnimatedView() }
+ animator.start()
+ }
}
}
- })
+ )
}
- /** Announces [contentDescriptions] for accessibility. */
- fun announceForAccessibility(contentDescriptions: String) {
+ override fun announceForAccessibility(contentDescriptions: String) {
currentAnimatedView?.view?.announceForAccessibility(contentDescriptions)
}
@@ -283,9 +338,9 @@
}
/**
- * Use the current status bar content area and the current chip's measured size to update
- * the animation rect and chipBounds. This method can be called at any time and will update
- * the current animation values properly during e.g. a rotation.
+ * Use the current status bar content area and the current chip's measured size to update the
+ * animation rect and chipBounds. This method can be called at any time and will update the
+ * current animation values properly during e.g. a rotation.
*/
private fun updateChipBounds(chip: BackgroundAnimatableView, contentArea: Rect) {
// decide which direction we're animating from, and then set some screen coordinates
@@ -309,14 +364,13 @@
}
private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
- FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
- it.gravity = Gravity.END or Gravity.CENTER_VERTICAL
- it.marginEnd = marginEnd
- }
+ FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
+ it.gravity = Gravity.END or Gravity.CENTER_VERTICAL
+ it.marginEnd = marginEnd
+ }
private fun initializeAnimRect() = animRect.set(chipBounds)
-
/**
* To be called during an animation, sets the width and updates the current animated chip view
*/
@@ -324,7 +378,8 @@
when (animationDirection) {
LEFT -> {
animRect.set((chipRight - width), animRect.top, chipRight, animRect.bottom)
- } else /* RIGHT */ -> {
+ }
+ else /* RIGHT */ -> {
animRect.set(chipLeft, animRect.top, (chipLeft + width), animRect.bottom)
}
}
@@ -337,44 +392,73 @@
*/
private fun updateAnimatedViewBoundsHeight(height: Int, verticalCenter: Int) {
animRect.set(
- animRect.left,
- verticalCenter - (height.toFloat() / 2).roundToInt(),
- animRect.right,
- verticalCenter + (height.toFloat() / 2).roundToInt())
+ animRect.left,
+ verticalCenter - (height.toFloat() / 2).roundToInt(),
+ animRect.right,
+ verticalCenter + (height.toFloat() / 2).roundToInt(),
+ )
updateCurrentAnimatedView()
}
- /**
- * To be called during an animation, updates the animation rect offset and updates the chip
- */
+ /** To be called during an animation, updates the animation rect offset and updates the chip */
private fun updateAnimatedBoundsX(translation: Int) {
currentAnimatedView?.view?.translationX = translation.toFloat()
}
- /**
- * To be called during an animation. Sets the chip rect to animRect
- */
+ /** To be called during an animation. Sets the chip rect to animRect */
private fun updateCurrentAnimatedView() {
currentAnimatedView?.setBoundsForAnimation(
- animRect.left, animRect.top, animRect.right, animRect.bottom
+ animRect.left,
+ animRect.top,
+ animRect.right,
+ animRect.bottom,
)
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ context: Context,
+ statusBarWindowController: StatusBarWindowController,
+ contentInsetsProvider: StatusBarContentInsetsProvider,
+ ): SystemEventChipAnimationControllerImpl
+ }
}
-/**
- * Chips should provide a view that can be animated with something better than a fade-in
- */
+/** Chips should provide a view that can be animated with something better than a fade-in */
interface BackgroundAnimatableView {
val view: View // Since this can't extend View, add a view prop
get() = this as View
+
val contentView: View? // This will be alpha faded during appear and disappear animation
get() = null
+
val chipWidth: Int
get() = view.measuredWidth
+
fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int)
}
// Animation directions
private const val LEFT = 1
private const val RIGHT = 2
+
+@Module
+object SystemEventChipAnimationControllerModule {
+
+ @Provides
+ @SysUISingleton
+ fun controller(
+ factory: SystemEventChipAnimationControllerImpl.Factory,
+ context: Context,
+ statusBarWindowControllerStore: StatusBarWindowControllerStore,
+ contentInsetsProvider: StatusBarContentInsetsProvider,
+ ): SystemEventChipAnimationController {
+ return factory.create(
+ context,
+ statusBarWindowControllerStore.defaultDisplay,
+ contentInsetsProvider,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
new file mode 100644
index 0000000..2ee1dffd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection
+
+import android.annotation.SuppressLint
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.SystemClockImpl
+import com.android.systemui.util.withIncreasedIndent
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A cache in which entries can "survive" getting purged [retainCount] times, given consecutive
+ * [purge] calls made at least [purgeTimeoutMillis] apart. See also [purge].
+ *
+ * This cache is safe for multithreaded usage, and is recommended for objects that take a while to
+ * resolve (such as drawables, or things that require binder calls). As such, [getOrFetch] is
+ * recommended to be run on a background thread, while [purge] can be done from any thread.
+ */
+@SuppressLint("DumpableNotRegistered") // this will be dumped by container classes
+class NotifCollectionCache<V>(
+ private val retainCount: Int = 1,
+ private val purgeTimeoutMillis: Long = 1000L,
+ private val systemClock: SystemClock = SystemClockImpl(),
+) : Dumpable {
+ @get:VisibleForTesting val cache = ConcurrentHashMap<String, CacheEntry>()
+
+ // Counters for cache hits and misses to be used to calculate and dump the hit ratio
+ @get:VisibleForTesting val misses = AtomicInteger(0)
+ @get:VisibleForTesting val hits = AtomicInteger(0)
+
+ init {
+ if (retainCount < 0) {
+ throw IllegalArgumentException("retainCount cannot be negative")
+ }
+ }
+
+ inner class CacheEntry(val key: String, val value: V) {
+ /**
+ * The "lives" represent how many times the entry will remain in the cache when purging it
+ * is attempted.
+ */
+ @get:VisibleForTesting var lives: Int = retainCount + 1
+ /**
+ * The last time this entry lost a "life". Starts at a negative value chosen so that the
+ * first purge is always considered "valid".
+ */
+ private var lastValidPurge: Long = -purgeTimeoutMillis
+
+ fun resetLives() {
+ // Lives/timeouts don't matter if retainCount is 0
+ if (retainCount == 0) {
+ return
+ }
+
+ synchronized(key) {
+ lives = retainCount + 1
+ lastValidPurge = -purgeTimeoutMillis
+ }
+ // Add it to the cache again just in case it was deleted before we could reset the lives
+ cache[key] = this
+ }
+
+ fun tryPurge(): Boolean {
+ // Lives/timeouts don't matter if retainCount is 0
+ if (retainCount == 0) {
+ return true
+ }
+
+ // Using uptimeMillis since it's guaranteed to be monotonic, as we don't want a
+ // timezone/clock change to break us
+ val now = systemClock.uptimeMillis()
+
+ // Cannot purge the same entry from two threads simultaneously
+ synchronized(key) {
+ if (now - lastValidPurge < purgeTimeoutMillis) {
+ return false
+ }
+ lastValidPurge = now
+ return --lives <= 0
+ }
+ }
+ }
+
+ /**
+ * Get value from cache, or fetch it and add it to cache if not found. This can be called from
+ * any thread, but is usually expected to be called from the background.
+ *
+ * @param key key for the object to be obtained
+ * @param fetch method to fetch the object and add it to the cache if not present; note that
+ * there is no guarantee that two [fetch] cannot run in parallel for the same [key] (if
+ * [getOrFetch] is called simultaneously from different threads), so be mindful of potential
+ * side effects
+ */
+ fun getOrFetch(key: String, fetch: (String) -> V): V {
+ val entry = cache[key]
+ if (entry != null) {
+ hits.incrementAndGet()
+ // Refresh lives on access
+ entry.resetLives()
+ return entry.value
+ }
+
+ misses.incrementAndGet()
+ val value = fetch(key)
+ cache[key] = CacheEntry(key, value)
+ return value
+ }
+
+ /**
+ * Clear entries that are NOT in [wantedKeys] if appropriate. This can be called from any
+ * thread.
+ *
+ * If retainCount > 0, a given entry will need to not be present in [wantedKeys] for
+ * ([retainCount] + 1) consecutive [purge] calls made within at least [purgeTimeoutMillis] of
+ * each other in order to be cleared. This count will be reset for any given entry 1) if
+ * [getOrFetch] is called for the entry or 2) if the entry is present in [wantedKeys] in a
+ * subsequent [purge] call. We prioritize keeping the entry if possible, so if [purge] is called
+ * simultaneously with [getOrFetch] on different threads for example, we will try to keep it in
+ * the cache, although it is not guaranteed. If avoiding cache misses is a concern, consider
+ * increasing the [retainCount] or [purgeTimeoutMillis].
+ *
+ * For example, say [retainCount] = 1 and [purgeTimeoutMillis] = 1000 and we start with entries
+ * (a, b, c) in the cache:
+ * ```kotlin
+ * purge((a, c)); // marks b for deletion
+ * Thread.sleep(500)
+ * purge((a, c)); // does nothing as it was called earlier than the min 1s
+ * Thread.sleep(500)
+ * purge((b, c)); // b is no longer marked for deletion, but now a is
+ * Thread.sleep(1000);
+ * purge((c)); // deletes a from the cache and marks b for deletion, etc.
+ * ```
+ */
+ fun purge(wantedKeys: List<String>) {
+ for ((key, entry) in cache) {
+ if (key in wantedKeys) {
+ entry.resetLives()
+ } else if (entry.tryPurge()) {
+ cache.remove(key)
+ }
+ }
+ }
+
+ /** Clear all entries from the cache. */
+ fun clear() {
+ cache.clear()
+ }
+
+ override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
+ val pw = pwOrig.asIndenting()
+
+ pw.println("$TAG(retainCount = $retainCount, purgeTimeoutMillis = $purgeTimeoutMillis)")
+ pw.withIncreasedIndent {
+ pw.printCollection("keys present in cache", cache.keys.stream().sorted().toList())
+
+ val misses = misses.get()
+ val hits = hits.get()
+ pw.println(
+ "cache hit ratio = ${(hits.toFloat() / (hits + misses)) * 100}% " +
+ "($hits hits, $misses misses)"
+ )
+ }
+ }
+
+ companion object {
+ const val TAG = "NotifCollectionCache"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 38e6609..933f793 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -262,6 +262,11 @@
*/
private boolean mIsHeadsUp;
+ /**
+ * Whether or not the notification is showing the app icon instead of the small icon.
+ */
+ private boolean mIsShowingAppIcon;
+
private boolean mLastChronometerRunning = true;
private ViewStub mChildrenContainerStub;
private GroupMembershipManager mGroupMembershipManager;
@@ -816,6 +821,20 @@
}
}
+ /**
+ * Indicate that the notification is showing the app icon instead of the small icon.
+ */
+ public void setIsShowingAppIcon(boolean isShowingAppIcon) {
+ mIsShowingAppIcon = isShowingAppIcon;
+ }
+
+ /**
+ * Whether or not the notification is showing the app icon instead of the small icon.
+ */
+ public boolean isShowingAppIcon() {
+ return mIsShowingAppIcon;
+ }
+
@Override
public boolean showingPulsing() {
return isHeadsUpState() && (isDozing() || (mOnKeyguard && isBypassEnabled()));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
index 79defd2..7b85bfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
@@ -21,6 +21,7 @@
import android.util.AttributeSet
import android.view.View
import com.android.internal.widget.NotificationRowIconView
+import com.android.internal.widget.NotificationRowIconView.NotificationIconProvider
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotifRemoteViewsFactory
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder
@@ -47,20 +48,27 @@
return when (name) {
NotificationRowIconView::class.java.name ->
NotificationRowIconView(context, attrs).also { view ->
- val sbn = row.entry.sbn
- view.setIconProvider(
- object : NotificationRowIconView.NotificationIconProvider {
- override fun shouldShowAppIcon(): Boolean {
- return iconStyleProvider.shouldShowAppIcon(row.entry.sbn, context)
- }
-
- override fun getAppIcon(): Drawable {
- return appIconProvider.getOrFetchAppIcon(sbn.packageName, context)
- }
- }
- )
+ view.setIconProvider(createIconProvider(row, context))
}
else -> null
}
}
+
+ private fun createIconProvider(
+ row: ExpandableNotificationRow,
+ context: Context,
+ ): NotificationIconProvider {
+ val sbn = row.entry.sbn
+ return object : NotificationIconProvider {
+ override fun shouldShowAppIcon(): Boolean {
+ val shouldShowAppIcon = iconStyleProvider.shouldShowAppIcon(row.entry.sbn, context)
+ row.setIsShowingAppIcon(shouldShowAppIcon)
+ return shouldShowAppIcon
+ }
+
+ override fun getAppIcon(): Drawable {
+ return appIconProvider.getOrFetchAppIcon(sbn.packageName, context)
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
index b4411f1..f8aff69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
@@ -16,15 +16,18 @@
package com.android.systemui.statusbar.notification.row.wrapper
+import android.app.Flags
import android.content.Context
import android.graphics.drawable.AnimatedImageDrawable
import android.view.View
import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
import com.android.internal.widget.CachingIconView
import com.android.internal.widget.ConversationLayout
import com.android.internal.widget.MessagingGroup
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLinearLayout
+import com.android.internal.widget.NotificationRowIconView
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.NotificationFadeAware
import com.android.systemui.statusbar.notification.NotificationUtils
@@ -32,23 +35,23 @@
import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform
import com.android.systemui.util.children
-/**
- * Wraps a notification containing a conversation template
- */
-class NotificationConversationTemplateViewWrapper constructor(
+/** Wraps a notification containing a conversation template */
+class NotificationConversationTemplateViewWrapper(
ctx: Context,
view: View,
- row: ExpandableNotificationRow
+ row: ExpandableNotificationRow,
) : NotificationTemplateViewWrapper(ctx, view, row) {
- private val minHeightWithActions: Int = NotificationUtils.getFontScaledHeight(
+ private val minHeightWithActions: Int =
+ NotificationUtils.getFontScaledHeight(
ctx,
- R.dimen.notification_messaging_actions_min_height
- )
+ R.dimen.notification_messaging_actions_min_height,
+ )
private val conversationLayout: ConversationLayout = view as ConversationLayout
private lateinit var conversationIconContainer: View
private lateinit var conversationIconView: CachingIconView
+ private lateinit var badgeIconView: NotificationRowIconView
private lateinit var conversationBadgeBg: View
private lateinit var expandBtn: View
private lateinit var expandBtnContainer: View
@@ -68,10 +71,13 @@
messageContainers = conversationLayout.messagingGroups
with(conversationLayout) {
conversationIconContainer =
- requireViewById(com.android.internal.R.id.conversation_icon_container)
+ requireViewById(com.android.internal.R.id.conversation_icon_container)
conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon)
+ if (Flags.notificationsRedesignAppIcons()) {
+ badgeIconView = requireViewById(com.android.internal.R.id.icon)
+ }
conversationBadgeBg =
- requireViewById(com.android.internal.R.id.conversation_icon_badge_bg)
+ requireViewById(com.android.internal.R.id.conversation_icon_badge_bg)
expandBtn = requireViewById(com.android.internal.R.id.expand_button)
expandBtnContainer = requireViewById(com.android.internal.R.id.expand_button_container)
importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring)
@@ -80,7 +86,7 @@
facePileTop = findViewById(com.android.internal.R.id.conversation_face_pile_top)
facePileBottom = findViewById(com.android.internal.R.id.conversation_face_pile_bottom)
facePileBottomBg =
- findViewById(com.android.internal.R.id.conversation_face_pile_bottom_background)
+ findViewById(com.android.internal.R.id.conversation_face_pile_bottom_background)
}
}
@@ -88,6 +94,13 @@
// Reinspect the notification. Before the super call, because the super call also updates
// the transformation types and we need to have our values set by then.
resolveViews()
+ if (Flags.notificationsRedesignAppIcons() && row.isShowingAppIcon) {
+ // Override the margins to be 2dp instead of 4dp according to the new design if we're
+ // showing the app icon.
+ val lp = badgeIconView.layoutParams as MarginLayoutParams
+ lp.setMargins(2, 2, 2, 2)
+ badgeIconView.layoutParams = lp
+ }
super.onContentUpdated(row)
}
@@ -96,56 +109,50 @@
super.updateTransformedTypes()
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, conversationTitleView)
- addTransformedViews(
- messagingLinearLayout,
- appName
- )
+ addTransformedViews(messagingLinearLayout, appName)
setCustomImageMessageTransform(mTransformationHelper, imageMessageContainer)
addViewsTransformingToSimilar(
- conversationIconView,
- conversationBadgeBg,
- expandBtn,
- importanceRing,
- facePileTop,
- facePileBottom,
- facePileBottomBg
+ conversationIconView,
+ conversationBadgeBg,
+ expandBtn,
+ importanceRing,
+ facePileTop,
+ facePileBottom,
+ facePileBottomBg,
)
}
override fun getShelfTransformationTarget(): View? =
- if (conversationLayout.isImportantConversation)
- if (conversationIconView.visibility != View.GONE)
- conversationIconView
- else
- // A notification with a fallback icon was set to important. Currently
- // the transformation doesn't work for these and needs to be fixed.
- // In the meantime those are using the icon.
- super.getShelfTransformationTarget()
+ if (conversationLayout.isImportantConversation)
+ if (conversationIconView.visibility != View.GONE) conversationIconView
else
- super.getShelfTransformationTarget()
+ // A notification with a fallback icon was set to important. Currently
+ // the transformation doesn't work for these and needs to be fixed.
+ // In the meantime those are using the icon.
+ super.getShelfTransformationTarget()
+ else super.getShelfTransformationTarget()
override fun setRemoteInputVisible(visible: Boolean) =
- conversationLayout.showHistoricMessages(visible)
+ conversationLayout.showHistoricMessages(visible)
override fun updateExpandability(
expandable: Boolean,
onClickListener: View.OnClickListener,
- requestLayout: Boolean
+ requestLayout: Boolean,
) = conversationLayout.updateExpandability(expandable, onClickListener)
override fun disallowSingleClick(x: Float, y: Float): Boolean {
- val isOnExpandButton = expandBtnContainer.visibility == View.VISIBLE &&
- isOnView(expandBtnContainer, x, y)
+ val isOnExpandButton =
+ expandBtnContainer.visibility == View.VISIBLE && isOnView(expandBtnContainer, x, y)
return isOnExpandButton || super.disallowSingleClick(x, y)
}
override fun getMinLayoutHeight(): Int =
- if (mActionsContainer != null && mActionsContainer.visibility != View.GONE)
- minHeightWithActions
- else
- super.getMinLayoutHeight()
+ if (mActionsContainer != null && mActionsContainer.visibility != View.GONE)
+ minHeightWithActions
+ else super.getMinLayoutHeight()
override fun setNotificationFaded(faded: Boolean) {
// Do not call super
@@ -157,16 +164,17 @@
override fun setAnimationsRunning(running: Boolean) {
// We apply to both the child message containers in a conversation group,
// and the top level image message container.
- val containers = messageContainers.asSequence().map { it.messageContainer } +
+ val containers =
+ messageContainers.asSequence().map { it.messageContainer } +
sequenceOf(imageMessageContainer)
val drawables =
- containers
- .flatMap { it.children }
- .mapNotNull { child ->
- (child as? MessagingImageMessage)?.let { imageMessage ->
- imageMessage.drawable as? AnimatedImageDrawable
- }
- }
+ containers
+ .flatMap { it.children }
+ .mapNotNull { child ->
+ (child as? MessagingImageMessage)?.let { imageMessage ->
+ imageMessage.drawable as? AnimatedImageDrawable
+ }
+ }
drawables.toSet().forEach {
when {
running -> it.start()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
index d2a17c2..ad58a01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/CameraProtectionLoaderImplTest.kt
@@ -37,14 +37,14 @@
overrideResource(R.string.config_protectedPhysicalCameraId, OUTER_CAMERA_PHYSICAL_ID)
overrideResource(
R.string.config_frontBuiltInDisplayCutoutProtection,
- OUTER_CAMERA_PROTECTION_PATH
+ OUTER_CAMERA_PROTECTION_PATH,
)
overrideResource(R.string.config_protectedScreenUniqueId, OUTER_SCREEN_UNIQUE_ID)
overrideResource(R.string.config_protectedInnerCameraId, INNER_CAMERA_LOGICAL_ID)
overrideResource(R.string.config_protectedInnerPhysicalCameraId, INNER_CAMERA_PHYSICAL_ID)
overrideResource(
R.string.config_innerBuiltInDisplayCutoutProtection,
- INNER_CAMERA_PROTECTION_PATH
+ INNER_CAMERA_PROTECTION_PATH,
)
overrideResource(R.string.config_protectedInnerScreenUniqueId, INNER_SCREEN_UNIQUE_ID)
}
@@ -107,7 +107,7 @@
private const val OUTER_CAMERA_PHYSICAL_ID = "11"
private const val OUTER_CAMERA_PROTECTION_PATH = "M 0,0 H 10,10 V 10,10 H 0,10 Z"
private val OUTER_CAMERA_PROTECTION_BOUNDS =
- Rect(/* left = */ 0, /* top = */ 0, /* right = */ 10, /* bottom = */ 10)
+ Rect(/* left= */ 0, /* top= */ 0, /* right= */ 10, /* bottom= */ 10)
private const val OUTER_SCREEN_UNIQUE_ID = "111"
private val OUTER_CAMERA_PROTECTION_INFO =
TestableProtectionInfo(
@@ -121,7 +121,7 @@
private const val INNER_CAMERA_PHYSICAL_ID = "22"
private const val INNER_CAMERA_PROTECTION_PATH = "M 0,0 H 20,20 V 20,20 H 0,20 Z"
private val INNER_CAMERA_PROTECTION_BOUNDS =
- Rect(/* left = */ 0, /* top = */ 0, /* right = */ 20, /* bottom = */ 20)
+ Rect(/* left= */ 0, /* top= */ 0, /* right= */ 20, /* bottom= */ 20)
private const val INNER_SCREEN_UNIQUE_ID = "222"
private val INNER_CAMERA_PROTECTION_INFO =
TestableProtectionInfo(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
index bc12aaa..a01feca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/FaceScanningProviderFactoryTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
import com.android.systemui.decor.FaceScanningProviderFactory
+import com.android.systemui.decor.FaceScanningProviderFactoryImpl
import com.android.systemui.log.ScreenDecorationsLogger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -69,20 +70,20 @@
dmGlobal,
displayId,
displayInfo,
- DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
+ DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS,
)
whenever(dmGlobal.getDisplayInfo(eq(displayId))).thenReturn(displayInfo)
val displayContext = context.createDisplayContext(display) as SysuiTestableContext
displayContext.orCreateTestableResources.addOverride(
R.array.config_displayUniqueIdArray,
- arrayOf(displayId)
+ arrayOf(displayId),
)
displayContext.orCreateTestableResources.addOverride(
R.bool.config_fillMainBuiltInDisplayCutout,
- true
+ true,
)
underTest =
- FaceScanningProviderFactory(
+ FaceScanningProviderFactoryImpl(
authController,
displayContext,
statusBarStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
index 61c7e1d..ef33210 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/SysUICutoutProviderTest.kt
@@ -42,7 +42,7 @@
fun cutoutInfoForCurrentDisplay_noCutout_returnsNull() {
val noCutoutDisplay = createDisplay(cutout = null)
val noCutoutDisplayContext = context.createDisplayContext(noCutoutDisplay)
- val provider = SysUICutoutProvider(noCutoutDisplayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(noCutoutDisplayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()
@@ -53,7 +53,7 @@
fun cutoutInfoForCurrentDisplay_returnsCutout() {
val cutoutDisplay = createDisplay()
val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
- val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(cutoutDisplayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
@@ -64,7 +64,7 @@
fun cutoutInfoForCurrentDisplay_noAssociatedProtection_returnsNoProtection() {
val cutoutDisplay = createDisplay()
val cutoutDisplayContext = context.createDisplayContext(cutoutDisplay)
- val provider = SysUICutoutProvider(cutoutDisplayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(cutoutDisplayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
@@ -75,7 +75,7 @@
fun cutoutInfoForCurrentDisplay_outerDisplay_protectionAssociated_returnsProtection() {
fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = OUTER_DISPLAY_UNIQUE_ID)
val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
- val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(outerDisplayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
@@ -86,7 +86,7 @@
fun cutoutInfoForCurrentDisplay_outerDisplay_protectionNotAvailable_returnsNullProtection() {
fakeProtectionLoader.clearProtectionInfoList()
val outerDisplayContext = context.createDisplayContext(OUTER_DISPLAY)
- val provider = SysUICutoutProvider(outerDisplayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(outerDisplayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
@@ -97,7 +97,7 @@
fun cutoutInfoForCurrentDisplay_displayWithNullId_protectionsWithNoId_returnsNullProtection() {
fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
val displayContext = context.createDisplayContext(createDisplay(uniqueId = null))
- val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(displayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
@@ -108,7 +108,7 @@
fun cutoutInfoForCurrentDisplay_displayWithEmptyId_protectionsWithNoId_returnsNullProtection() {
fakeProtectionLoader.addOuterCameraProtection(displayUniqueId = "")
val displayContext = context.createDisplayContext(createDisplay(uniqueId = ""))
- val provider = SysUICutoutProvider(displayContext, fakeProtectionLoader)
+ val provider = SysUICutoutProviderImpl(displayContext, fakeProtectionLoader)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
@@ -123,15 +123,13 @@
displayHeight = 1000,
rotation = Surface.ROTATION_0,
protectionBounds =
- Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+ Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
assertThat(sysUICutout.cameraProtection!!.bounds)
- .isEqualTo(
- Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
- )
+ .isEqualTo(Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110))
}
@Test
@@ -142,13 +140,13 @@
displayHeight = 1000,
rotation = Surface.ROTATION_90,
protectionBounds =
- Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+ Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
assertThat(sysUICutout.cameraProtection!!.bounds)
- .isEqualTo(Rect(/* left = */ 10, /* top = */ 10, /* right = */ 110, /* bottom = */ 60))
+ .isEqualTo(Rect(/* left= */ 10, /* top= */ 10, /* right= */ 110, /* bottom= */ 60))
}
@Test
@@ -156,7 +154,7 @@
val displayNaturalWidth = 500
val displayNaturalHeight = 1000
val originalProtectionBounds =
- Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+ Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110)
// Safe copy as we don't know at which layer the mutation could happen
val originalProtectionBoundsCopy = Rect(originalProtectionBounds)
val display =
@@ -168,10 +166,10 @@
)
fakeProtectionLoader.addOuterCameraProtection(
displayUniqueId = OUTER_DISPLAY_UNIQUE_ID,
- bounds = originalProtectionBounds
+ bounds = originalProtectionBounds,
)
val provider =
- SysUICutoutProvider(context.createDisplayContext(display), fakeProtectionLoader)
+ SysUICutoutProviderImpl(context.createDisplayContext(display), fakeProtectionLoader)
// Here we get the rotated bounds once
provider.cutoutInfoForCurrentDisplayAndRotation()
@@ -194,13 +192,13 @@
displayHeight = 1000,
rotation = Surface.ROTATION_180,
protectionBounds =
- Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+ Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
assertThat(sysUICutout.cameraProtection!!.bounds)
- .isEqualTo(Rect(/* left = */ 10, /* top = */ 890, /* right = */ 60, /* bottom = */ 990))
+ .isEqualTo(Rect(/* left= */ 10, /* top= */ 890, /* right= */ 60, /* bottom= */ 990))
}
@Test
@@ -211,15 +209,13 @@
displayHeight = 1000,
rotation = Surface.ROTATION_270,
protectionBounds =
- Rect(/* left = */ 440, /* top = */ 10, /* right = */ 490, /* bottom = */ 110)
+ Rect(/* left= */ 440, /* top= */ 10, /* right= */ 490, /* bottom= */ 110),
)
val sysUICutout = provider.cutoutInfoForCurrentDisplayAndRotation()!!
assertThat(sysUICutout.cameraProtection!!.bounds)
- .isEqualTo(
- Rect(/* left = */ 890, /* top = */ 440, /* right = */ 990, /* bottom = */ 490)
- )
+ .isEqualTo(Rect(/* left= */ 890, /* top= */ 440, /* right= */ 990, /* bottom= */ 490))
}
private fun setUpProviderWithCameraProtection(
@@ -245,9 +241,9 @@
)
fakeProtectionLoader.addOuterCameraProtection(
displayUniqueId = OUTER_DISPLAY_UNIQUE_ID,
- bounds = protectionBounds
+ bounds = protectionBounds,
)
- return SysUICutoutProvider(context.createDisplayContext(display), fakeProtectionLoader)
+ return SysUICutoutProviderImpl(context.createDisplayContext(display), fakeProtectionLoader)
}
companion object {
@@ -259,7 +255,7 @@
height: Int = 1000,
@Rotation rotation: Int = Surface.ROTATION_0,
uniqueId: String? = "uniqueId",
- cutout: DisplayCutout? = mock<DisplayCutout>()
+ cutout: DisplayCutout? = mock<DisplayCutout>(),
) =
mock<Display> {
whenever(this.getDisplayInfo(any())).thenAnswer {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
index fd550b0..6e36d42b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerContentTest.kt
@@ -88,7 +88,7 @@
},
layout = BouncerSceneLayout.BESIDE_USER_SWITCHER,
modifier = Modifier.fillMaxSize().testTag("BouncerContent"),
- dialogFactory = bouncerDialogFactory
+ dialogFactory = bouncerDialogFactory,
)
}
}
@@ -110,11 +110,19 @@
}
}
) {
- feature(hasTestTag("UserSwitcher"), positionInRoot, "userSwitcher_pos")
- feature(hasTestTag("UserSwitcher"), alpha, "userSwitcher_alpha")
+ feature(
+ hasTestTag("com.android.systemui:id/UserSwitcher"),
+ positionInRoot,
+ "userSwitcher_pos",
+ )
+ feature(
+ hasTestTag("com.android.systemui:id/UserSwitcher"),
+ alpha,
+ "userSwitcher_alpha",
+ )
feature(hasTestTag("FoldAware"), positionInRoot, "foldAware_pos")
feature(hasTestTag("FoldAware"), alpha, "foldAware_alpha")
- }
+ },
)
assertThat(motion).timeSeriesMatchesGolden()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 8731853..63ec78fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -35,6 +35,8 @@
import android.app.WallpaperColors;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper;
import android.view.View;
import android.widget.LinearLayout;
@@ -44,6 +46,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.media.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.systemui.SysuiTestCase;
@@ -738,4 +741,68 @@
assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(updatedList.size());
}
+
+ @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagDisabled_InputDeviceMutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(true /* isInputDevice */, true /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.media_output_icon_volume_off);
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagDisabled_OutputDeviceMutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(false /* isInputDevice */, true /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.media_output_icon_volume_off);
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagDisabled_InputDeviceUnmutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(true /* isInputDevice */, false /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.media_output_icon_volume);
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagDisabled_OutputDeviceUnmutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(false /* isInputDevice */, false /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.media_output_icon_volume);
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagEnabled_InputDeviceMutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(true /* isInputDevice */, true /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.ic_mic_off);
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagEnabled_OutputDeviceMutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(false /* isInputDevice */, true /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.media_output_icon_volume_off);
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagEnabled_InputDeviceUnmutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(true /* isInputDevice */, false /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.ic_mic_26dp);
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_AUDIO_INPUT_DEVICE_ROUTING_AND_VOLUME_CONTROL)
+ @Test
+ public void getDrawableId_FlagEnabled_OutputDeviceUnmutedIcon() {
+ assertThat(
+ mViewHolder.getDrawableId(false /* isInputDevice */, false /* isMutedVolumeIcon */))
+ .isEqualTo(R.drawable.media_output_icon_volume);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 5d8a8fd..4e7de81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -88,30 +88,26 @@
.thenReturn(statusBarWindowController)
systemClock = FakeSystemClock()
chipAnimationController =
- SystemEventChipAnimationController(
+ SystemEventChipAnimationControllerImpl(
mContext,
- statusBarWindowControllerStore,
+ statusBarWindowController,
statusBarContentInsetProvider,
)
// StatusBarContentInsetProvider is mocked. Ensure that it returns some mocked values.
whenever(statusBarContentInsetProvider.getStatusBarContentInsetsForCurrentRotation())
- .thenReturn(
- Insets.of(/* left = */ 10, /* top = */ 10, /* right = */ 10, /* bottom = */ 0)
- )
+ .thenReturn(Insets.of(/* left= */ 10, /* top= */ 10, /* right= */ 10, /* bottom= */ 0))
whenever(statusBarContentInsetProvider.getStatusBarContentAreaForCurrentRotation())
- .thenReturn(
- Rect(/* left = */ 10, /* top = */ 10, /* right = */ 990, /* bottom = */ 100)
- )
+ .thenReturn(Rect(/* left= */ 10, /* top= */ 10, /* right= */ 990, /* bottom= */ 100))
// StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
// ensure that the chip view is added to a parent view
whenever(statusBarWindowController.addViewToWindow(any(), any())).then {
val statusbarFake = FrameLayout(mContext)
- statusbarFake.layout(/* l = */ 0, /* t = */ 0, /* r = */ 1000, /* b = */ 100)
+ statusbarFake.layout(/* l= */ 0, /* t= */ 0, /* r= */ 1000, /* b= */ 100)
statusbarFake.addView(
it.arguments[0] as View,
- it.arguments[1] as FrameLayout.LayoutParams
+ it.arguments[1] as FrameLayout.LayoutParams,
)
}
}
@@ -386,7 +382,7 @@
scheduleFakeEventWithView(
accessibilityDesc,
mockAnimatableView,
- shouldAnnounceAccessibilityEvent = true
+ shouldAnnounceAccessibilityEvent = true,
)
fastForwardAnimationToState(ANIMATING_OUT)
@@ -405,7 +401,7 @@
scheduleFakeEventWithView(
accessibilityDesc,
mockAnimatableView,
- shouldAnnounceAccessibilityEvent = true
+ shouldAnnounceAccessibilityEvent = true,
)
fastForwardAnimationToState(ANIMATING_OUT)
@@ -424,7 +420,7 @@
scheduleFakeEventWithView(
accessibilityDesc,
mockAnimatableView,
- shouldAnnounceAccessibilityEvent = false
+ shouldAnnounceAccessibilityEvent = false,
)
fastForwardAnimationToState(ANIMATING_OUT)
@@ -637,13 +633,13 @@
private fun scheduleFakeEventWithView(
desc: String?,
view: BackgroundAnimatableView,
- shouldAnnounceAccessibilityEvent: Boolean
+ shouldAnnounceAccessibilityEvent: Boolean,
) {
val fakeEvent =
FakeStatusEvent(
viewCreator = { view },
contentDescription = desc,
- shouldAnnounceAccessibilityEvent = shouldAnnounceAccessibilityEvent
+ shouldAnnounceAccessibilityEvent = shouldAnnounceAccessibilityEvent,
)
systemStatusAnimationScheduler.onStatusEvent(fakeEvent)
}
@@ -668,7 +664,7 @@
dumpManager,
systemClock,
CoroutineScope(StandardTestDispatcher(testScope.testScheduler)),
- logger
+ logger,
)
// add a mock listener
systemStatusAnimationScheduler.addCallback(listener)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
deleted file mode 100644
index 3190171..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/FakeShortcutHelperStartActivity.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyboard.shortcut
-
-import android.content.Intent
-
-class FakeShortcutHelperStartActivity : (Intent) -> Unit {
-
- val startIntents = mutableListOf<Intent>()
-
- override fun invoke(intent: Intent) {
- startIntents += intent
- }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index c2a03d4..fbfaba6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -33,7 +33,6 @@
import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperCategoriesInteractor
import com.android.systemui.keyboard.shortcut.domain.interactor.ShortcutHelperStateInteractor
-import com.android.systemui.keyboard.shortcut.ui.ShortcutHelperActivityStarter
import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
import com.android.systemui.keyguard.data.repository.fakeCommandQueue
import com.android.systemui.kosmos.Kosmos
@@ -45,12 +44,7 @@
import com.android.systemui.settings.fakeUserTracker
var Kosmos.shortcutHelperAppCategoriesShortcutsSource: KeyboardShortcutGroupsSource by
- Kosmos.Fixture {
- AppCategoriesShortcutsSource(
- windowManager,
- testDispatcher,
- )
- }
+ Kosmos.Fixture { AppCategoriesShortcutsSource(windowManager, testDispatcher) }
var Kosmos.shortcutHelperSystemShortcutsSource: KeyboardShortcutGroupsSource by
Kosmos.Fixture { SystemShortcutsSource(mainResources) }
@@ -65,7 +59,7 @@
broadcastDispatcher,
fakeInputManager.inputManager,
testScope,
- testDispatcher
+ testDispatcher,
)
}
@@ -109,7 +103,7 @@
displayTracker,
testScope,
sysUiState,
- shortcutHelperStateRepository
+ shortcutHelperStateRepository,
)
}
@@ -124,18 +118,6 @@
applicationCoroutineScope,
testDispatcher,
shortcutHelperStateInteractor,
- shortcutHelperCategoriesInteractor
- )
- }
-
-val Kosmos.fakeShortcutHelperStartActivity by Kosmos.Fixture { FakeShortcutHelperStartActivity() }
-
-val Kosmos.shortcutHelperActivityStarter by
- Kosmos.Fixture {
- ShortcutHelperActivityStarter(
- applicationContext,
- applicationCoroutineScope,
- shortcutHelperViewModel,
- fakeShortcutHelperStartActivity,
+ shortcutHelperCategoriesInteractor,
)
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index e2d73d1..9a145cb 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -137,9 +137,6 @@
private static RavenwoodConfig sConfig;
private static RavenwoodSystemProperties sProps;
- // TODO: use the real UiAutomation class instead of a mock
- private static UiAutomation sMockUiAutomation;
- private static Set<String> sAdoptedPermissions = Collections.emptySet();
private static boolean sInitialized = false;
/**
@@ -187,7 +184,6 @@
"androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
assertMockitoVersion();
- sMockUiAutomation = createMockUiAutomation();
}
/**
@@ -273,7 +269,7 @@
// Prepare other fields.
config.mInstrumentation = new Instrumentation();
- config.mInstrumentation.basicInit(instContext, targetContext, sMockUiAutomation);
+ config.mInstrumentation.basicInit(instContext, targetContext, createMockUiAutomation());
InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY);
RavenwoodSystemServer.init(config);
@@ -318,7 +314,6 @@
((RavenwoodContext) config.mTargetContext).cleanUp();
config.mTargetContext = null;
}
- sMockUiAutomation.dropShellPermissionIdentity();
Looper.getMainLooper().quit();
Looper.clearMainLooperForTest();
@@ -421,28 +416,30 @@
() -> Class.forName("org.mockito.Matchers"));
}
+ // TODO: use the real UiAutomation class instead of a mock
private static UiAutomation createMockUiAutomation() {
+ final Set[] adoptedPermission = { Collections.emptySet() };
var mock = mock(UiAutomation.class, inv -> {
HostTestUtils.onThrowMethodCalled();
return null;
});
doAnswer(inv -> {
- sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+ adoptedPermission[0] = UiAutomation.ALL_PERMISSIONS;
return null;
}).when(mock).adoptShellPermissionIdentity();
doAnswer(inv -> {
if (inv.getArgument(0) == null) {
- sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+ adoptedPermission[0] = UiAutomation.ALL_PERMISSIONS;
} else {
- sAdoptedPermissions = (Set) Set.of(inv.getArguments());
+ adoptedPermission[0] = Set.of(inv.getArguments());
}
return null;
}).when(mock).adoptShellPermissionIdentity(any());
doAnswer(inv -> {
- sAdoptedPermissions = Collections.emptySet();
+ adoptedPermission[0] = Collections.emptySet();
return null;
}).when(mock).dropShellPermissionIdentity();
- doAnswer(inv -> sAdoptedPermissions).when(mock).getAdoptedShellPermissions();
+ doAnswer(inv -> adoptedPermission[0]).when(mock).getAdoptedShellPermissions();
return mock;
}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 7057cc3..cb4e994 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -192,6 +192,16 @@
}
flag {
+ name: "package_monitor_dedicated_thread"
+ namespace: "accessibility"
+ description: "Runs the A11yManagerService PackageMonitor on a dedicated thread"
+ bug: "348138695"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "manager_package_monitor_logic_fix"
namespace: "accessibility"
description: "Corrects the return values of the HandleForceStop function"
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index ec8908b..c6fe497 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -116,6 +116,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
@@ -901,7 +902,19 @@
private void registerBroadcastReceivers() {
// package changes
mPackageMonitor = new ManagerPackageMonitor(this);
- mPackageMonitor.register(mContext, null, UserHandle.ALL, true);
+ final Looper packageMonitorLooper;
+ if (Flags.packageMonitorDedicatedThread()) {
+ // Use a dedicated thread because the default BackgroundThread used by PackageMonitor
+ // is shared by other components and can get busy, causing a delay and eventual ANR when
+ // responding to broadcasts sent to this PackageMonitor.
+ HandlerThread packageMonitorThread = new HandlerThread(LOG_TAG + " PackageMonitor",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ packageMonitorThread.start();
+ packageMonitorLooper = packageMonitorThread.getLooper();
+ } else {
+ packageMonitorLooper = null;
+ }
+ mPackageMonitor.register(mContext, packageMonitorLooper, UserHandle.ALL, true);
// user change and unlock
IntentFilter intentFilter = new IntentFilter();
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 74908a4..3608360 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -57,8 +57,11 @@
/** Association id -> Transport */
@GuardedBy("mTransports")
private final SparseArray<Transport> mTransports = new SparseArray<>();
+
+ // Use mTransports to synchronize both mTransports and mTransportsListeners to avoid deadlock
+ // between threads that access both
@NonNull
- @GuardedBy("mTransportsListeners")
+ @GuardedBy("mTransports")
private final RemoteCallbackList<IOnTransportsChangedListener> mTransportsListeners =
new RemoteCallbackList<>();
@@ -95,7 +98,7 @@
*/
public void addListener(IOnTransportsChangedListener listener) {
Slog.i(TAG, "Registering OnTransportsChangedListener");
- synchronized (mTransportsListeners) {
+ synchronized (mTransports) {
mTransportsListeners.register(listener);
mTransportsListeners.broadcast(listener1 -> {
// callback to the current listener with all the associations of the transports
@@ -114,7 +117,7 @@
* Remove the listener for receiving callbacks when any of the transports is changed
*/
public void removeListener(IOnTransportsChangedListener listener) {
- synchronized (mTransportsListeners) {
+ synchronized (mTransports) {
mTransportsListeners.unregister(listener);
}
}
@@ -204,7 +207,7 @@
}
private void notifyOnTransportsChanged() {
- synchronized (mTransportsListeners) {
+ synchronized (mTransports) {
mTransportsListeners.broadcast(listener -> {
try {
listener.onTransportsChanged(getAssociationsWithTransport());
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 6405ebb..217ef20 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2757,8 +2757,11 @@
if (isolated) {
if (mIsolatedAppBindArgs == null) {
mIsolatedAppBindArgs = new ArrayMap<>(1);
+ // See b/79378449 about the following exemption.
addServiceToMap(mIsolatedAppBindArgs, "package");
- addServiceToMap(mIsolatedAppBindArgs, "permissionmgr");
+ if (!android.server.Flags.removeJavaServiceManagerCache()) {
+ addServiceToMap(mIsolatedAppBindArgs, "permissionmgr");
+ }
}
return mIsolatedAppBindArgs;
}
@@ -2769,27 +2772,33 @@
// Add common services.
// IMPORTANT: Before adding services here, make sure ephemeral apps can access them too.
// Enable the check in ApplicationThread.bindApplication() to make sure.
+ if (!android.server.Flags.removeJavaServiceManagerCache()) {
+ addServiceToMap(mAppBindArgs, "permissionmgr");
+ addServiceToMap(mAppBindArgs, Context.ALARM_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.DISPLAY_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.NETWORKMANAGEMENT_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.CONNECTIVITY_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.ACCESSIBILITY_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.INPUT_METHOD_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.INPUT_SERVICE);
+ addServiceToMap(mAppBindArgs, "graphicsstats");
+ addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
+ addServiceToMap(mAppBindArgs, "content");
+ addServiceToMap(mAppBindArgs, Context.JOB_SCHEDULER_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.NOTIFICATION_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.VIBRATOR_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.ACCOUNT_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.POWER_SERVICE);
+ addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
+ addServiceToMap(mAppBindArgs, "mount");
+ addServiceToMap(mAppBindArgs, Context.PLATFORM_COMPAT_SERVICE);
+ }
+ // See b/79378449
+ // Getting the window service and package service binder from servicemanager
+ // is blocked for Apps. However they are necessary for apps.
+ // TODO: remove exception
addServiceToMap(mAppBindArgs, "package");
- addServiceToMap(mAppBindArgs, "permissionmgr");
addServiceToMap(mAppBindArgs, Context.WINDOW_SERVICE);
- addServiceToMap(mAppBindArgs, Context.ALARM_SERVICE);
- addServiceToMap(mAppBindArgs, Context.DISPLAY_SERVICE);
- addServiceToMap(mAppBindArgs, Context.NETWORKMANAGEMENT_SERVICE);
- addServiceToMap(mAppBindArgs, Context.CONNECTIVITY_SERVICE);
- addServiceToMap(mAppBindArgs, Context.ACCESSIBILITY_SERVICE);
- addServiceToMap(mAppBindArgs, Context.INPUT_METHOD_SERVICE);
- addServiceToMap(mAppBindArgs, Context.INPUT_SERVICE);
- addServiceToMap(mAppBindArgs, "graphicsstats");
- addServiceToMap(mAppBindArgs, Context.APP_OPS_SERVICE);
- addServiceToMap(mAppBindArgs, "content");
- addServiceToMap(mAppBindArgs, Context.JOB_SCHEDULER_SERVICE);
- addServiceToMap(mAppBindArgs, Context.NOTIFICATION_SERVICE);
- addServiceToMap(mAppBindArgs, Context.VIBRATOR_SERVICE);
- addServiceToMap(mAppBindArgs, Context.ACCOUNT_SERVICE);
- addServiceToMap(mAppBindArgs, Context.POWER_SERVICE);
- addServiceToMap(mAppBindArgs, Context.USER_SERVICE);
- addServiceToMap(mAppBindArgs, "mount");
- addServiceToMap(mAppBindArgs, Context.PLATFORM_COMPAT_SERVICE);
}
return mAppBindArgs;
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 592d89e..c47cad9 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -196,6 +196,7 @@
private final PowerAttributor mPowerAttributor;
private volatile boolean mMonitorEnabled = true;
+ private boolean mRailsStatsCollectionEnabled = true;
private native void getRailEnergyPowerStats(RailStats railStats);
private CharsetDecoder mDecoderStat = StandardCharsets.UTF_8
@@ -312,8 +313,17 @@
}
}
+ public void setRailsStatsCollectionEnabled(boolean railsStatsCollectionEnabled) {
+ mRailsStatsCollectionEnabled = railsStatsCollectionEnabled;
+ }
+
@Override
public void fillRailDataStats(RailStats railStats) {
+ if (!mRailsStatsCollectionEnabled) {
+ railStats.setRailStatsAvailability(false);
+ return;
+ }
+
if (DBG) Slog.d(TAG, "begin getRailEnergyPowerStats");
try {
getRailEnergyPowerStats(railStats);
@@ -423,7 +433,7 @@
mStats = new BatteryStatsImpl(mBatteryStatsConfig, Clock.SYSTEM_CLOCK, mMonotonicClock,
systemDir, mHandler, this, this, mUserManagerUserInfoProvider, mPowerProfile,
mCpuScalingPolicies, mPowerStatsUidResolver);
- mWorker = new BatteryExternalStatsWorker(context, mStats);
+ mWorker = new BatteryExternalStatsWorker(context, mStats, mHandler);
mStats.setExternalStatsSyncLocked(mWorker);
mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
@@ -436,9 +446,12 @@
mCpuScalingPolicies, () -> mStats.getBatteryCapacity(),
mPowerStatsUidResolver);
mPowerStatsScheduler = createPowerStatsScheduler(mContext);
+
+ int accumulatedBatteryUsageStatsSpanSize = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_accumulatedBatteryUsageStatsSpanSize);
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context,
mPowerAttributor, mPowerProfile, mCpuScalingPolicies,
- mPowerStatsStore, Clock.SYSTEM_CLOCK);
+ mPowerStatsStore, accumulatedBatteryUsageStatsSpanSize, Clock.SYSTEM_CLOCK);
mDumpHelper = new BatteryStatsDumpHelperImpl(mBatteryUsageStatsProvider);
mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
@@ -506,7 +519,7 @@
public void systemServicesReady() {
mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
- Flags.accumulateBatteryUsageStats());
+ isBatteryUsageStatsAccumulationSupported());
MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor;
mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU,
@@ -588,6 +601,12 @@
BatteryConsumer.POWER_COMPONENT_CAMERA,
Flags.streamlinedMiscBatteryStats());
+ // Currently unimplemented.
+ mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_MEMORY,
+ Flags.streamlinedMiscBatteryStats());
+ attributor.setPowerComponentSupported(BatteryConsumer.POWER_COMPONENT_MEMORY,
+ Flags.streamlinedMiscBatteryStats());
+
// By convention POWER_COMPONENT_ANY represents custom Energy Consumers
mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_ANY,
Flags.streamlinedMiscBatteryStats());
@@ -631,6 +650,13 @@
registerStatsCallbacks();
}
+ private static boolean isBatteryUsageStatsAccumulationSupported() {
+ return Flags.accumulateBatteryUsageStats()
+ && Flags.streamlinedBatteryStats()
+ && Flags.streamlinedConnectivityBatteryStats()
+ && Flags.streamlinedMiscBatteryStats();
+ }
+
/**
* Notifies BatteryStatsService that the system server is ready.
*/
@@ -776,7 +802,8 @@
private void syncStats(String reason, int flags) {
mStats.collectPowerStatsSamples();
- awaitUninterruptibly(mWorker.scheduleSync(reason, flags));
+ mWorker.scheduleSync(reason, flags);
+ awaitCompletion();
}
private void awaitCompletion() {
@@ -1135,7 +1162,7 @@
.includeVirtualUids()
.setMinConsumedPowerThreshold(minConsumedPowerThreshold);
- if (Flags.accumulateBatteryUsageStats()) {
+ if (isBatteryUsageStatsAccumulationSupported()) {
query.accumulated();
}
@@ -3054,7 +3081,7 @@
if (Flags.streamlinedBatteryStats()) {
pw.println(" --sample: collect and dump a sample of stats for debugging purpose");
}
- if (Flags.accumulateBatteryUsageStats()) {
+ if (isBatteryUsageStatsAccumulationSupported()) {
pw.println(" --accumulated: continuously accumulated since setup or reset-all");
}
pw.println(" <package.name>: optional name of package to filter output by.");
@@ -3670,24 +3697,12 @@
android.Manifest.permission.BATTERY_STATS, null);
}
- Future future;
if (shouldCollectExternalStats()) {
- future = mWorker.scheduleSync("get-health-stats-for-uids",
+ mWorker.scheduleSync("get-health-stats-for-uids",
BatteryExternalStatsWorker.UPDATE_ALL);
- } else {
- future = null;
}
mHandler.post(() -> {
- if (future != null) {
- try {
- // Worker uses a separate thread pool, so waiting here won't cause a deadlock
- future.get();
- } catch (InterruptedException | ExecutionException e) {
- Slog.e(TAG, "Sync failed", e);
- }
- }
-
final long ident = Binder.clearCallingIdentity();
int i = -1;
try {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 37a2fba..78a1fa7 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3567,8 +3567,10 @@
* @see AudioManager#addOnDevicesForAttributesChangedListener(
* AudioAttributes, Executor, OnDevicesForAttributesChangedListener)
*/
+ @android.annotation.EnforcePermission(anyOf = { MODIFY_AUDIO_ROUTING, QUERY_AUDIO_STATE })
public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes,
IDevicesForAttributesCallback callback) {
+ super.addOnDevicesForAttributesChangedListener_enforcePermission();
mAudioSystem.addOnDevicesForAttributesChangedListener(
attributes, false /* forVolume */, callback);
}
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index dc59e66..7892639 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -78,6 +78,7 @@
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.annotations.WeaklyReferencedCallback;
import com.android.internal.util.DumpUtils;
import com.android.server.DisplayThread;
import com.android.server.LocalServices;
@@ -1795,6 +1796,7 @@
/**
* Interface for applying transforms to a given AppWindow.
*/
+ @WeaklyReferencedCallback
public interface ColorTransformController {
/**
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 07343f4..c0aa4cc 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -237,6 +237,11 @@
Flags::enableHasArrSupport
);
+ private final FlagState mAutoBrightnessModeBedtimeWearFlagState = new FlagState(
+ Flags.FLAG_AUTO_BRIGHTNESS_MODE_BEDTIME_WEAR,
+ Flags::autoBrightnessModeBedtimeWear
+ );
+
/**
* @return {@code true} if 'port' is allowed in display layout configuration file.
*/
@@ -503,6 +508,15 @@
public boolean hasArrSupportFlag() {
return mHasArrSupport.isEnabled();
}
+
+ /**
+ * @return {@code true} if bedtime mode specific auto-brightness curve should be loaded and be
+ * applied when bedtime mode is enabled.
+ */
+ public boolean isAutoBrightnessModeBedtimeWearEnabled() {
+ return mAutoBrightnessModeBedtimeWearFlagState.isEnabled();
+ }
+
/**
* dumps all flagstates
* @param pw printWriter
@@ -553,6 +567,7 @@
pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage);
pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
pw.println(" " + mHasArrSupport);
+ pw.println(" " + mAutoBrightnessModeBedtimeWearFlagState);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index ddb2969..36cadf5 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -422,3 +422,11 @@
bug: "361433651"
is_fixed_read_only: true
}
+
+flag {
+ name: "auto_brightness_mode_bedtime_wear"
+ namespace: "wear_frameworks"
+ description: "Feature flag for loading and applying auto-brightness curve while wear bedtime mode enabled."
+ bug: "350617205"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 6ac8b22..6c03214 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -5790,6 +5790,9 @@
}
userInfo.partial = false;
+ if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+ UserManager.invalidateCacheOnUserListChange();
+ }
synchronized (mPackagesLock) {
writeUserLP(userData);
}
@@ -6382,6 +6385,9 @@
// on next startup, in case the runtime stops now before stopping and
// removing the user completely.
userData.info.partial = true;
+ if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+ UserManager.invalidateCacheOnUserListChange();
+ }
// Mark it as disabled, so that it isn't returned any more when
// profiles are queried.
userData.info.flags |= UserInfo.FLAG_DISABLED;
diff --git a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
index 8311034..f90da64 100644
--- a/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/power/stats/BatteryExternalStatsWorker.java
@@ -27,12 +27,11 @@
import android.os.BatteryConsumer;
import android.os.BatteryStats;
import android.os.Bundle;
+import android.os.Handler;
import android.os.OutcomeReceiver;
import android.os.Parcelable;
-import android.os.Process;
import android.os.SynchronousResultReceiver;
import android.os.SystemClock;
-import android.os.ThreadLocalWorkSource;
import android.os.connectivity.WifiActivityEnergyInfo;
import android.power.PowerStatsInternal;
import android.telephony.ModemActivityInfo;
@@ -50,11 +49,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
import java.util.concurrent.Future;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -85,29 +80,23 @@
// stop running.
public static final int UID_FINAL_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS = 10_000;
- private final ScheduledExecutorService mExecutorService =
- Executors.newSingleThreadScheduledExecutor(
- (ThreadFactory) r -> {
- Thread t = new Thread(
- () -> {
- ThreadLocalWorkSource.setUid(Process.myUid());
- r.run();
- },
- "batterystats-worker");
- t.setPriority(Thread.NORM_PRIORITY);
- return t;
- });
+ // Various types of sync, passed to Handler
+ private static final int SYNC_UPDATE = 1;
+ private static final int SYNC_WAKELOCK_CHANGE = 2;
+ private static final int SYNC_BATTERY_LEVEL_CHANGE = 3;
+ private static final int SYNC_PROCESS_STATE_CHANGE = 4;
+ private static final int SYNC_USER_REMOVAL = 5;
+
+ private final Handler mHandler;
@GuardedBy("mStats")
private final BatteryStatsImpl mStats;
@GuardedBy("this")
+ @ExternalUpdateFlag
private int mUpdateFlags = 0;
@GuardedBy("this")
- private Future<?> mCurrentFuture = null;
-
- @GuardedBy("this")
private String mCurrentReason = null;
@GuardedBy("this")
@@ -125,15 +114,6 @@
@GuardedBy("this")
private boolean mUseLatestStates = true;
- @GuardedBy("this")
- private Future<?> mWakelockChangesUpdate;
-
- @GuardedBy("this")
- private Future<?> mBatteryLevelSync;
-
- @GuardedBy("this")
- private Future<?> mProcessStateSync;
-
// If both mStats and mWorkerLock need to be synchronized, mWorkerLock must be acquired first.
private final Object mWorkerLock = new Object();
@@ -190,14 +170,15 @@
}
}
- public BatteryExternalStatsWorker(Context context, BatteryStatsImpl stats) {
- this(new Injector(context), stats);
+ public BatteryExternalStatsWorker(Context context, BatteryStatsImpl stats, Handler handler) {
+ this(new Injector(context), stats, handler);
}
@VisibleForTesting
- BatteryExternalStatsWorker(Injector injector, BatteryStatsImpl stats) {
+ BatteryExternalStatsWorker(Injector injector, BatteryStatsImpl stats, Handler handler) {
mInjector = injector;
mStats = stats;
+ mHandler = handler;
}
public void systemServicesReady() {
@@ -249,20 +230,20 @@
}
@Override
- public synchronized Future<?> scheduleSync(String reason, int flags) {
- return scheduleSyncLocked(reason, flags);
+ public synchronized void scheduleSync(String reason, @ExternalUpdateFlag int flags) {
+ scheduleSyncLocked(reason, flags);
}
@Override
- public synchronized Future<?> scheduleCpuSyncDueToRemovedUid(int uid) {
- return scheduleSyncLocked("remove-uid", UPDATE_CPU);
+ public synchronized void scheduleCpuSyncDueToRemovedUid(int uid) {
+ scheduleSyncLocked("remove-uid", UPDATE_CPU);
}
@Override
- public Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
+ public void scheduleSyncDueToScreenStateChange(@ExternalUpdateFlag int flags, boolean onBattery,
boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
synchronized (BatteryExternalStatsWorker.this) {
- if (mCurrentFuture == null || (mUpdateFlags & UPDATE_CPU) == 0) {
+ if (!mHandler.hasMessages(SYNC_UPDATE) || (mUpdateFlags & UPDATE_CPU) == 0) {
mOnBattery = onBattery;
mOnBatteryScreenOff = onBatteryScreenOff;
mUseLatestStates = false;
@@ -270,91 +251,70 @@
// always update screen state
mScreenState = screenState;
mPerDisplayScreenStates = perDisplayScreenStates;
- return scheduleSyncLocked("screen-state", flags);
+ scheduleSyncLocked("screen-state", flags);
}
}
@Override
- public Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis) {
+ public void scheduleCpuSyncDueToWakelockChange(long delayMillis) {
synchronized (BatteryExternalStatsWorker.this) {
- mWakelockChangesUpdate = scheduleDelayedSyncLocked(mWakelockChangesUpdate,
+ scheduleDelayedSyncLocked(SYNC_WAKELOCK_CHANGE,
() -> {
scheduleSync("wakelock-change", UPDATE_CPU);
scheduleRunnable(() -> mStats.postBatteryNeedsCpuUpdateMsg());
},
delayMillis);
- return mWakelockChangesUpdate;
}
}
@Override
public void cancelCpuSyncDueToWakelockChange() {
- synchronized (BatteryExternalStatsWorker.this) {
- if (mWakelockChangesUpdate != null) {
- mWakelockChangesUpdate.cancel(false);
- mWakelockChangesUpdate = null;
- }
- }
+ mHandler.removeMessages(SYNC_WAKELOCK_CHANGE);
}
@Override
- public Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis) {
+ public void scheduleSyncDueToBatteryLevelChange(long delayMillis) {
synchronized (BatteryExternalStatsWorker.this) {
- mBatteryLevelSync = scheduleDelayedSyncLocked(mBatteryLevelSync,
+ scheduleDelayedSyncLocked(SYNC_BATTERY_LEVEL_CHANGE,
() -> scheduleSync("battery-level", UPDATE_ALL),
delayMillis);
- return mBatteryLevelSync;
}
}
@GuardedBy("this")
private void cancelSyncDueToBatteryLevelChangeLocked() {
- if (mBatteryLevelSync != null) {
- mBatteryLevelSync.cancel(false);
- mBatteryLevelSync = null;
- }
+ mHandler.removeMessages(SYNC_BATTERY_LEVEL_CHANGE);
}
@Override
public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) {
synchronized (BatteryExternalStatsWorker.this) {
- mProcessStateSync = scheduleDelayedSyncLocked(mProcessStateSync,
+ scheduleDelayedSyncLocked(SYNC_PROCESS_STATE_CHANGE,
() -> scheduleSync("procstate-change", flags),
delayMillis);
}
}
public void cancelSyncDueToProcessStateChange() {
- synchronized (BatteryExternalStatsWorker.this) {
- if (mProcessStateSync != null) {
- mProcessStateSync.cancel(false);
- mProcessStateSync = null;
- }
- }
+ mHandler.removeMessages(SYNC_PROCESS_STATE_CHANGE);
}
@Override
- public Future<?> scheduleCleanupDueToRemovedUser(int userId) {
- synchronized (BatteryExternalStatsWorker.this) {
- try {
- // Initial quick clean-up after a user removal
- mExecutorService.schedule(() -> {
- synchronized (mStats) {
- mStats.clearRemovedUserUidsLocked(userId);
- }
- }, UID_QUICK_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS, TimeUnit.MILLISECONDS);
-
- // Final clean-up after a user removal, to take care of UIDs that were running
- // longer than expected
- return mExecutorService.schedule(() -> {
- synchronized (mStats) {
- mStats.clearRemovedUserUidsLocked(userId);
- }
- }, UID_FINAL_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS, TimeUnit.MILLISECONDS);
- } catch (RejectedExecutionException e) {
- return CompletableFuture.failedFuture(e);
+ public void scheduleCleanupDueToRemovedUser(int userId) {
+ // Initial quick clean-up after a user removal
+ mHandler.postDelayed(() -> {
+ synchronized (mStats) {
+ mStats.clearRemovedUserUidsLocked(userId);
}
- }
+ }, SYNC_USER_REMOVAL, UID_QUICK_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS);
+
+ // Final clean-up after a user removal, to take care of UIDs that were running
+ // longer than expected
+ mHandler.postDelayed(() -> {
+ synchronized (mStats) {
+ mStats.clearRemovedUserUidsLocked(userId);
+ }
+ }, SYNC_USER_REMOVAL, UID_FINAL_REMOVAL_AFTER_USER_REMOVAL_DELAY_MILLIS);
}
/**
@@ -368,42 +328,27 @@
* cancel it if needed
*/
@GuardedBy("this")
- private Future<?> scheduleDelayedSyncLocked(Future<?> lastScheduledSync, Runnable syncRunnable,
+ private void scheduleDelayedSyncLocked(int what, Runnable syncRunnable,
long delayMillis) {
- if (mExecutorService.isShutdown()) {
- return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
- }
-
- if (lastScheduledSync != null) {
+ if (mHandler.hasMessages(what)) {
// If there's already a scheduled task, leave it as is if we're trying to
// re-schedule it again with a delay, otherwise cancel and re-schedule it.
if (delayMillis == 0) {
- lastScheduledSync.cancel(false);
+ mHandler.removeMessages(what);
} else {
- return lastScheduledSync;
+ return;
}
}
- try {
- return mExecutorService.schedule(syncRunnable, delayMillis, TimeUnit.MILLISECONDS);
- } catch (RejectedExecutionException e) {
- return CompletableFuture.failedFuture(e);
- }
+ mHandler.postDelayed(syncRunnable, what, delayMillis);
}
- public synchronized Future<?> scheduleWrite() {
- if (mExecutorService.isShutdown()) {
- return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
- }
-
+ /**
+ * Schedule and async writing of battery stats to disk
+ */
+ public synchronized void scheduleWrite() {
scheduleSyncLocked("write", UPDATE_ALL);
- // Since we use a single threaded executor, we can assume the next scheduled task's
- // Future finishes after the sync.
- try {
- return mExecutorService.submit(mWriteTask);
- } catch (RejectedExecutionException e) {
- return CompletableFuture.failedFuture(e);
- }
+ mHandler.post(mWriteTask);
}
/**
@@ -411,34 +356,25 @@
* within the task, never wait on the resulting Future. This will result in a deadlock.
*/
public synchronized void scheduleRunnable(Runnable runnable) {
- try {
- mExecutorService.submit(runnable);
- } catch (RejectedExecutionException e) {
- Slog.e(TAG, "Couldn't schedule " + runnable, e);
- }
+ mHandler.post(runnable);
}
public void shutdown() {
- mExecutorService.shutdownNow();
+ mHandler.removeMessages(SYNC_UPDATE);
+ mHandler.removeMessages(SYNC_WAKELOCK_CHANGE);
+ mHandler.removeMessages(SYNC_BATTERY_LEVEL_CHANGE);
+ mHandler.removeMessages(SYNC_PROCESS_STATE_CHANGE);
+ mHandler.removeMessages(SYNC_USER_REMOVAL);
}
@GuardedBy("this")
- private Future<?> scheduleSyncLocked(String reason, int flags) {
- if (mExecutorService.isShutdown()) {
- return CompletableFuture.failedFuture(new IllegalStateException("worker shutdown"));
- }
-
- if (mCurrentFuture == null) {
+ private void scheduleSyncLocked(String reason, @ExternalUpdateFlag int flags) {
+ if (!mHandler.hasMessages(SYNC_UPDATE)) {
mUpdateFlags = flags;
mCurrentReason = reason;
- try {
- mCurrentFuture = mExecutorService.submit(mSyncTask);
- } catch (RejectedExecutionException e) {
- return CompletableFuture.failedFuture(e);
- }
+ mHandler.postDelayed(mSyncTask, SYNC_UPDATE, 0);
}
mUpdateFlags |= flags;
- return mCurrentFuture;
}
public long getLastCollectionTimeStamp() {
@@ -468,7 +404,6 @@
useLatestStates = mUseLatestStates;
mUpdateFlags = 0;
mCurrentReason = null;
- mCurrentFuture = null;
mUseLatestStates = true;
if ((updateFlags & UPDATE_ALL) == UPDATE_ALL) {
cancelSyncDueToBatteryLevelChangeLocked();
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 391071f..c04158f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -176,7 +176,6 @@
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
@@ -949,19 +948,38 @@
public @interface ExternalUpdateFlag {
}
- Future<?> scheduleSync(String reason, int flags);
- Future<?> scheduleCpuSyncDueToRemovedUid(int uid);
+ /**
+ * Schedules a sync of kernel metrics in accordance with the specified flags.
+ */
+ void scheduleSync(String reason, @ExternalUpdateFlag int flags);
+
+ /**
+ * Schedules a CPU stats sync after a UID removal.
+ */
+ void scheduleCpuSyncDueToRemovedUid(int uid);
/**
* Schedule a sync because of a screen state change.
*/
- Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
+ void scheduleSyncDueToScreenStateChange(@ExternalUpdateFlag int flags, boolean onBattery,
boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates);
- Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis);
+
+ /**
+ * Schedules a sync after a wakelock state change
+ */
+ void scheduleCpuSyncDueToWakelockChange(long delayMillis);
+
+ /**
+ * Canceles any pending sync due to a wakelock state change
+ */
void cancelCpuSyncDueToWakelockChange();
- Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis);
+
+ /**
+ * Schedules a sync caused by the battery level change
+ */
+ void scheduleSyncDueToBatteryLevelChange(long delayMillis);
/** Schedule removal of UIDs corresponding to a removed user */
- Future<?> scheduleCleanupDueToRemovedUser(int userId);
+ void scheduleCleanupDueToRemovedUser(int userId);
/** Schedule a sync because of a process state change */
void scheduleSyncDueToProcessStateChange(int flags, long delayMillis);
}
@@ -12263,14 +12281,8 @@
// start time
long monotonicStartTime =
mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
- mHandler.post(() -> {
- mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
- try {
- batteryUsageStats.close();
- } catch (IOException e) {
- Log.e(TAG, "Cannot close BatteryUsageStats", e);
- }
- });
+ commitMonotonicClock();
+ mPowerStatsStore.storeBatteryUsageStatsAsync(monotonicStartTime, batteryUsageStats);
}
}
@@ -15391,6 +15403,10 @@
mMaxLearnedBatteryCapacityUah = Math.max(mMaxLearnedBatteryCapacityUah, chargeFullUah);
mBatteryTimeToFullSeconds = chargeTimeToFullSeconds;
+
+ if (mAccumulateBatteryUsageStats) {
+ mBatteryUsageStatsProvider.accumulateBatteryUsageStatsAsync(this, mHandler);
+ }
}
public static boolean isOnBattery(int plugType, int status) {
@@ -17699,6 +17715,13 @@
}
}
+ /**
+ * Persists the monotonic clock associated with battery stats.
+ */
+ public void commitMonotonicClock() {
+ mMonotonicClock.write();
+ }
+
@GuardedBy("this")
public void prepareForDumpLocked() {
// Need to retrieve current kernel wake lock stats before printing.
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index b466dd2..265f1dfc 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -23,17 +23,16 @@
import android.os.BatteryStats;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
+import android.os.Handler;
import android.os.Process;
-import android.os.UidBatteryConsumer;
import android.util.Log;
-import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.os.Clock;
import com.android.internal.os.CpuScalingPolicies;
+import com.android.internal.os.MonotonicClock;
import com.android.internal.os.PowerProfile;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -49,20 +48,31 @@
private final PowerStatsStore mPowerStatsStore;
private final PowerProfile mPowerProfile;
private final CpuScalingPolicies mCpuScalingPolicies;
+ private final int mAccumulatedBatteryUsageStatsSpanSize;
private final Clock mClock;
private final Object mLock = new Object();
private List<PowerCalculator> mPowerCalculators;
private UserPowerCalculator mUserPowerCalculator;
+ private long mLastAccumulationMonotonicHistorySize;
+
+ private static class AccumulatedBatteryUsageStats {
+ public BatteryUsageStats.Builder builder;
+ public long startWallClockTime;
+ public long startMonotonicTime;
+ public long endMonotonicTime;
+ }
public BatteryUsageStatsProvider(@NonNull Context context,
@NonNull PowerAttributor powerAttributor,
@NonNull PowerProfile powerProfile, @NonNull CpuScalingPolicies cpuScalingPolicies,
- @NonNull PowerStatsStore powerStatsStore, @NonNull Clock clock) {
+ @NonNull PowerStatsStore powerStatsStore, int accumulatedBatteryUsageStatsSpanSize,
+ @NonNull Clock clock) {
mContext = context;
mPowerAttributor = powerAttributor;
mPowerStatsStore = powerStatsStore;
mPowerProfile = powerProfile;
mCpuScalingPolicies = cpuScalingPolicies;
+ mAccumulatedBatteryUsageStatsSpanSize = accumulatedBatteryUsageStatsSpanSize;
mClock = clock;
mUserPowerCalculator = new UserPowerCalculator();
@@ -85,7 +95,10 @@
mPowerCalculators.add(
new CpuPowerCalculator(mCpuScalingPolicies, mPowerProfile));
}
- mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));
+ if (!mPowerAttributor.isPowerComponentSupported(
+ BatteryConsumer.POWER_COMPONENT_MEMORY)) {
+ mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));
+ }
if (!mPowerAttributor.isPowerComponentSupported(
BatteryConsumer.POWER_COMPONENT_WAKELOCK)) {
mPowerCalculators.add(new WakelockPowerCalculator(mPowerProfile));
@@ -141,7 +154,11 @@
BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY)) {
mPowerCalculators.add(new AmbientDisplayPowerCalculator(mPowerProfile));
}
- mPowerCalculators.add(new IdlePowerCalculator(mPowerProfile));
+ // IDLE power attribution is covered by WakelockPowerStatsProcessor
+ if (!mPowerAttributor.isPowerComponentSupported(
+ BatteryConsumer.POWER_COMPONENT_WAKELOCK)) {
+ mPowerCalculators.add(new IdlePowerCalculator(mPowerProfile));
+ }
if (!mPowerAttributor.isPowerComponentSupported(
BatteryConsumer.POWER_COMPONENT_ANY)) {
mPowerCalculators.add(new CustomEnergyConsumerPowerCalculator(mPowerProfile));
@@ -159,53 +176,45 @@
}
/**
- * Compute BatteryUsageStats for the period since the last accumulated stats were stored,
- * add them to the accumulated stats and save the result.
+ * Conditionally runs a battery usage stats accumulation on the supplied handler.
+ */
+ public void accumulateBatteryUsageStatsAsync(BatteryStatsImpl stats, Handler handler) {
+ synchronized (this) {
+ long historySize = stats.getHistory().getMonotonicHistorySize();
+ if (historySize - mLastAccumulationMonotonicHistorySize
+ < mAccumulatedBatteryUsageStatsSpanSize) {
+ return;
+ }
+ mLastAccumulationMonotonicHistorySize = historySize;
+ }
+
+ handler.post(() -> accumulateBatteryUsageStats(stats));
+ }
+
+ /**
+ * Computes BatteryUsageStats for the period since the last accumulated stats were stored,
+ * adds them to the accumulated stats and saves the result.
*/
public void accumulateBatteryUsageStats(BatteryStatsImpl stats) {
- BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+ AccumulatedBatteryUsageStats accumulatedStats = loadAccumulatedBatteryUsageStats();
- PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
- AccumulatedBatteryUsageStatsSection.ID,
- AccumulatedBatteryUsageStatsSection.TYPE);
- if (powerStatsSpan != null) {
- List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
- for (int i = sections.size() - 1; i >= 0; i--) {
- PowerStatsSpan.Section section = sections.get(i);
- if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
- accumulatedBatteryUsageStatsBuilder =
- ((AccumulatedBatteryUsageStatsSection) section)
- .getBatteryUsageStatsBuilder();
- break;
- }
- }
- }
+ final BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includeProcessStateData()
+ .includePowerStateData()
+ .includeScreenStateData()
+ .build();
+ updateAccumulatedBatteryUsageStats(accumulatedStats, stats, query);
- // TODO(b/366493365): add the current batteryusagestats directly into the "accumulated"
- // builder to avoid allocating a second CursorWindow
- BatteryUsageStats.Builder currentBatteryUsageStatsBuilder =
- getCurrentBatteryUsageStatsBuilder(stats,
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includeProcessStateData()
- .includePowerStateData()
- .includeScreenStateData()
- .build(),
- mClock.currentTimeMillis());
-
- if (accumulatedBatteryUsageStatsBuilder == null) {
- accumulatedBatteryUsageStatsBuilder = currentBatteryUsageStatsBuilder;
- } else {
- accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStatsBuilder.build());
- currentBatteryUsageStatsBuilder.discard();
- }
-
- powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
+ PowerStatsSpan powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
powerStatsSpan.addSection(
- new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder));
-
+ new AccumulatedBatteryUsageStatsSection(accumulatedStats.builder));
+ powerStatsSpan.addTimeFrame(accumulatedStats.startMonotonicTime,
+ accumulatedStats.startWallClockTime,
+ accumulatedStats.endMonotonicTime - accumulatedStats.startMonotonicTime);
+ stats.commitMonotonicClock();
mPowerStatsStore.storePowerStatsSpanAsync(powerStatsSpan,
- accumulatedBatteryUsageStatsBuilder::discard);
+ accumulatedStats.builder::discard);
}
/**
@@ -252,68 +261,73 @@
BatteryUsageStatsQuery query, long currentTimeMs) {
if ((query.getFlags()
& BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_ACCUMULATED) != 0) {
- return getAccumulatedBatteryUsageStats(stats, query);
- } else if (query.getToTimestamp() == 0) {
- return getCurrentBatteryUsageStats(stats, query, currentTimeMs);
+ return getAccumulatedBatteryUsageStats(stats, query, currentTimeMs);
+ } else if (query.getAggregatedToTimestamp() == 0) {
+ BatteryUsageStats.Builder builder = computeBatteryUsageStats(stats, query,
+ query.getMonotonicStartTime(),
+ query.getMonotonicEndTime(), currentTimeMs);
+ return builder.build();
} else {
return getAggregatedBatteryUsageStats(stats, query);
}
}
private BatteryUsageStats getAccumulatedBatteryUsageStats(BatteryStatsImpl stats,
- BatteryUsageStatsQuery query) {
+ BatteryUsageStatsQuery query, long currentTimeMs) {
+ AccumulatedBatteryUsageStats accumulatedStats = loadAccumulatedBatteryUsageStats();
+ updateAccumulatedBatteryUsageStats(accumulatedStats, stats, query);
+ return accumulatedStats.builder.build();
+ }
+
+ private AccumulatedBatteryUsageStats loadAccumulatedBatteryUsageStats() {
+ AccumulatedBatteryUsageStats stats = new AccumulatedBatteryUsageStats();
+ stats.startWallClockTime = 0;
+ stats.startMonotonicTime = MonotonicClock.UNDEFINED;
+ stats.endMonotonicTime = MonotonicClock.UNDEFINED;
PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
AccumulatedBatteryUsageStatsSection.ID,
AccumulatedBatteryUsageStatsSection.TYPE);
-
- BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
if (powerStatsSpan != null) {
List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
- if (sections.size() == 1) {
- accumulatedBatteryUsageStatsBuilder =
- ((AccumulatedBatteryUsageStatsSection) sections.get(0))
- .getBatteryUsageStatsBuilder();
- } else {
- Slog.wtf(TAG, "Unexpected number of sections for type "
- + AccumulatedBatteryUsageStatsSection.TYPE);
+ for (int i = sections.size() - 1; i >= 0; i--) {
+ PowerStatsSpan.Section section = sections.get(i);
+ if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
+ stats.builder = ((AccumulatedBatteryUsageStatsSection) section)
+ .getBatteryUsageStatsBuilder();
+ stats.startWallClockTime = powerStatsSpan.getMetadata().getStartTime();
+ stats.startMonotonicTime = powerStatsSpan.getMetadata().getStartMonotonicTime();
+ stats.endMonotonicTime = powerStatsSpan.getMetadata().getEndMonotonicTime();
+ break;
+ }
}
}
+ return stats;
+ }
- BatteryUsageStats currentBatteryUsageStats = getCurrentBatteryUsageStats(stats, query,
+ private void updateAccumulatedBatteryUsageStats(AccumulatedBatteryUsageStats accumulatedStats,
+ BatteryStatsImpl stats, BatteryUsageStatsQuery query) {
+ // TODO(b/366493365): add the current batteryusagestats directly into
+ // `accumulatedStats.builder` to avoid allocating a second CursorWindow
+ BatteryUsageStats.Builder remainingBatteryUsageStats = computeBatteryUsageStats(stats,
+ query, accumulatedStats.endMonotonicTime, query.getMonotonicEndTime(),
mClock.currentTimeMillis());
- BatteryUsageStats result;
- if (accumulatedBatteryUsageStatsBuilder == null) {
- result = currentBatteryUsageStats;
+ if (accumulatedStats.builder == null) {
+ accumulatedStats.builder = remainingBatteryUsageStats;
+ accumulatedStats.startWallClockTime = stats.getStartClockTime();
+ accumulatedStats.startMonotonicTime = stats.getMonotonicStartTime();
+ accumulatedStats.endMonotonicTime = accumulatedStats.startMonotonicTime
+ + accumulatedStats.builder.getStatsDuration();
} else {
- accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStats);
- try {
- currentBatteryUsageStats.close();
- } catch (IOException ex) {
- Slog.e(TAG, "Closing BatteryUsageStats", ex);
- }
- result = accumulatedBatteryUsageStatsBuilder.build();
+ accumulatedStats.builder.add(remainingBatteryUsageStats.build());
+ accumulatedStats.endMonotonicTime += remainingBatteryUsageStats.getStatsDuration();
+ remainingBatteryUsageStats.discard();
}
-
- return result;
}
- private BatteryUsageStats getCurrentBatteryUsageStats(BatteryStatsImpl stats,
- BatteryUsageStatsQuery query, long currentTimeMs) {
- BatteryUsageStats.Builder builder = getCurrentBatteryUsageStatsBuilder(stats, query,
- currentTimeMs);
- BatteryUsageStats batteryUsageStats = builder.build();
- if (batteryUsageStats.isProcessStateDataIncluded()) {
- verify(batteryUsageStats);
- }
- return batteryUsageStats;
- }
-
- private BatteryUsageStats.Builder getCurrentBatteryUsageStatsBuilder(BatteryStatsImpl stats,
- BatteryUsageStatsQuery query, long currentTimeMs) {
- final long realtimeUs = mClock.elapsedRealtime() * 1000;
- final long uptimeUs = mClock.uptimeMillis() * 1000;
-
+ private BatteryUsageStats.Builder computeBatteryUsageStats(BatteryStatsImpl stats,
+ BatteryUsageStatsQuery query, long monotonicStartTime, long monotonicEndTime,
+ long currentTimeMs) {
final boolean includePowerModels = (query.getFlags()
& BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
final boolean includeProcessStateData = ((query.getFlags()
@@ -324,11 +338,8 @@
final double minConsumedPowerThreshold = query.getMinConsumedPowerThreshold();
String[] customEnergyConsumerNames;
- long monotonicStartTime, monotonicEndTime;
synchronized (stats) {
customEnergyConsumerNames = stats.getCustomEnergyConsumerNames();
- monotonicStartTime = stats.getMonotonicStartTime();
- monotonicEndTime = stats.getMonotonicEndTime();
}
final BatteryUsageStats.Builder batteryUsageStatsBuilder = new BatteryUsageStats.Builder(
@@ -337,12 +348,31 @@
query.isPowerStateDataNeeded(), minConsumedPowerThreshold);
synchronized (stats) {
- // TODO(b/188068523): use a monotonic clock to ensure resilience of order and duration
- // of batteryUsageStats sessions to wall-clock adjustments
- batteryUsageStatsBuilder.setStatsStartTimestamp(stats.getStartClockTime());
- batteryUsageStatsBuilder.setStatsEndTimestamp(currentTimeMs);
final List<PowerCalculator> powerCalculators = getPowerCalculators();
if (!powerCalculators.isEmpty()) {
+ if (monotonicStartTime != MonotonicClock.UNDEFINED
+ || monotonicEndTime != MonotonicClock.UNDEFINED) {
+ throw new IllegalStateException("BatteryUsageStatsQuery specifies a time "
+ + "range that is incompatible with PowerCalculators: "
+ + powerCalculators);
+ }
+ }
+
+ if (monotonicStartTime == MonotonicClock.UNDEFINED) {
+ monotonicStartTime = stats.getMonotonicStartTime();
+ }
+ batteryUsageStatsBuilder.setStatsStartTimestamp(stats.getStartClockTime()
+ + (monotonicStartTime - stats.getMonotonicStartTime()));
+ if (monotonicEndTime != MonotonicClock.UNDEFINED) {
+ batteryUsageStatsBuilder.setStatsEndTimestamp(stats.getStartClockTime()
+ + (monotonicEndTime - stats.getMonotonicStartTime()));
+ } else {
+ batteryUsageStatsBuilder.setStatsEndTimestamp(currentTimeMs);
+ }
+
+ if (!powerCalculators.isEmpty()) {
+ final long realtimeUs = mClock.elapsedRealtime() * 1000;
+ final long uptimeUs = mClock.uptimeMillis() * 1000;
final int[] powerComponents = query.getPowerComponents();
SparseArray<? extends BatteryStats.Uid> uidStats = stats.getUidStats();
for (int i = uidStats.size() - 1; i >= 0; i--) {
@@ -381,8 +411,7 @@
monotonicStartTime, monotonicEndTime);
// Combine apps by the user if necessary
- mUserPowerCalculator.calculate(batteryUsageStatsBuilder, stats, realtimeUs, uptimeUs,
- query);
+ mUserPowerCalculator.calculate(batteryUsageStatsBuilder, stats, 0, 0, query);
populateGeneralInfo(batteryUsageStatsBuilder, stats);
return batteryUsageStatsBuilder;
@@ -402,48 +431,6 @@
}
}
- // STOPSHIP(b/229906525): remove verification before shipping
- private static boolean sErrorReported;
-
- private void verify(BatteryUsageStats stats) {
- if (sErrorReported) {
- return;
- }
-
- final double precision = 2.0; // Allow rounding errors up to 2 mAh
- final int[] components =
- {BatteryConsumer.POWER_COMPONENT_CPU,
- BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
- BatteryConsumer.POWER_COMPONENT_WIFI,
- BatteryConsumer.POWER_COMPONENT_BLUETOOTH};
- final int[] states =
- {BatteryConsumer.PROCESS_STATE_FOREGROUND,
- BatteryConsumer.PROCESS_STATE_BACKGROUND,
- BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE,
- BatteryConsumer.PROCESS_STATE_CACHED};
- for (UidBatteryConsumer ubc : stats.getUidBatteryConsumers()) {
- for (int component : components) {
- double consumedPower = ubc.getConsumedPower(ubc.getKey(component));
- double sumStates = 0;
- for (int state : states) {
- sumStates += ubc.getConsumedPower(ubc.getKey(component, state));
- }
- if (sumStates > consumedPower + precision) {
- String error = "Sum of states exceeds total. UID = " + ubc.getUid() + " "
- + BatteryConsumer.powerComponentIdToString(component)
- + " total = " + consumedPower + " states = " + sumStates;
- if (!sErrorReported) {
- Slog.wtf(TAG, error);
- sErrorReported = true;
- } else {
- Slog.e(TAG, error);
- }
- return;
- }
- }
- }
- }
-
private BatteryUsageStats getAggregatedBatteryUsageStats(BatteryStatsImpl stats,
BatteryUsageStatsQuery query) {
final boolean includePowerModels = (query.getFlags()
@@ -489,8 +476,10 @@
// Per BatteryUsageStatsQuery API, the "from" timestamp is *exclusive*,
// while the "to" timestamp is *inclusive*.
boolean isInRange =
- (query.getFromTimestamp() == 0 || minTime > query.getFromTimestamp())
- && (query.getToTimestamp() == 0 || maxTime <= query.getToTimestamp());
+ (query.getAggregatedFromTimestamp() == 0
+ || minTime > query.getAggregatedFromTimestamp())
+ && (query.getAggregatedToTimestamp() == 0
+ || maxTime <= query.getAggregatedToTimestamp());
if (!isInRange) {
continue;
}
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsSpan.java b/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
index fc0611f..5105272 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsSpan.java
@@ -25,6 +25,7 @@
import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.MonotonicClock;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -147,6 +148,40 @@
mTimeFrames.add(timeFrame);
}
+ long getStartTime() {
+ long startTime = Long.MAX_VALUE;
+ for (int i = 0; i < mTimeFrames.size(); i++) {
+ TimeFrame timeFrame = mTimeFrames.get(i);
+ if (timeFrame.startTime < startTime) {
+ startTime = timeFrame.startTime;
+ }
+ }
+ return startTime != Long.MAX_VALUE ? startTime : 0;
+ }
+
+ long getStartMonotonicTime() {
+ long startTime = Long.MAX_VALUE;
+ for (int i = 0; i < mTimeFrames.size(); i++) {
+ TimeFrame timeFrame = mTimeFrames.get(i);
+ if (timeFrame.startMonotonicTime < startTime) {
+ startTime = timeFrame.startMonotonicTime;
+ }
+ }
+ return startTime != Long.MAX_VALUE ? startTime : MonotonicClock.UNDEFINED;
+ }
+
+ long getEndMonotonicTime() {
+ long maxTime = Long.MIN_VALUE;
+ for (int i = 0; i < mTimeFrames.size(); i++) {
+ TimeFrame timeFrame = mTimeFrames.get(i);
+ long endTime = timeFrame.startMonotonicTime + timeFrame.duration;
+ if (endTime > maxTime) {
+ maxTime = endTime;
+ }
+ }
+ return maxTime != Long.MIN_VALUE ? maxTime : MonotonicClock.UNDEFINED;
+ }
+
void addSection(String sectionType) {
// The number of sections per span is small, so there is no need to use a Set
if (!mSections.contains(sectionType)) {
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
index 5a6f973..3673617 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -149,7 +149,6 @@
* Saves the specified span in the store.
*/
public void storePowerStatsSpan(PowerStatsSpan span) {
- maybeClearLegacyStore();
lockStoreDirectory();
try {
if (!mStoreDir.exists()) {
@@ -203,13 +202,23 @@
* Stores a {@link PowerStatsSpan} containing a single section for the supplied
* battery usage stats.
*/
- public void storeBatteryUsageStats(long monotonicStartTime,
+ public void storeBatteryUsageStatsAsync(long monotonicStartTime,
BatteryUsageStats batteryUsageStats) {
- PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime);
- span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(),
- batteryUsageStats.getStatsDuration());
- span.addSection(new BatteryUsageStatsSection(batteryUsageStats));
- storePowerStatsSpan(span);
+ mHandler.post(() -> {
+ try {
+ PowerStatsSpan span = new PowerStatsSpan(monotonicStartTime);
+ span.addTimeFrame(monotonicStartTime, batteryUsageStats.getStatsStartTimestamp(),
+ batteryUsageStats.getStatsDuration());
+ span.addSection(new BatteryUsageStatsSection(batteryUsageStats));
+ storePowerStatsSpan(span);
+ } finally {
+ try {
+ batteryUsageStats.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Cannot close BatteryUsageStats", e);
+ }
+ }
+ });
}
/**
diff --git a/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java b/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java
index 4a26d83..657701b 100644
--- a/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java
+++ b/services/core/java/com/android/server/power/stats/format/BinaryStatePowerStatsLayout.java
@@ -21,7 +21,9 @@
public class BinaryStatePowerStatsLayout extends EnergyConsumerPowerStatsLayout {
public BinaryStatePowerStatsLayout() {
addDeviceSectionUsageDuration();
+ addDeviceSectionPowerEstimate();
addUidSectionUsageDuration();
+ addUidSectionPowerEstimate();
}
public BinaryStatePowerStatsLayout(PowerStats.Descriptor descriptor) {
diff --git a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
index c7ad564..c8170a1 100644
--- a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
@@ -117,6 +117,7 @@
PowerStatsSpan.Section section = sections.get(k);
populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder,
((AggregatedPowerStatsSection) section).getAggregatedPowerStats());
+ // TODO(b/371614748): close the builder
}
}
@@ -197,6 +198,7 @@
&& powerState == BatteryConsumer.POWER_STATE_BATTERY;
double[] totalPower = new double[1];
+ long[] durationMs = new long[1];
MultiStateStats.States.forEachTrackedStateCombination(
powerComponentStats.getConfig().getDeviceStateConfig(),
states -> {
@@ -209,6 +211,7 @@
}
totalPower[0] += layout.getDevicePowerEstimate(deviceStats);
+ durationMs[0] += layout.getUsageDuration(deviceStats);
if (hasBatteryLevelProperties) {
gatherBatteryLevelInfo(batteryLevelInfo, deviceStats);
@@ -223,9 +226,13 @@
if (key != null) {
deviceScope.addConsumedPower(key, totalPower[0],
BatteryConsumer.POWER_MODEL_UNDEFINED);
+ deviceScope.addUsageDurationMillis(key, durationMs[0]);
}
- deviceScope.addConsumedPower(powerComponentId, totalPower[0],
- BatteryConsumer.POWER_MODEL_UNDEFINED);
+ key = deviceScope.getKey(powerComponentId, BatteryConsumer.PROCESS_STATE_UNSPECIFIED);
+ if (key != null) {
+ deviceScope.addConsumedPower(key, totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED);
+ deviceScope.addUsageDurationMillis(key, durationMs[0]);
+ }
}
private void gatherBatteryLevelInfo(BatteryLevelInfo batteryLevelInfo, long[] deviceStats) {
@@ -373,9 +380,15 @@
if (resultScreenState != BatteryConsumer.SCREEN_STATE_UNSPECIFIED
|| resultPowerState != BatteryConsumer.POWER_STATE_UNSPECIFIED) {
- builder.addConsumedPower(powerComponentId,
- powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED],
- BatteryConsumer.POWER_MODEL_UNDEFINED);
+ BatteryConsumer.Key key = builder.getKey(powerComponentId,
+ BatteryConsumer.PROCESS_STATE_UNSPECIFIED);
+ if (key != null) {
+ builder.addConsumedPower(key,
+ powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED],
+ BatteryConsumer.POWER_MODEL_UNDEFINED);
+ builder.addUsageDurationMillis(key,
+ durationByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED]);
+ }
}
powerAllApps += powerByProcState[BatteryConsumer.PROCESS_STATE_UNSPECIFIED];
}
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 6a4c9c2..a492a72 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -70,10 +70,6 @@
return mIsInTestMode;
}
- public boolean isFlagNetworkMetricMonitorEnabled() {
- return mFeatureFlags.networkMetricMonitor();
- }
-
public boolean isFlagIpSecTransformStateEnabled() {
// TODO: b/328844044: Ideally this code should gate the behavior by checking the
// android.net.platform.flags.ipsec_transform_state flag but that flag is not accessible
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index b574782..a81ad22 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -1913,7 +1913,6 @@
mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform);
if (direction == IpSecManager.DIRECTION_IN
- && mVcnContext.isFlagNetworkMetricMonitorEnabled()
&& mVcnContext.isFlagIpSecTransformStateEnabled()) {
mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform);
}
diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
index b9b1060..0d4c373 100644
--- a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
+++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java
@@ -62,12 +62,6 @@
@Nullable PersistableBundleWrapper carrierConfig,
@NonNull NetworkMetricMonitorCallback callback)
throws IllegalAccessException {
- if (!vcnContext.isFlagNetworkMetricMonitorEnabled()) {
- // Caller error
- logWtf("networkMetricMonitor flag disabled");
- throw new IllegalAccessException("networkMetricMonitor flag disabled");
- }
-
mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
mNetwork = Objects.requireNonNull(network, "Missing network");
mCallback = Objects.requireNonNull(callback, "Missing callback");
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
index 2b0ca08..ad5bc72 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java
@@ -204,8 +204,7 @@
List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
mCellBringupCallbacks.clear();
- if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
- && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+ if (mVcnContext.isFlagIpSecTransformStateEnabled()) {
for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) {
evaluator.close();
}
@@ -431,8 +430,7 @@
.getAllSubIdsInGroup(mSubscriptionGroup)
.equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) {
- if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
- && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+ if (mVcnContext.isFlagIpSecTransformStateEnabled()) {
reevaluateNetworks();
}
return;
@@ -447,8 +445,7 @@
*/
public void updateInboundTransform(
@NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) {
- if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
- || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+ if (!mVcnContext.isFlagIpSecTransformStateEnabled()) {
logWtf("#updateInboundTransform: unexpected call; flags missing");
return;
}
@@ -575,8 +572,7 @@
@Override
public void onLost(@NonNull Network network) {
- if (mVcnContext.isFlagNetworkMetricMonitorEnabled()
- && mVcnContext.isFlagIpSecTransformStateEnabled()) {
+ if (mVcnContext.isFlagIpSecTransformStateEnabled()) {
mUnderlyingNetworkRecords.get(network).close();
}
@@ -652,8 +648,7 @@
class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback {
@Override
public void onEvaluationResultChanged() {
- if (!mVcnContext.isFlagNetworkMetricMonitorEnabled()
- || !mVcnContext.isFlagIpSecTransformStateEnabled()) {
+ if (!mVcnContext.isFlagIpSecTransformStateEnabled()) {
logWtf("#onEvaluationResultChanged: unexpected call; flags missing");
return;
}
diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
index c852fb4..53b0751 100644
--- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
+++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java
@@ -193,8 +193,7 @@
}
private static boolean isIpSecPacketLossDetectorEnabled(VcnContext vcnContext) {
- return vcnContext.isFlagIpSecTransformStateEnabled()
- && vcnContext.isFlagNetworkMetricMonitorEnabled();
+ return vcnContext.isFlagIpSecTransformStateEnabled();
}
/** Get the comparator for UnderlyingNetworkEvaluator */
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 460de01..054f931 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -5508,7 +5508,8 @@
clearAllDrawn();
// Reset the draw state in order to prevent the starting window to be immediately
// dismissed when the app still has the surface.
- if (!isVisible() && !isClientVisible()) {
+ if (!Flags.resetDrawStateOnClientInvisible()
+ && !isVisible() && !isClientVisible()) {
forAllWindows(w -> {
if (w.mWinAnimator.mDrawState == HAS_DRAWN) {
w.mWinAnimator.resetDrawState();
@@ -6852,7 +6853,7 @@
} else if (associatedTask.getActivity(
r -> r.isVisibleRequested() && !r.firstWindowDrawn) == null) {
// The last drawn activity may not be the one that owns the starting window.
- final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
+ final ActivityRecord r = associatedTask.getActivity(ar -> ar.mStartingData != null);
if (r != null) {
r.removeStartingWindow();
}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 70f9ebb..ccd5996 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -201,6 +201,9 @@
infoBuilder.setTouchableRegion(window.getFrame());
infoBuilder.setAppProgressAllowed((window.getAttrs().privateFlags
& PRIVATE_FLAG_APP_PROGRESS_GENERATION_ALLOWED) != 0);
+ if (currentTask != null) {
+ infoBuilder.setFocusedTaskId(currentTask.mTaskId);
+ }
mNavigationMonitor.startMonitor(window, navigationObserver);
ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, "
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 76f2437..6141876 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3304,6 +3304,16 @@
android.os.Process.killProcess(mSession.mPid);
}
}
+
+ // Because the client is notified to be invisible, it should no longer be considered as
+ // drawn state. This prevent the app from showing incomplete content if the app is
+ // requested to be visible in a short time (e.g. before activity stopped).
+ if (Flags.resetDrawStateOnClientInvisible() && !clientVisible && mActivityRecord != null
+ && mWinAnimator.mDrawState == HAS_DRAWN) {
+ mWinAnimator.resetDrawState();
+ // Make sure the app can report drawn if it becomes visible again.
+ forceReportingResized();
+ }
}
void onStartFreezingScreen() {
diff --git a/services/core/xsd/vts/Android.bp b/services/core/xsd/vts/Android.bp
index 4d3c79e..e1478d6 100644
--- a/services/core/xsd/vts/Android.bp
+++ b/services/core/xsd/vts/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_android_kernel",
// 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"
diff --git a/services/java/com/android/server/flags.aconfig b/services/java/com/android/server/flags.aconfig
index 0990691..e2ac22d 100644
--- a/services/java/com/android/server/flags.aconfig
+++ b/services/java/com/android/server/flags.aconfig
@@ -10,6 +10,14 @@
}
flag {
+ name: "remove_java_service_manager_cache"
+ namespace: "system_performance"
+ description: "This flag turns off Java's Service Manager caching mechanism."
+ bug: "333854840"
+ is_fixed_read_only: true
+}
+
+flag {
name: "remove_text_service"
namespace: "wear_frameworks"
description: "Remove TextServiceManagerService on Wear"
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS b/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS
index 6f207fb1..6eb986b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/ALARM_OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/OWNERS b/services/tests/mockingservicestests/src/com/android/server/job/OWNERS
index 6f207fb1..c8345f7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/job/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/JOB_OWNERS
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index 7b635d4..d7b60cf 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -39,7 +39,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.connectivity.WifiActivityEnergyInfo;
-import android.platform.test.ravenwood.RavenwoodRule;
+import android.platform.test.ravenwood.RavenwoodConfig;
import android.power.PowerStatsInternal;
import android.util.IntArray;
import android.util.SparseArray;
@@ -52,7 +52,6 @@
import com.android.internal.os.PowerProfile;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import java.util.Arrays;
@@ -67,25 +66,27 @@
@SuppressWarnings("GuardedBy")
@android.platform.test.annotations.DisabledOnRavenwood
public class BatteryExternalStatsWorkerTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule();
+ @RavenwoodConfig.Config
+ public final RavenwoodConfig mRavenwood = new RavenwoodConfig.Builder().build();
private BatteryExternalStatsWorker mBatteryExternalStatsWorker;
private TestPowerStatsInternal mPowerStatsInternal;
+ private Handler mHandler;
@Before
public void setUp() {
final Context context = InstrumentationRegistry.getContext();
+ mHandler = new Handler(Looper.getMainLooper());
BatteryStatsImpl batteryStats = new BatteryStatsImpl(
new BatteryStatsImpl.BatteryStatsConfig.Builder().build(), Clock.SYSTEM_CLOCK,
new MonotonicClock(0, Clock.SYSTEM_CLOCK), null,
- new Handler(Looper.getMainLooper()), null, null, null,
+ mHandler, null, null, null,
new PowerProfile(context, true /* forTest */), buildScalingPolicies(),
new PowerStatsUidResolver());
mPowerStatsInternal = new TestPowerStatsInternal();
mBatteryExternalStatsWorker =
- new BatteryExternalStatsWorker(new TestInjector(context), batteryStats);
+ new BatteryExternalStatsWorker(new TestInjector(context), batteryStats, mHandler);
}
@Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
index d36b553..d6f5036 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
@@ -38,7 +38,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
-import java.util.concurrent.Future;
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -324,9 +323,8 @@
private boolean mSyncScheduled;
@Override
- public Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis) {
+ public void scheduleCpuSyncDueToWakelockChange(long delayMillis) {
mSyncScheduled = true;
- return null;
}
public boolean isSyncScheduled() {
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index e40a3e3..b67ec8b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -653,6 +653,44 @@
}
@Test
+ public void getMonotonicHistorySize() {
+ long lastHistorySize = mHistory.getMonotonicHistorySize();
+ mHistory.forceRecordAllHistory();
+
+ mClock.realtime = 1000;
+ mClock.uptime = 1000;
+ mHistory.recordEvent(mClock.realtime, mClock.uptime,
+ BatteryStats.HistoryItem.EVENT_JOB_START, "job", 42);
+ long size = mHistory.getMonotonicHistorySize();
+ assertThat(size).isGreaterThan(lastHistorySize);
+ lastHistorySize = size;
+
+ mHistory.startNextFile(mClock.realtime);
+
+ size = mHistory.getMonotonicHistorySize();
+ assertThat(size).isEqualTo(lastHistorySize);
+
+ mClock.realtime = 2000;
+ mClock.uptime = 2000;
+ mHistory.recordEvent(mClock.realtime, mClock.uptime,
+ BatteryStats.HistoryItem.EVENT_JOB_FINISH, "job", 42);
+
+ size = mHistory.getMonotonicHistorySize();
+ assertThat(size).isGreaterThan(lastHistorySize);
+ lastHistorySize = size;
+
+ mHistory.startNextFile(mClock.realtime);
+
+ mClock.realtime = 3000;
+ mClock.uptime = 3000;
+ mHistory.recordEvent(mClock.realtime, mClock.uptime,
+ HistoryItem.EVENT_ALARM, "alarm", 42);
+
+ size = mHistory.getMonotonicHistorySize();
+ assertThat(size).isGreaterThan(lastHistorySize);
+ }
+
+ @Test
public void testVarintParceler() {
long[] values = {
0,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 2408cc1..177f30a 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -159,7 +159,7 @@
}
mPowerStatsStore = new PowerStatsStore(systemDir, mHandler);
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mPowerAttributor,
- mPowerProfile, mBatteryStatsImpl.getCpuScalingPolicies(), mPowerStatsStore,
+ mPowerProfile, mBatteryStatsImpl.getCpuScalingPolicies(), mPowerStatsStore, 0,
mMockClock);
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
index 3931201..5912a99 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
@@ -134,7 +134,7 @@
private int getNumberOfUidsInBatteryStats() throws Exception {
ArraySet<Integer> uids = new ArraySet<>();
- final String dumpsys = executeShellCommand("dumpsys batterystats --checkin");
+ final String dumpsys = executeShellCommand("dumpsys batterystats -c");
for (String line : dumpsys.split("\n")) {
final String[] parts = line.trim().split(",");
if (parts.length < 5 ||
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index b30224b..e9d95fc 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -22,6 +22,7 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -119,7 +120,7 @@
.isWithin(PRECISION).of(0.4);
assertThat(batteryUsageStats.getStatsStartTimestamp()).isEqualTo(12345);
- assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(54321);
+ assertThat(batteryUsageStats.getStatsEndTimestamp()).isEqualTo(180 * MINUTE_IN_MS);
}
@Test
@@ -142,7 +143,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+ mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
final BatteryUsageStats batteryUsageStats =
provider.getBatteryUsageStats(batteryStats,
@@ -249,7 +250,8 @@
}
}
- mStatsRule.setCurrentTime(54321);
+ setTime(180 * MINUTE_IN_MS);
+
return batteryStats;
}
@@ -266,7 +268,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
powerAttributor, mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+ mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
return provider.getBatteryUsageStats(batteryStats, BatteryUsageStatsQuery.DEFAULT);
}
@@ -296,7 +298,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+ mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
final BatteryUsageStats batteryUsageStats =
provider.getBatteryUsageStats(batteryStats,
@@ -385,7 +387,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+ mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
final BatteryUsageStats batteryUsageStats =
provider.getBatteryUsageStats(batteryStats,
@@ -474,7 +476,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+ mStatsRule.getCpuScalingPolicies(), powerStatsStore, 0, mMockClock);
batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
/* accumulateBatteryUsageStats */ false);
@@ -564,8 +566,19 @@
}
@Test
- public void accumulateBatteryUsageStats() {
+ public void accumulateBatteryUsageStats() throws Throwable {
+ accumulateBatteryUsageStats(10000000, 1);
+ // Accumulate every 200 bytes of battery history
+ accumulateBatteryUsageStats(200, 2);
+ accumulateBatteryUsageStats(50, 5);
+ // Accumulate on every invocation of accumulateBatteryUsageStats
+ accumulateBatteryUsageStats(0, 7);
+ }
+
+ private void accumulateBatteryUsageStats(int accumulatedBatteryUsageStatsSpanSize,
+ int expectedNumberOfUpdates) throws Throwable {
BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+ batteryStats.forceRecordAllHistory();
setTime(5 * MINUTE_IN_MS);
@@ -574,69 +587,86 @@
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
- PowerStatsStore powerStatsStore = new PowerStatsStore(
+ PowerStatsStore powerStatsStore = spy(new PowerStatsStore(
new File(mStatsRule.getHistoryDir(), getClass().getSimpleName()),
- mStatsRule.getHandler());
+ mStatsRule.getHandler()));
powerStatsStore.reset();
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
- mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+ int[] count = new int[1];
+ doAnswer(inv -> {
+ count[0]++;
+ return null;
+ }).when(powerStatsStore).storePowerStatsSpan(any(PowerStatsSpan.class));
- batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
- /* accumulateBatteryUsageStats */ true);
+ MultiStatePowerAttributor powerAttributor = new MultiStatePowerAttributor(mContext,
+ powerStatsStore, mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(),
+ () -> 3500, new PowerStatsUidResolver());
+ for (int powerComponentId = 0; powerComponentId < BatteryConsumer.POWER_COMPONENT_COUNT;
+ powerComponentId++) {
+ powerAttributor.setPowerComponentSupported(powerComponentId, true);
+ }
+ powerAttributor.setPowerComponentSupported(BatteryConsumer.POWER_COMPONENT_ANY, true);
+
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
+ powerAttributor, mStatsRule.getPowerProfile(),
+ mStatsRule.getCpuScalingPolicies(), powerStatsStore,
+ accumulatedBatteryUsageStatsSpanSize, mMockClock);
+
+ provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
synchronized (batteryStats) {
batteryStats.noteFlashlightOnLocked(APP_UID,
10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
}
+
+ provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
+
synchronized (batteryStats) {
batteryStats.noteFlashlightOffLocked(APP_UID,
20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
}
- synchronized (batteryStats) {
- batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
- }
+ provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
synchronized (batteryStats) {
batteryStats.noteFlashlightOnLocked(APP_UID,
30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
}
+
+ provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
+
synchronized (batteryStats) {
batteryStats.noteFlashlightOffLocked(APP_UID,
50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
}
setTime(55 * MINUTE_IN_MS);
- synchronized (batteryStats) {
- batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
- }
+
+ provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
// This section has not been saved yet, but should be added to the accumulated totals
synchronized (batteryStats) {
batteryStats.noteFlashlightOnLocked(APP_UID,
80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
}
+
+ provider.accumulateBatteryUsageStatsAsync(batteryStats, mStatsRule.getHandler());
+
synchronized (batteryStats) {
batteryStats.noteFlashlightOffLocked(APP_UID,
110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
}
setTime(115 * MINUTE_IN_MS);
- // Await completion
- ConditionVariable done = new ConditionVariable();
- mStatsRule.getHandler().post(done::open);
- done.block();
+ // Pick up the remainder of battery history that has not yet been accumulated
+ provider.accumulateBatteryUsageStats(batteryStats);
+
+ mStatsRule.waitForBackgroundThread();
BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats,
new BatteryUsageStatsQuery.Builder().accumulated().build());
-
assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
- // Section 1 (saved): 20 - 10 = 10
- // Section 2 (saved): 50 - 30 = 20
- // Section 3 (fresh): 110 - 80 = 30
// Total: 10 + 20 + 30 = 60
assertThat(stats.getAggregateBatteryConsumer(
BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
@@ -657,6 +687,8 @@
assertThat(uidBatteryConsumer
.getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
.isEqualTo(60 * MINUTE_IN_MS);
+
+ assertThat(count[0]).isEqualTo(expectedNumberOfUpdates);
}
private void setTime(long timeMs) {
@@ -682,7 +714,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), mMockClock);
+ mStatsRule.getCpuScalingPolicies(), mock(PowerStatsStore.class), 0, mMockClock);
PowerStatsStore powerStatsStore = mock(PowerStatsStore.class);
doAnswer(invocation -> {
@@ -702,7 +734,7 @@
assertThat(uid.getConsumedPower(componentId1))
.isWithin(PRECISION).of(8.33333);
return null;
- }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+ }).when(powerStatsStore).storeBatteryUsageStatsAsync(anyLong(), any());
mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore,
/* accumulateBatteryUsageStats */ false);
@@ -714,7 +746,7 @@
mStatsRule.waitForBackgroundThread();
- verify(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
+ verify(powerStatsStore).storeBatteryUsageStatsAsync(anyLong(), any());
}
@Test
@@ -746,7 +778,7 @@
BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
- mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+ mStatsRule.getCpuScalingPolicies(), powerStatsStore, 0, mMockClock);
BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder()
.aggregateSnapshots(0, 3000)
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 2c03f9d..1e4454c 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -43,7 +43,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Queue;
-import java.util.concurrent.Future;
/**
* Mocks a BatteryStatsImpl object.
@@ -288,30 +287,25 @@
public int flags = 0;
@Override
- public Future<?> scheduleSync(String reason, int flags) {
- return null;
+ public void scheduleSync(String reason, int flags) {
}
@Override
- public Future<?> scheduleCleanupDueToRemovedUser(int userId) {
- return null;
+ public void scheduleCleanupDueToRemovedUser(int userId) {
}
@Override
- public Future<?> scheduleCpuSyncDueToRemovedUid(int uid) {
- return null;
+ public void scheduleCpuSyncDueToRemovedUid(int uid) {
}
@Override
- public Future<?> scheduleSyncDueToScreenStateChange(int flag, boolean onBattery,
+ public void scheduleSyncDueToScreenStateChange(int flag, boolean onBattery,
boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
flags |= flag;
- return null;
}
@Override
- public Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis) {
- return null;
+ public void scheduleCpuSyncDueToWakelockChange(long delayMillis) {
}
@Override
@@ -319,8 +313,7 @@
}
@Override
- public Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis) {
- return null;
+ public void scheduleSyncDueToBatteryLevelChange(long delayMillis) {
}
@Override
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
index 03491bc..0d5d277 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java
@@ -65,12 +65,14 @@
private WakelockPowerStatsLayout mStatsLayout = new WakelockPowerStatsLayout();
@Before
- public void setup() {
- mBatteryStats = new MockBatteryStatsImpl(mClock);
+ public void setup() throws Throwable {
+ mBatteryStats = mStatsRule.getBatteryStats();
mBatteryStats.setPowerStatsCollectorEnabled(POWER_COMPONENT_WAKELOCK, true);
mBatteryStats.getPowerStatsCollector(POWER_COMPONENT_WAKELOCK)
.addConsumer(ps -> mPowerStats = ps);
mBatteryStats.onSystemReady(mock(Context.class));
+ // onSystemReady schedules the initial power stats collection. Wait for it to finish
+ mStatsRule.waitForBackgroundThread();
}
@Test
@@ -79,9 +81,6 @@
PowerStatsCollector powerStatsCollector = mBatteryStats.getPowerStatsCollector(
POWER_COMPONENT_WAKELOCK);
- // Establish a baseline
- powerStatsCollector.collectAndDeliverStats();
-
mBatteryStats.forceRecordAllHistory();
mStatsRule.advanceSuspendedTime(1000);
diff --git a/services/tests/servicestests/src/com/android/server/OWNERS b/services/tests/servicestests/src/com/android/server/OWNERS
index d49bc43..d8a9400 100644
--- a/services/tests/servicestests/src/com/android/server/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/OWNERS
@@ -1,4 +1,4 @@
-per-file *Alarm* = file:/apex/jobscheduler/OWNERS
+per-file *Alarm* = file:/apex/jobscheduler/ALARM_OWNERS
per-file *AppOp* = file:/core/java/android/permission/OWNERS
per-file *BinaryTransparency* = file:/core/java/android/transparency/OWNERS
per-file *Bluetooth* = file:platform/packages/modules/Bluetooth:master:/framework/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java b/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
index 467c15d..ea70287 100644
--- a/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BatteryStatsServiceTest.java
@@ -55,6 +55,7 @@
File systemDir = context.getCacheDir();
Handler handler = new Handler(mBgThread.getLooper());
mBatteryStatsService = new BatteryStatsService(context, systemDir);
+ mBatteryStatsService.setRailsStatsCollectionEnabled(false);
}
@After
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 577b02a..c30b4bb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3214,23 +3214,32 @@
assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
}
+ @SetupWindows(addWindows = W_ACTIVITY)
@Test
public void testSetVisibility_visibleToInvisible() {
- final ActivityRecord activity = new ActivityBuilder(mAtm)
- .setCreateTask(true).build();
+ final TestTransitionPlayer player = registerTestTransitionPlayer();
+ final ActivityRecord activity = mAppWindow.mActivityRecord;
+ makeWindowVisibleAndDrawn(mAppWindow);
// By default, activity is visible.
assertTrue(activity.isVisible());
assertTrue(activity.isVisibleRequested());
- assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
+ assertTrue(mAppWindow.isDrawn());
+ assertFalse(mAppWindow.setReportResizeHints());
// Request the activity to be invisible. Since the visibility changes, app transition
// animation should be applied on this activity.
- mDisplayContent.prepareAppTransition(0);
+ activity.mTransitionController.requestCloseTransitionIfNeeded(activity);
activity.setVisibility(false);
assertTrue(activity.isVisible());
assertFalse(activity.isVisibleRequested());
- assertFalse(activity.mDisplayContent.mOpeningApps.contains(activity));
- assertTrue(activity.mDisplayContent.mClosingApps.contains(activity));
+
+ player.start();
+ mSetFlagsRule.enableFlags(Flags.FLAG_RESET_DRAW_STATE_ON_CLIENT_INVISIBLE);
+ // ActivityRecord#commitVisibility(false) -> WindowState#sendAppVisibilityToClients().
+ player.finish();
+ assertFalse(activity.isVisible());
+ assertFalse("Reset draw state after committing invisible", mAppWindow.isDrawn());
+ assertTrue("Set pending redraw hint", mAppWindow.setReportResizeHints());
}
@Test
diff --git a/tests/JobSchedulerPerfTests/OWNERS b/tests/JobSchedulerPerfTests/OWNERS
index 6f207fb1..c8345f7 100644
--- a/tests/JobSchedulerPerfTests/OWNERS
+++ b/tests/JobSchedulerPerfTests/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/JOB_OWNERS
diff --git a/tests/JobSchedulerTestApp/OWNERS b/tests/JobSchedulerTestApp/OWNERS
index 6f207fb1..c8345f7 100644
--- a/tests/JobSchedulerTestApp/OWNERS
+++ b/tests/JobSchedulerTestApp/OWNERS
@@ -1 +1 @@
-include /apex/jobscheduler/OWNERS
+include /apex/jobscheduler/JOB_OWNERS
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
index e29e462..e045f10 100644
--- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java
@@ -224,7 +224,6 @@
doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags();
doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled();
doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
- doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
doReturn(mUnderlyingNetworkController)
.when(mDeps)
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
index 421e1ad..bc7ff47 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java
@@ -127,7 +127,6 @@
false /* isInTestMode */));
doNothing().when(mVcnContext).ensureRunningOnLooperThread();
- doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
setupSystemService(
diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
index 588624b..6f31d8d 100644
--- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java
@@ -226,7 +226,6 @@
private void resetVcnContext(VcnContext vcnContext) {
reset(vcnContext);
doNothing().when(vcnContext).ensureRunningOnLooperThread();
- doReturn(true).when(vcnContext).isFlagNetworkMetricMonitorEnabled();
doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled();
}