diff options
155 files changed, 4164 insertions, 2077 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index fd6180068fd6..b95295cd4ad9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10864,6 +10864,7 @@ package android.content { 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 @@ package android.security.keystore { 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 @@ package android.view { 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 4fc90ae9d22c..d84a4c12a2cd 100644 --- a/core/java/android/animation/AnimationHandler.java +++ b/core/java/android/animation/AnimationHandler.java @@ -81,6 +81,25 @@ public class AnimationHandler { */ 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 @@ public class AnimationHandler { } } + /** + * 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 c58624e9dfab..d1eb8e8fa2ff 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.compat.annotation.UnsupportedAppUsage; 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 @@ public abstract class Animator implements Cloneable { 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 @@ public abstract class Animator implements Cloneable { } /** + * @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 @@ public abstract class Animator implements Cloneable { } } + 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 ac3711366ec7..76098db2dc5b 100644 --- a/core/java/android/animation/AnimatorSet.java +++ b/core/java/android/animation/AnimatorSet.java @@ -1442,6 +1442,8 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim } private void endAnimation() { + final boolean postNotifyEndListener = sPostNotifyEndListenerEnabled && mListeners != null + && mLastFrameTime > 0; mStarted = false; mLastFrameTime = -1; mFirstFrame = -1; @@ -1453,7 +1455,12 @@ public final class AnimatorSet extends Animator implements AnimationHandler.Anim // 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 5de7f387b206..e849abaf4aec 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1289,6 +1289,8 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio if (mAnimationEndRequested) { return; } + final boolean postNotifyEndListener = sPostNotifyEndListenerEnabled && mListeners != null + && mLastFrameTime > 0; removeAnimationCallback(); mAnimationEndRequested = true; @@ -1303,15 +1305,20 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio 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 ba71afb49629..6e4c28f3eca6 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 @@ per-file InstantAppResolveInfo.aidl = file:/services/core/java/com/android/serve # 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 c17da249f322..e4d3baa2f05a 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.text.TextUtils; 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.io.PrintWriter; 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 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * 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 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = propertyName; validateCacheKey(mPropertyName); mCacheName = cacheName; + mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = new DefaultComputer<>(this); mCache = createMap(); @@ -441,6 +798,7 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = createPropertyName(module, api); validateCacheKey(mPropertyName); mCacheName = cacheName; + mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = computer; mCache = createMap(); @@ -484,130 +842,69 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * 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); - } - - // Read the system property associated with the current cache. This method uses the - // handle for faster reading. - 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. - } + synchronized (sGlobalLock) { + if (sTestMode == false) { + throw new IllegalStateException("cannot test property name with test mode off"); } + mNonce.setTestMode(true); } - // 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); + // Read the nonce associated with the current cache. + @GuardedBy("mLock") + private long getCurrentNonce() { + 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 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { * @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 @@ public class PropertyInvalidatedCache<Query, Result> { * @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 @@ public class PropertyInvalidatedCache<Query, Result> { * @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 @@ public class PropertyInvalidatedCache<Query, Result> { * @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 @@ public class PropertyInvalidatedCache<Query, Result> { @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 class PropertyInvalidatedCache<Query, Result> { } 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { /** * 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { 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 @@ public class PropertyInvalidatedCache<Query, Result> { // 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 cc90ce582048..bd26db55052b 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -236,6 +236,7 @@ import android.security.advancedprotection.AdvancedProtectionManager; 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 @@ public final class SystemServiceRegistry { } }); + 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 ffa33757599b..07106e8359d4 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4770,6 +4770,18 @@ public abstract class Context { /** * 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 6d9dc456f38d..392f62a87302 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 efddd1f9fc6f..5b306243fc36 100644 --- a/core/java/android/net/vcn/flags.aconfig +++ b/core/java/android/net/vcn/flags.aconfig @@ -14,11 +14,4 @@ flag { 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 a698b9d97215..ddcb5c60ab80 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -1031,7 +1031,10 @@ public final class BatteryUsageStats implements Parcelable, Closeable { 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 b533225192ba..e68c4ca5c070 100644 --- a/core/java/android/os/BatteryUsageStatsQuery.java +++ b/core/java/android/os/BatteryUsageStatsQuery.java @@ -20,6 +20,8 @@ import android.annotation.IntDef; 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { @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 @@ public final class BatteryUsageStatsQuery implements Parcelable { : 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { } /** + * Returns the exclusive lower bound of the battery history that should be included in + * the aggregated battery usage stats. + */ + 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 #getToTimestamp()} is zero. + * in the aggregation. Ignored if {@link #getAggregatedToTimestamp()} is zero. */ - public long getFromTimestamp() { - return mFromTimestamp; + public long getAggregatedFromTimestamp() { + return mAggregatedFromTimestamp; } /** @@ -175,8 +198,8 @@ public final class BatteryUsageStatsQuery implements Parcelable { * 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { } /** + * 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { */ // 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 b2d926044869..6afb8e097121 100644 --- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java @@ -754,7 +754,7 @@ public final class MessageQueue { // 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 a1b75034442e..590ddb404b63 100644 --- a/core/java/android/os/OWNERS +++ b/core/java/android/os/OWNERS @@ -122,3 +122,6 @@ per-file StatsBootstrapAtom.aidl = file:/services/core/java/com/android/server/s 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 80c24a9003e8..023359726f90 100644 --- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java @@ -712,7 +712,7 @@ public final class MessageQueue { // 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 7f7ef0467e08..f893739fcf28 100644 --- a/core/java/android/os/UidBatteryConsumer.java +++ b/core/java/android/os/UidBatteryConsumer.java @@ -109,9 +109,11 @@ public final class UidBatteryConsumer extends BatteryConsumer { * 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 acf4a2f3ec71..fa99f3563de9 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -5602,14 +5602,30 @@ public class UserManager { 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 class UserManager { */ 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 aedf8e097e67..1d35344e5218 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -113,3 +113,10 @@ flag { 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 14d5800e4db7..5295b606dd19 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -33,6 +33,7 @@ import android.telephony.ims.MediaThreshold; 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 @@ import java.util.stream.Collectors; * 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 18080e4478fc..fc7a65dbdc41 100644 --- a/core/java/android/view/DisplayEventReceiver.java +++ b/core/java/android/view/DisplayEventReceiver.java @@ -24,6 +24,7 @@ import android.os.MessageQueue; 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 @@ import java.lang.ref.WeakReference; * * @hide */ +@WeaklyReferencedCallback public abstract class DisplayEventReceiver { /** diff --git a/core/java/android/view/LetterboxScrollProcessor.java b/core/java/android/view/LetterboxScrollProcessor.java index dc736d6abf7d..1364a82e60a1 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 android.os.Handler; 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 @@ import java.util.Set; * * @hide */ +@VisibleForTesting(visibility = PACKAGE) public class LetterboxScrollProcessor { private enum LetterboxScrollState { @@ -53,6 +58,7 @@ public class LetterboxScrollProcessor { /** 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 @@ public class LetterboxScrollProcessor { * @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 @@ public class LetterboxScrollProcessor { 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 @@ public class LetterboxScrollProcessor { * @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 1ea58bcbb76a..80484a6328e0 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/service/autofil 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 82235d252a72..9cad3e58fa02 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.FloatRange; 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 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } /** - * 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 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall * 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 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall 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 bb61ae49259c..1b86f96d7eb7 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -119,6 +119,14 @@ flag { } 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 59639d04b45b..6cefc4d2fecf 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 @@ public final class BackNavigationInfo implements Parcelable { private final Rect mTouchableRegion; private final boolean mAppProgressGenerationAllowed; + private final int mFocusedTaskId; /** * Create a new {@link BackNavigationInfo} instance. @@ -135,7 +138,8 @@ public final class BackNavigationInfo implements Parcelable { @Nullable CustomAnimationInfo customAnimationInfo, int letterboxColor, @Nullable Rect touchableRegion, - boolean appProgressGenerationAllowed) { + boolean appProgressGenerationAllowed, + int focusedTaskId) { mType = type; mOnBackNavigationDone = onBackNavigationDone; mOnBackInvokedCallback = onBackInvokedCallback; @@ -145,6 +149,7 @@ public final class BackNavigationInfo implements Parcelable { mLetterboxColor = letterboxColor; mTouchableRegion = new Rect(touchableRegion); mAppProgressGenerationAllowed = appProgressGenerationAllowed; + mFocusedTaskId = focusedTaskId; } private BackNavigationInfo(@NonNull Parcel in) { @@ -157,6 +162,7 @@ public final class BackNavigationInfo implements Parcelable { mLetterboxColor = in.readInt(); mTouchableRegion = in.readTypedObject(Rect.CREATOR); mAppProgressGenerationAllowed = in.readBoolean(); + mFocusedTaskId = in.readInt(); } /** @hide */ @@ -171,6 +177,7 @@ public final class BackNavigationInfo implements Parcelable { dest.writeInt(mLetterboxColor); dest.writeTypedObject(mTouchableRegion, flags); dest.writeBoolean(mAppProgressGenerationAllowed); + dest.writeInt(mFocusedTaskId); } /** @@ -238,6 +245,14 @@ public final class BackNavigationInfo implements Parcelable { } /** + * @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 @@ public final class BackNavigationInfo implements Parcelable { private int mLetterboxColor = Color.TRANSPARENT; private Rect mTouchableRegion; private boolean mAppProgressGenerationAllowed; + private int mFocusedTaskId = INVALID_TASK_ID; /** * @see BackNavigationInfo#getType() @@ -527,6 +543,14 @@ public final class BackNavigationInfo implements Parcelable { } /** + * @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 @@ public final class BackNavigationInfo implements Parcelable { mCustomAnimationInfo, mLetterboxColor, mTouchableRegion, - mAppProgressGenerationAllowed); + mAppProgressGenerationAllowed, + mFocusedTaskId); } } } diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java index 9a7bce0c52ee..5397da11eb36 100644 --- a/core/java/android/window/SnapshotDrawerUtils.java +++ b/core/java/android/window/SnapshotDrawerUtils.java @@ -151,7 +151,9 @@ public class SnapshotDrawerUtils { @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 0d235ffad9b5..d15f52c39efa 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -9,6 +9,16 @@ flag { } 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 d474c6db4f02..6448f10f01b9 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_CA 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 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai @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 d61785e5f3e9..07aa7206c0e7 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -83,7 +83,7 @@ public class BatteryStatsHistory { 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 @@ public class BatteryStatsHistory { 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 @@ public class BatteryStatsHistory { } // 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 @@ public class BatteryStatsHistory { } // skip monotonic time field. out.readLong(); + // skip monotonic size field + out.readLong(); return true; } @@ -987,6 +993,7 @@ public class BatteryStatsHistory { 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 @@ public class BatteryStatsHistory { // 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 @@ public class BatteryStatsHistory { } 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 @@ public class BatteryStatsHistory { } mHistoryBufferStartTime = in.readLong(); + mMonotonicHistorySize = in.readLong(); + mHistoryBuffer.setDataSize(0); mHistoryBuffer.setDataPosition(0); @@ -2370,6 +2381,7 @@ public class BatteryStatsHistory { 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 @@ public class BatteryStatsHistory { } /** + * 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 a137ea97b3a4..519bf9a7fa4b 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 d109cee5d910..faed4d80f39b 100644 --- a/core/res/OWNERS +++ b/core/res/OWNERS @@ -53,7 +53,7 @@ per-file res/values/config_battery_saver.xml = file:/services/core/java/com/andr 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 80cf0881e8cc..9498273e17e0 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 b4249557b4c9..0b2b3453f20a 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 eb463fd9a76b..04698465e971 100644 --- a/core/tests/coretests/src/android/animation/ValueAnimatorTests.java +++ b/core/tests/coretests/src/android/animation/ValueAnimatorTests.java @@ -18,6 +18,8 @@ package android.animation; 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 @@ public class ValueAnimatorTests { } @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 b5ee1302fc1d..dcea5b299829 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -19,6 +19,8 @@ package android.app; 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 android.platform.test.ravenwood.RavenwoodRule; 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 class PropertyInvalidatedCacheTests { 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 @@ public class PropertyInvalidatedCacheTests { 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 @@ public class PropertyInvalidatedCacheTests { 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 @@ public class PropertyInvalidatedCacheTests { 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 64f77b309829..5c56fdcd4a2a 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 android.platform.test.ravenwood.RavenwoodRule; 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 class IpcDataCacheTest { 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 @@ public class IpcDataCacheTest { 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 @@ public class IpcDataCacheTest { 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 @@ public class IpcDataCacheTest { 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 @@ public class IpcDataCacheTest { 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 @@ public class IpcDataCacheTest { TestCache(IpcDataCache.Config c, TestQuery query) { super(c, query); mQuery = query; - setTestMode(true); - testPropertyName(); } int getRecomputeCount() { @@ -456,4 +451,52 @@ public class IpcDataCacheTest { 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 f8ec9f4fb51b..235625d8f14c 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.Handler; 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 @@ public class LetterboxScrollProcessorTest { 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,12 +89,31 @@ public class LetterboxScrollProcessorTest { } @Test + 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). - List<MotionEvent> tapGestureEvents = createTapGestureEvents(-100f, -100f); + final List<MotionEvent> tapGestureEvents = createTapGestureEvents( + /* startX= */ -100f, /* startY= */ -100f); // Get processed events from Letterbox Scroll Processor. - List<MotionEvent> processedEvents = processMotionEvents(tapGestureEvents); + 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 @@ public class LetterboxScrollProcessorTest { @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 @@ public class LetterboxScrollProcessorTest { @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 @@ public class LetterboxScrollProcessorTest { @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 @@ public class LetterboxScrollProcessorTest { // 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 @@ public class LetterboxScrollProcessorTest { @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 @@ public class LetterboxScrollProcessorTest { 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 @@ public class LetterboxScrollProcessorTest { 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 @@ public class LetterboxScrollProcessorTest { } 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 @@ public class LetterboxScrollProcessorTest { } } - 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 c3a5b19c9442..1cbc7d6d3f98 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.spy; 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 @@ public class FrameTrackerTest { } @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 5d8407f584bc..239eed0c44f0 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 ed17fdefcb53..792e248ce839 100644 --- a/graphics/java/android/graphics/text/PositionedGlyphs.java +++ b/graphics/java/android/graphics/text/PositionedGlyphs.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; 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 @@ public final class PositionedGlyphs { 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 @@ public final class PositionedGlyphs { 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 ed30587418dd..32759b2402a9 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 000000000000..197aaba4bcb5 --- /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 99100de12684..dcc8844b59bd 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 @@ public class AndroidKeyStoreProvider extends Provider { } /** - * 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 class AndroidKeyStoreProvider extends Provider { 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 0b3be327b521..bcf619b66439 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java @@ -44,6 +44,22 @@ public abstract class AndroidKeyStorePublicKey extends AndroidKeyStoreKey implem 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 7f11feaa585e..2ab0310d6789 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 @@ class WindowExtensionsImpl implements WindowExtensions { */ 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 @@ class WindowExtensionsImpl implements WindowExtensions { */ @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 19b51f143241..e4db7b636ed9 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mBackTransitionHandler = new BackTransitionHandler(); mTransitions.addHandler(mBackTransitionHandler); mHandler = handler; + mTransitions.registerObserver(mBackTransitionObserver); + mBackTransitionObserver.setBackTransitionHandler(mBackTransitionHandler); updateTouchableArea(); } @@ -729,6 +734,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } /** + * @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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } if (shouldCancelAnimation(info)) { + mPrepareOpenTransition = null; return false; } @@ -1645,4 +1663,58 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont 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 13a805aef0f1..e71b4f3abf14 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 @@ class DesktopHandleManageWindowsMenu( 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 05391a8343a5..173bc08970ca 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.PixelFormat 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 @@ import java.util.function.Supplier */ 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 @@ class DesktopHeaderManageWindowsMenu( 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 @@ class DesktopHeaderManageWindowsMenu( } 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 @@ class DesktopHeaderManageWindowsMenu( .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 @@ class DesktopHeaderManageWindowsMenu( 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 d3b7ca15856f..6eb20b9e3ae5 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin && Flags.enableDesktopWindowingMultiInstanceFeatures(); final boolean shouldShowManageWindowsButton = supportsMultiInstance && mMinimumInstancesFound; + final boolean inDesktopImmersive = mDesktopRepository + .isTaskInFullImmersiveState(mTaskInfo.taskId); mHandleMenu = mHandleMenuFactory.create( this, mWindowManagerWrapper, @@ -1271,7 +1279,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /* onOutsideTouchListener= */ () -> { closeHandleMenu(); return Unit.INSTANCE; - } + }, + /* forceShowSystemBars= */ inDesktopImmersive ); if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { notifyCaptionStateChanged(); @@ -1316,9 +1329,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 2e327035bddf..93bd9290dfeb 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 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 @@ class HandleMenu( 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 @@ class HandleMenu( get() = openInBrowserIntent != null init { - updateHandleMenuPillPositions(captionX) + updateHandleMenuPillPositions(captionX, captionY) } fun show( @@ -123,6 +125,7 @@ class HandleMenu( onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, onOutsideTouchListener: () -> Unit, + forceShowSystemBars: Boolean = false, ) { val ssg = SurfaceSyncGroup(TAG) val t = SurfaceControl.Transaction() @@ -139,6 +142,7 @@ class HandleMenu( onOpenByDefaultClickListener = onOpenByDefaultClickListener, onCloseMenuClickListener = onCloseMenuClickListener, onOutsideTouchListener = onOutsideTouchListener, + forceShowSystemBars = forceShowSystemBars, ) ssg.addTransaction(t) ssg.markSyncReady() @@ -157,7 +161,8 @@ class HandleMenu( openInBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, - onOutsideTouchListener: () -> Unit + onOutsideTouchListener: () -> Unit, + forceShowSystemBars: Boolean = false, ) { val handleMenuView = HandleMenuView( context = context, @@ -185,7 +190,7 @@ class HandleMenu( 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 @@ class HandleMenu( 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 @@ class HandleMenu( /** * 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 @@ class HandleMenu( 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 @@ class HandleMenu( 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 @@ class HandleMenu( */ 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 @@ interface HandleMenuFactory { openInBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, - captionX: Int + captionX: Int, + captionY: Int, ): HandleMenu } @@ -694,7 +702,8 @@ object DefaultHandleMenuFactory : HandleMenuFactory { openInBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, - captionX: Int + captionX: Int, + captionY: Int, ): HandleMenu { return HandleMenu( parentDecor, @@ -709,7 +718,8 @@ object DefaultHandleMenuFactory : HandleMenuFactory { 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 ce5cfd0bdc36..6b3b357f2f7b 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 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> 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 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> int mCaptionHeight; int mCaptionWidth; int mCaptionX; + int mCaptionY; + int mCaptionTopPadding; final Region mCustomizableCaptionRegion = Region.obtain(); int mWidth; int mHeight; @@ -797,6 +801,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> 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 1be26f080ac8..8b6aaaf619e0 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.Gravity 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 @@ class AdditionalSystemViewContainer( 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 @@ class AdditionalSystemViewContainer( 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 000000000000..f9f760e3f482 --- /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 1d11d2e8ff06..320887212f54 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { openInBrowserCaptor.capture(), any(), any(), - any() + any(), + anyBoolean() ); openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1)); @@ -1099,7 +1101,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { openInBrowserCaptor.capture(), any(), any(), - any() + any(), + anyBoolean() ); openInBrowserCaptor.getValue().invoke(new Intent(Intent.ACTION_MAIN, TEST_URI1)); @@ -1151,7 +1154,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), closeClickListener.capture(), - any() + any(), + anyBoolean() ); closeClickListener.getValue().invoke(); @@ -1161,6 +1165,30 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @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 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { 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 1820133a4795..9544fa823b5a 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.LayoutInflater 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 @@ class HandleMenuTest : ShellTestCase() { 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 @@ class HandleMenuTest : ShellTestCase() { .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 @@ class HandleMenuTest : ShellTestCase() { 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 @@ class HandleMenuTest : ShellTestCase() { 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( + onToDesktopClickListener = mock(), + onToFullscreenClickListener = mock(), + onToSplitScreenClickListener = mock(), + onNewWindowClickListener = mock(), + onManageWindowsClickListener = mock(), + openInBrowserClickListener = mock(), + onOpenByDefaultClickListener = mock(), + onCloseMenuClickListener = mock(), + onOutsideTouchListener = mock(), + forceShowSystemBars = forceShowSystemBars ) - handleMenu.show(mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock(), mock()) 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 bb41e9c81ece..fb17ae93030d 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 @@ public class WindowDecorationTests extends ShellTestCase { } @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 02ca307eb2ee..c22b67462af2 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -471,6 +471,7 @@ interface IAudioService { 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 9a9a0735d089..17d1ff6a86a7 100644 --- a/media/java/android/media/flags/projection.aconfig +++ b/media/java/android/media/flags/projection.aconfig @@ -10,3 +10,11 @@ flag { 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 06214eb6e743..8ef4c58d8985 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 @@ abstract class CatalystScreenTestCase { @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 9726ecf74e06..510c9b742e74 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 94f884673fbd..0b15d230dee2 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_OLD_NONE; 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 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner 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 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner } }; } + + /** + * 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 025c8b9dce04..f426aa597a84 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 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : } """ + 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 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : 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 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : 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 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : 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 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : */ fun setLumaMatteFactors( lumaMatteBlendFactor: Float = 1f, - lumaMatteOverallBrightness: Float = 0f + lumaMatteOverallBrightness: Float = 0f, ) { setFloatUniform("in_lumaMatteBlendFactor", lumaMatteBlendFactor) setFloatUniform("in_lumaMatteOverallBrightness", lumaMatteOverallBrightness) @@ -279,8 +297,10 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : /** 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 c1c3b1fb6c5a..d08df26ea8da 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 @@ private fun BesideUserSwitcherLayout( .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 @@ private fun UserSwitcher(viewModel: BouncerSceneContentViewModel, modifier: Modi Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = modifier, + modifier = modifier.sysuiResTag("UserSwitcher"), ) { selectedUserImage?.let { Image( @@ -781,7 +778,7 @@ private fun UserSwitcher(viewModel: BouncerSceneContentViewModel, modifier: Modi 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 @@ private fun UserSwitcherDropdownMenu( 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 3a9587c5fe23..521330f60fa8 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.foundation.lazy.grid.LazyGridState 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.platform.LocalDensity 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 @@ private fun BoxScope.DragHandle( /** * 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 @@ private fun BoxScope.DragHandle( * @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 @@ fun ResizableItemFrame( } 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 a0fed9064181..339445e3483a 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.foundation.gestures.Orientation 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 @@ class DraggableHandlerTest { 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 @@ class DraggableHandlerTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) nestedScroll.onPreScroll( available = downOffset(fractionOfScreen = 0.1f), - source = NestedScrollSource.Drag, + source = UserInput, ) assertIdle(currentScene = SceneA) } @@ -750,7 +745,7 @@ class DraggableHandlerTest { nestedScroll.onPostScroll( consumed = Offset.Zero, available = Offset.Zero, - source = NestedScrollSource.Drag, + source = UserInput, ) assertIdle(currentScene = SceneA) @@ -764,7 +759,7 @@ class DraggableHandlerTest { nestedScroll.onPostScroll( consumed = Offset.Zero, available = downOffset(fractionOfScreen = 0.1f), - source = NestedScrollSource.Drag, + source = UserInput, ) assertTransition(currentScene = SceneA) @@ -784,16 +779,12 @@ class DraggableHandlerTest { 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 @@ class DraggableHandlerTest { 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 index f8e2f47f939a..d1431eecfc68 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 @@ import org.junit.runner.RunWith @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 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { 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 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { } @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 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { testHelper.toggle(deviceId = 456) - assertThat(fakeStartActivity.startIntents).isEmpty() - } - - @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) + assertThat(starter.dialog).isNull() } @Test + @UiThreadTest fun start_onRequestShowShortcuts_startsActivity() = testScope.runTest { starter.start() testHelper.showFromActivity() - verifyShortcutHelperActivityStarted() + assertThat(starter.dialog?.isShowing).isTrue() } @Test @@ -140,10 +132,11 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { 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 @@ class ShortcutHelperActivityStarterTest : SysuiTestCase() { 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) - } - - 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() + assertThat(starter.dialog?.isShowing).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 a629b2447921..5f3668af6e45 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.animation.AnimatorTestRule 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 @@ import org.mockito.MockitoAnnotations @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 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() { @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 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() { ) statusbarFake.addView( it.arguments[0] as View, - it.arguments[1] as FrameLayout.LayoutParams + it.arguments[1] as FrameLayout.LayoutParams, ) } @@ -86,16 +83,16 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() { /* 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 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() { 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 97fa6eb17b5b..75479ad35725 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { @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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { scrollConnection.onPostScroll( consumed = Offset.Zero, available = availableOffset, - source = NestedScrollSource.Drag + source = UserInput, ) assertThat(offsetConsumed).isEqualTo(availableOffset) @@ -165,7 +150,7 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { scrollConnection.onPostScroll( consumed = Offset.Zero, available = availableOffset, - source = NestedScrollSource.Drag + source = UserInput, ) assertThat(offsetConsumed).isEqualTo(consumableOffset) @@ -180,7 +165,7 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { 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 000000000000..d2a3a19e291c --- /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 9d990b1d7edf..9a6a6997b96f 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.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.SysuiTestCase import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationTestHelper @@ -90,7 +91,7 @@ class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() { private fun fakeConversationLayout( mockDrawableGroupMessage: AnimatedImageDrawable, - mockDrawableImageMessage: AnimatedImageDrawable + mockDrawableImageMessage: AnimatedImageDrawable, ): View { val mockMessagingImageMessage: MessagingImageMessage = mock<MessagingImageMessage>().apply { @@ -126,6 +127,7 @@ class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() { 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 286f01736497..dbe90a5b85a9 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 androidx.test.filters.SmallTest 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 @@ import org.junit.runner.RunWith @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 47fd78a4d368..000000000000 --- 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 77edf588bd2e..000000000000 --- 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 17ba2e5c10c4..d5d52e333caf 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 7cebac2feaba..bda34530e6eb 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 6cee28be78f1..0c29466d8dc5 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.Rect 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 @@ class CameraProtectionLoaderImpl @Inject constructor(private val context: Contex 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 @@ class CameraProtectionLoaderImpl @Inject constructor(private val context: Contex physicalCameraId, protectionPath, protectionBounds, - displayUniqueId + displayUniqueId, ) } @@ -95,4 +98,9 @@ class CameraProtectionLoaderImpl @Inject constructor(private val context: Contex 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 58680a88d670..442a1e4d60a7 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 044312bcfe14..6fc50fb1f460 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 @@ interface ScreenDecorationsModule { @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 7309599d9c04..b4cb1033ab82 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.graphics.Rect 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 @@ constructor( * 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 @@ constructor( /* 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 50970a5d006f..8b5fde384837 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 @@ interface AudioSharingModule { 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 cbed21cf65d6..bfd6b5bd6ddd 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.Display 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 c60cad8419d2..16e73f5ba8e2 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 3bc4f342c566..88580cf02f15 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.keyguard.KeyguardUpdateMonitor 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 @@ class FaceScanningProviderFactory @Inject constructor( } // 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 @@ class FaceScanningProviderFactory @Inject constructor( // 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 @@ class FaceScanningOverlayProviderImpl( 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 @@ class FaceScanningOverlayProviderImpl( 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 @@ class FaceScanningOverlayProviderImpl( 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 DisplayCutout.getBoundBaseOnCurrentRotation(): List<Int> { 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 14ecc66f555e..9aa7fd100068 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.LayoutInflater 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 @@ open class PrivacyDotDecorProviderFactory @Inject constructor( 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 @@ class PrivacyDotCornerDecorProviderImpl( 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 @@ class PrivacyDotCornerDecorProviderImpl( reloadToken: Int, rotation: Int, tintColor: Int, - displayUniqueId: String? + displayUniqueId: String?, ) { // Do nothing here because it is handled inside PrivacyDotViewController } @@ -95,7 +98,7 @@ class PrivacyDotCornerDecorProviderImpl( 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 2f2c952fb778..39fd551e15d3 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 @@ import com.android.systemui.res.R 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 906f600e3826..7b3380a6a608 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.CurrentAppShortcuts 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 @@ import dagger.multibindings.IntoMap interface ShortcutHelperModule { @Binds - @IntoMap - @ClassKey(ShortcutHelperActivity::class) - fun activity(impl: ShortcutHelperActivity): Activity - - @Binds @SystemShortcuts fun systemShortcutsSource(impl: SystemShortcutsSource): KeyboardShortcutGroupsSource @@ -73,8 +66,8 @@ interface ShortcutHelperModule { 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 fbf52e773599..000000000000 --- 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 000000000000..d33ab2acc8fb --- /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 1f0d696eebd6..e295564a740f 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 @@ import com.android.compose.windowsizeclass.LocalWindowSizeClass 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 52263ce64a85..000000000000 --- 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 2961d05b2e51..742f43533e6f 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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { // 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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { && 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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } 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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { // 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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { 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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { && !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 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { 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 63a7e013022a..574ccee28faa 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.NonNull; 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 @@ public abstract class MediaOutputBaseAdapter extends // 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 @@ public abstract class MediaOutputBaseAdapter extends } } else if (currentVolume == 0) { mSeekBar.resetVolume(); - updateMutedVolumeIcon(); + updateMutedVolumeIcon(device); } if (currentVolume == mLatestUpdateVolume) { mLatestUpdateVolume = -1; @@ -365,7 +368,7 @@ public abstract class MediaOutputBaseAdapter extends 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 @@ public abstract class MediaOutputBaseAdapter extends 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 @@ public abstract class MediaOutputBaseAdapter extends }); } - 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 @@ public abstract class MediaOutputBaseAdapter extends 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 8d3f7284e359..30f564f63fa1 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 @@ public class NotificationGroupingUtil { 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 class NotificationGroupingUtil { 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 @@ public class NotificationGroupingUtil { mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow); mApply = !mComparator.isEmpty(mParentView); } + public void compareToGroupParent(ExpandableNotificationRow row) { if (!mApply) { return; @@ -356,12 +368,21 @@ public class NotificationGroupingUtil { } 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 f6f4503b210a..f65ae67efbf1 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 @@ abstract class StatusBarModule { @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 ed964821a5c1..415d99045000 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.Binds import dagger.Module import dagger.Provides -@Module +@Module(includes = [SystemEventChipAnimationControllerModule::class]) interface StatusBarEventsModule { companion object { @@ -41,4 +41,4 @@ interface StatusBarEventsModule { 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 bf7e879bb72f..35816c25e976 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.AnimatorListenerAdapter 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 @@ class SystemEventChipAnimationController @Inject constructor( 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 dotSize = context.resources.getDimensionPixelSize( - R.dimen.ongoing_appops_dot_diameter) + 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 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 @@ class SystemEventChipAnimationController @Inject constructor( // 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)) - - updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()) - } + (animationWindowView.parent as View).height, + AT_MOST, + ), + ) + + 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 @@ class SystemEventChipAnimationController @Inject constructor( override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator { initializeAnimRect() - val finish = if (hasPersistentDot) { - createMoveOutAnimationForDot() - } else { - createMoveOutAnimationDefault() - } + val finish = + if (hasPersistentDot) { + createMoveOutAnimationForDot() + } else { + createMoveOutAnimationDefault() + } - finish.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - animationWindowView.removeView(currentAnimatedView!!.view) + 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 @@ class SystemEventChipAnimationController @Inject constructor( } 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 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 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) - - // 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() + 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() + } } } - }) + ) } - /** Announces [contentDescriptions] for accessibility. */ - fun announceForAccessibility(contentDescriptions: String) { + override fun announceForAccessibility(contentDescriptions: String) { currentAnimatedView?.view?.announceForAccessibility(contentDescriptions) } @@ -283,9 +338,9 @@ class SystemEventChipAnimationController @Inject constructor( } /** - * 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 @@ class SystemEventChipAnimationController @Inject constructor( } 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 @@ class SystemEventChipAnimationController @Inject constructor( 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 @@ class SystemEventChipAnimationController @Inject constructor( */ 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 000000000000..2ee1dffd14f3 --- /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 38e66099022c..933f79341b27 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } + /** + * 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 79defd255de0..7b85bfdfb197 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.graphics.drawable.Drawable 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 @@ constructor( 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 b4411f1a33a5..f8aff69f0531 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.ExpandableNotificationRow 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 @@ class NotificationConversationTemplateViewWrapper constructor( 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 @@ class NotificationConversationTemplateViewWrapper constructor( 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 @@ class NotificationConversationTemplateViewWrapper constructor( // 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 @@ class NotificationConversationTemplateViewWrapper constructor( 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 @@ class NotificationConversationTemplateViewWrapper constructor( 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 d2a17c2ccbb4..ad58a0175a39 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 @@ class CameraProtectionLoaderImplTest : SysuiTestCase() { 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 @@ class CameraProtectionLoaderImplTest : SysuiTestCase() { 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 @@ class CameraProtectionLoaderImplTest : SysuiTestCase() { 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 bc12aaaa52d2..a01fecaaaefe 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.keyguard.KeyguardUpdateMonitor 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 @@ class FaceScanningProviderFactoryTest : SysuiTestCase() { 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 61c7e1d63e51..ef33210dd6b7 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { ) 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { ) 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 @@ class SysUICutoutProviderTest : SysuiTestCase() { 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 fd550b05fdc9..6e36d42bd99f 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 @@ class BouncerContentTest : SysuiTestCase() { }, layout = BouncerSceneLayout.BESIDE_USER_SWITCHER, modifier = Modifier.fillMaxSize().testTag("BouncerContent"), - dialogFactory = bouncerDialogFactory + dialogFactory = bouncerDialogFactory, ) } } @@ -110,11 +110,19 @@ class BouncerContentTest : SysuiTestCase() { } } ) { - 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 8731853e4939..63ec78fd9ee5 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 static org.mockito.Mockito.when; 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.core.graphics.drawable.IconCompat; 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 @@ public class MediaOutputAdapterTest extends SysuiTestCase { 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 5d8a8fd03bc0..4e7de8101342 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 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { .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 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { scheduleFakeEventWithView( accessibilityDesc, mockAnimatableView, - shouldAnnounceAccessibilityEvent = true + shouldAnnounceAccessibilityEvent = true, ) fastForwardAnimationToState(ANIMATING_OUT) @@ -405,7 +401,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { scheduleFakeEventWithView( accessibilityDesc, mockAnimatableView, - shouldAnnounceAccessibilityEvent = true + shouldAnnounceAccessibilityEvent = true, ) fastForwardAnimationToState(ANIMATING_OUT) @@ -424,7 +420,7 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { scheduleFakeEventWithView( accessibilityDesc, mockAnimatableView, - shouldAnnounceAccessibilityEvent = false + shouldAnnounceAccessibilityEvent = false, ) fastForwardAnimationToState(ANIMATING_OUT) @@ -637,13 +633,13 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { 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 @@ class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() { 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 3190171b180d..000000000000 --- 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 c2a03d46cd30..fbfaba60ebf0 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.MultitaskingShortcutsS 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.displayTracker 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 @@ val Kosmos.shortcutHelperStateRepository by broadcastDispatcher, fakeInputManager.inputManager, testScope, - testDispatcher + testDispatcher, ) } @@ -109,7 +103,7 @@ val Kosmos.shortcutHelperStateInteractor by displayTracker, testScope, sysUiState, - shortcutHelperStateRepository + shortcutHelperStateRepository, ) } @@ -124,18 +118,6 @@ val Kosmos.shortcutHelperViewModel by 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 e2d73d1f1cb5..9a145cb93811 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 @@ public class RavenwoodRuntimeEnvironmentController { 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 @@ public class RavenwoodRuntimeEnvironmentController { "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"); assertMockitoVersion(); - sMockUiAutomation = createMockUiAutomation(); } /** @@ -273,7 +269,7 @@ public class RavenwoodRuntimeEnvironmentController { // 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 @@ public class RavenwoodRuntimeEnvironmentController { ((RavenwoodContext) config.mTargetContext).cleanUp(); config.mTargetContext = null; } - sMockUiAutomation.dropShellPermissionIdentity(); Looper.getMainLooper().quit(); Looper.clearMainLooperForTest(); @@ -421,28 +416,30 @@ public class RavenwoodRuntimeEnvironmentController { () -> 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 7057cc361a1a..cb4e9949a8ff 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -192,6 +192,16 @@ flag { } 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 ec8908bc7c91..c6fe4971b9e5 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.Binder; 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 74908a4613be..36083607bfcd 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 @@ public class CompanionTransportManager { /** 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 class CompanionTransportManager { */ 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 @@ public class CompanionTransportManager { * 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 @@ public class CompanionTransportManager { } 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 6405ebbb3dd5..217ef205a3e8 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 @@ public class ActivityManagerService extends IActivityManager.Stub 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 @@ public class ActivityManagerService extends IActivityManager.Stub // 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 592d89eca285..c47cad955202 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 final class BatteryStatsService extends IBatteryStats.Stub } } + 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 final class BatteryStatsService extends IBatteryStats.Stub public void systemServicesReady() { mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore, - Flags.accumulateBatteryUsageStats()); + isBatteryUsageStatsAccumulationSupported()); MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor; mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU, @@ -588,6 +601,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub .includeVirtualUids() .setMinConsumedPowerThreshold(minConsumedPowerThreshold); - if (Flags.accumulateBatteryUsageStats()) { + if (isBatteryUsageStatsAccumulationSupported()) { query.accumulated(); } @@ -3054,7 +3081,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 37a2fba8fcb5..78a1fa768879 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 @@ public class AudioService extends IAudioService.Stub * @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 dc59e66d85f2..7892639fc8ed 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 android.view.animation.AnimationUtils; 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 @@ public final class ColorDisplayService extends SystemService { /** * 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 07343f469ed7..c0aa4cc6fa24 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 @@ public class DisplayManagerFlags { 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 class DisplayManagerFlags { 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 @@ public class DisplayManagerFlags { 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 ddb29691f42e..36cadf5271c4 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 @@ flag { 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 6ac8b221b21f..6c03214a2610 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 @@ public class UserManagerService extends IUserManager.Stub { } userInfo.partial = false; + if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) { + UserManager.invalidateCacheOnUserListChange(); + } synchronized (mPackagesLock) { writeUserLP(userData); } @@ -6382,6 +6385,9 @@ public class UserManagerService extends IUserManager.Stub { // 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 8311034c0298..f90da644c0ce 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.net.wifi.WifiManager; 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 com.android.server.LocalServices; 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat // 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat @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 class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat } } - 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat } @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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat // 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); + 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 - 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); + // 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat * 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat * 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 @@ public class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStat 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 391071ff9fe8..c04158fe5243 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.List; 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 class BatteryStatsImpl extends BatteryStats { 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 @@ public class BatteryStatsImpl extends BatteryStats { // 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 @@ public class BatteryStatsImpl extends BatteryStats { 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 @@ public class BatteryStatsImpl extends BatteryStats { } } + /** + * 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 b466dd2d8721..265f1dfce90f 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.BatteryConsumer; 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { } /** - * 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 accumulateBatteryUsageStats(BatteryStatsImpl stats) { - BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null; - - 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; - } + public void accumulateBatteryUsageStatsAsync(BatteryStatsImpl stats, Handler handler) { + synchronized (this) { + long historySize = stats.getHistory().getMonotonicHistorySize(); + if (historySize - mLastAccumulationMonotonicHistorySize + < mAccumulatedBatteryUsageStatsSpanSize) { + return; } + mLastAccumulationMonotonicHistorySize = historySize; } - // 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(); - } + handler.post(() -> accumulateBatteryUsageStats(stats)); + } - powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID); - powerStatsSpan.addSection( - new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder)); + /** + * 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) { + AccumulatedBatteryUsageStats accumulatedStats = loadAccumulatedBatteryUsageStats(); + final BatteryUsageStatsQuery query = new BatteryUsageStatsQuery.Builder() + .setMaxStatsAgeMs(0) + .includeProcessStateData() + .includePowerStateData() + .includeScreenStateData() + .build(); + updateAccumulatedBatteryUsageStats(accumulatedStats, stats, query); + + PowerStatsSpan powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID); + powerStatsSpan.addSection( + 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { } } - // 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 @@ public class BatteryUsageStatsProvider { // 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 fc0611f7fcff..5105272e6980 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.Slog; 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 @@ public class PowerStatsSpan { 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 5a6f973424d1..3673617f6104 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 @@ public class PowerStatsStore { * Saves the specified span in the store. */ public void storePowerStatsSpan(PowerStatsSpan span) { - maybeClearLegacyStore(); lockStoreDirectory(); try { if (!mStoreDir.exists()) { @@ -203,13 +202,23 @@ public class PowerStatsStore { * 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 4a26d83175fa..657701be60cc 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 @@ import com.android.internal.os.PowerStats; 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 c7ad5641806c..c8170a1982bb 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 @@ class PowerStatsExporter { PowerStatsSpan.Section section = sections.get(k); populateBatteryUsageStatsBuilder(batteryUsageStatsBuilder, ((AggregatedPowerStatsSection) section).getAggregatedPowerStats()); + // TODO(b/371614748): close the builder } } @@ -197,6 +198,7 @@ class PowerStatsExporter { && 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 @@ class PowerStatsExporter { } totalPower[0] += layout.getDevicePowerEstimate(deviceStats); + durationMs[0] += layout.getUsageDuration(deviceStats); if (hasBatteryLevelProperties) { gatherBatteryLevelInfo(batteryLevelInfo, deviceStats); @@ -223,9 +226,13 @@ class PowerStatsExporter { if (key != null) { deviceScope.addConsumedPower(key, totalPower[0], BatteryConsumer.POWER_MODEL_UNDEFINED); + deviceScope.addUsageDurationMillis(key, durationMs[0]); + } + 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]); } - deviceScope.addConsumedPower(powerComponentId, totalPower[0], - BatteryConsumer.POWER_MODEL_UNDEFINED); } private void gatherBatteryLevelInfo(BatteryLevelInfo batteryLevelInfo, long[] deviceStats) { @@ -373,9 +380,15 @@ class PowerStatsExporter { 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 6a4c9c26d314..a492a72933d7 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 @@ public class VcnContext { 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 b5747828349e..a81ad22ddf4a 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 @@ public class VcnGatewayConnection extends StateMachine { 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 b9b10606a188..0d4c3736775b 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 @@ public abstract class NetworkMetricMonitor implements AutoCloseable { @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 2b0ca0802359..ad5bc7294f85 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 @@ public class UnderlyingNetworkController { 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 @@ public class UnderlyingNetworkController { .getAllSubIdsInGroup(mSubscriptionGroup) .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) { - if (mVcnContext.isFlagNetworkMetricMonitorEnabled() - && mVcnContext.isFlagIpSecTransformStateEnabled()) { + if (mVcnContext.isFlagIpSecTransformStateEnabled()) { reevaluateNetworks(); } return; @@ -447,8 +445,7 @@ public class UnderlyingNetworkController { */ 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 @@ public class UnderlyingNetworkController { @Override public void onLost(@NonNull Network network) { - if (mVcnContext.isFlagNetworkMetricMonitorEnabled() - && mVcnContext.isFlagIpSecTransformStateEnabled()) { + if (mVcnContext.isFlagIpSecTransformStateEnabled()) { mUnderlyingNetworkRecords.get(network).close(); } @@ -652,8 +648,7 @@ public class UnderlyingNetworkController { 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 c852fb4e170f..53b0751d196e 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 @@ public class UnderlyingNetworkEvaluator { } 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 460de01a7d1d..054f9318dfce 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } 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 70f9ebb0e61e..ccd59969cec8 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 @@ class BackNavigationController { 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 76f2437cbc3f..614187682ef4 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 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP 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 4d3c79eb28ae..e1478d616121 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 09906912ef3f..e2ac22de29a4 100644 --- a/services/java/com/android/server/flags.aconfig +++ b/services/java/com/android/server/flags.aconfig @@ -10,6 +10,14 @@ flag { } 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 6f207fb1a00e..6eb986b2b86b 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 6f207fb1a00e..c8345f71b39a 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 7b635d4198c3..d7b60cffa623 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.hardware.power.stats.StateResidencyResult; 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.MonotonicClock; 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 @@ import java.util.concurrent.CompletableFuture; @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 d36b55345418..d6f50368df84 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.io.File; 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 @@ public class BatteryStatsHistoryIteratorTest { 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 e40a3e314e58..b67ec8b2c828 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 @@ public class BatteryStatsHistoryTest { } @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 2408cc1d0b3d..177f30a79ebc 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 @@ public class BatteryStatsImplTest { } 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 3931201aaf03..5912a9959e02 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 @@ public class BatteryStatsUserLifecycleTests { 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 b30224b8327b..e9d95fcbccea 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.any; 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 @@ public class BatteryUsageStatsProviderTest { .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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { } } - mStatsRule.setCurrentTime(54321); + setTime(180 * MINUTE_IN_MS); + return batteryStats; } @@ -266,7 +268,7 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { } @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 @@ public class BatteryUsageStatsProviderTest { 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(); + int[] count = new int[1]; + doAnswer(inv -> { + count[0]++; + return null; + }).when(powerStatsStore).storePowerStatsSpan(any(PowerStatsSpan.class)); + + 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, - mock(PowerAttributor.class), mStatsRule.getPowerProfile(), - mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock); + powerAttributor, mStatsRule.getPowerProfile(), + mStatsRule.getCpuScalingPolicies(), powerStatsStore, + accumulatedBatteryUsageStatsSpanSize, mMockClock); - batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore, - /* accumulateBatteryUsageStats */ true); + 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { mStatsRule.waitForBackgroundThread(); - verify(powerStatsStore).storeBatteryUsageStats(anyLong(), any()); + verify(powerStatsStore).storeBatteryUsageStatsAsync(anyLong(), any()); } @Test @@ -746,7 +778,7 @@ public class BatteryUsageStatsProviderTest { 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 2c03f9d1a9aa..1e4454c9a3f0 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.io.File; 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 class MockBatteryStatsImpl extends BatteryStatsImpl { 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 @@ public class MockBatteryStatsImpl extends BatteryStatsImpl { } @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 03491bcfdf5a..0d5d277b00ef 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 @@ public class WakelockPowerStatsCollectorTest { 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 @@ public class WakelockPowerStatsCollectorTest { 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 d49bc4326a5a..d8a9400d5180 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 467c15dd2a75..ea70287466ff 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 @@ public final class BatteryStatsServiceTest { 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 577b02a4ff7a..c30b4bb6f65d 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 @@ public class ActivityRecordTests extends WindowTestsBase { 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 6f207fb1a00e..c8345f71b39a 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 6f207fb1a00e..c8345f71b39a 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 e29e4621d571..e045f100e549 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 @@ public class VcnGatewayConnectionTestBase { 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 421e1ad20b78..bc7ff47d9a01 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 @@ public abstract class NetworkEvaluationTestBase { 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 588624b56221..6f31d8db070f 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 @@ public class UnderlyingNetworkControllerTest { private void resetVcnContext(VcnContext vcnContext) { reset(vcnContext); doNothing().when(vcnContext).ensureRunningOnLooperThread(); - doReturn(true).when(vcnContext).isFlagNetworkMetricMonitorEnabled(); doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled(); } |