diff options
23 files changed, 1221 insertions, 951 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 012a2e62755c..fd6180068fd6 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16103,6 +16103,7 @@ package android.graphics { enum_constant public static final android.graphics.ColorSpace.Named CIE_LAB; enum_constant public static final android.graphics.ColorSpace.Named CIE_XYZ; enum_constant public static final android.graphics.ColorSpace.Named DCI_P3; + enum_constant @FlaggedApi("com.android.graphics.flags.display_bt2020_colorspace") public static final android.graphics.ColorSpace.Named DISPLAY_BT2020; enum_constant public static final android.graphics.ColorSpace.Named DISPLAY_P3; enum_constant public static final android.graphics.ColorSpace.Named EXTENDED_SRGB; enum_constant public static final android.graphics.ColorSpace.Named LINEAR_EXTENDED_SRGB; @@ -18619,6 +18620,7 @@ package android.hardware { field public static final int DATASPACE_BT709 = 281083904; // 0x10c10000 field public static final int DATASPACE_DCI_P3 = 155844608; // 0x94a0000 field public static final int DATASPACE_DEPTH = 4096; // 0x1000 + field @FlaggedApi("com.android.graphics.flags.display_bt2020_colorspace") public static final int DATASPACE_DISPLAY_BT2020 = 142999552; // 0x8860000 field public static final int DATASPACE_DISPLAY_P3 = 143261696; // 0x88a0000 field public static final int DATASPACE_DYNAMIC_DEPTH = 4098; // 0x1002 field public static final int DATASPACE_HEIF = 4100; // 0x1004 diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 4ef5b5163fef..64aa705447aa 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -11176,7 +11176,7 @@ public class Notification implements Parcelable } /** - * A Notification Style used to to define a notification whose expanded state includes + * A Notification Style used to define a notification whose expanded state includes * a highly customizable progress bar with segments, points, a custom tracker icon, * and custom icons at the start and end of the progress bar. * diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 74817643964d..c17da249f322 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -16,28 +16,25 @@ package android.app; -import static android.text.TextUtils.formatSimple; - import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; -import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; -import android.os.Process; import android.os.SystemClock; import android.os.SystemProperties; 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; @@ -45,14 +42,12 @@ 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; /** @@ -80,15 +75,10 @@ public class PropertyInvalidatedCache<Query, Result> { public abstract @Nullable R apply(@NonNull Q query); /** - * Return true if a query should not use the cache. The default implementation returns true - * if the process UID differs from the calling UID. This is to prevent a binder caller from - * reading a cached value created due to a different binder caller, when processes are - * caching on behalf of other processes. + * Return true if a query should not use the cache. The default implementation + * always uses the cache. */ public boolean shouldBypassCache(@NonNull Q query) { - if(android.multiuser.Flags.propertyInvalidatedCacheBypassMismatchedUids()) { - return Binder.getCallingUid() != Process.myUid(); - } return false; } }; @@ -234,24 +224,12 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note that all - * reserved values cause the cache to be skipped. + * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note + * that all 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 global 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) { @@ -259,7 +237,7 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * The names of the reserved nonces. + * The names of the nonces */ private static final String[] sNonceName = new String[]{ "unset", "disabled", "corked", "bypass" }; @@ -299,17 +277,32 @@ 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. 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. + * 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. */ @GuardedBy("sGlobalLock") private static final HashSet<String> sDisabledKeys = new HashSet<>(); @@ -322,6 +315,14 @@ 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. */ @@ -333,6 +334,12 @@ 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. @@ -362,13 +369,7 @@ public class PropertyInvalidatedCache<Query, Result> { private final LinkedHashMap<Query, Result> mCache; /** - * The nonce handler for this cache. - */ - @GuardedBy("mLock") - private final NonceHandler mNonce; - - /** - * The last nonce value that was observed. + * The last value of the {@code mPropertyHandle} that we observed. */ @GuardedBy("mLock") private long mLastSeenNonce = NONCE_UNSET; @@ -384,297 +385,6 @@ 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; - - // The methods to get and set a nonce from whatever storage is being used. - abstract long getNonce(); - abstract void setNonce(long value); - - NonceHandler(@NonNull String name) { - mName = name; - } - - /** - * 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); - } - } - } - } - - void disable() { - if (!sEnabled) { - return; - } - synchronized (mLock) { - setNonce(NONCE_DISABLED); - } - } - - 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); - } - - @Override - long getNonce() { - if (mHandle == null) { - synchronized (mLock) { - mHandle = SystemProperties.find(mName); - if (mHandle == null) { - return NONCE_UNSET; - } - } - } - return mHandle.getLong(NONCE_UNSET); - } - - @Override - void setNonce(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 NonceTest handler stores the nonce locally. - */ - private static class NonceTest extends NonceHandler { - // The saved nonce. - private long mValue; - - // If this flag is false, the handler has been shutdown during a test. Access to the - // handler in this state is an error. - private boolean mIsActive = true; - - NonceTest(@NonNull String name) { - super(name); - } - - void shutdown() { - // The handler has been discarded as part of test cleanup. Further access is an - // error. - mIsActive = false; - } - - @Override - long getNonce() { - if (!mIsActive) { - throw new IllegalStateException("handler " + mName + " is shutdown"); - } - return mValue; - } - - @Override - void setNonce(long value) { - if (!mIsActive) { - throw new IllegalStateException("handler " + mName + " is shutdown"); - } - mValue = value; - } - } - - /** - * 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("cache_key.test.")) { - h = new NonceTest(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. @@ -707,7 +417,6 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = propertyName; validateCacheKey(mPropertyName); mCacheName = cacheName; - mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = new DefaultComputer<>(this); mCache = createMap(); @@ -732,7 +441,6 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = createPropertyName(module, api); validateCacheKey(mPropertyName); mCacheName = cacheName; - mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = computer; mCache = createMap(); @@ -776,58 +484,130 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Enable or disable testing. At this time, no action is taken when testing begins. + * 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. * @hide */ @TestApi public static void setTestMode(boolean mode) { - if (mode) { - // No action when testing begins. - } else { - resetAfterTest(); + sTesting = mode; + synchronized (sTestingPropertyMap) { + sTestingPropertyMap.clear(); } } /** - * Enable testing the specific cache key. This is a legacy API that will be removed as part of - * b/360897450. - * @hide + * 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. */ - @TestApi - public void testPropertyName() { + private static void testPropertyName(@NonNull String name) { + synchronized (sTestingPropertyMap) { + sTestingPropertyMap.put(name, (long) NONCE_UNSET); + } } /** - * Clean up when testing ends. All NonceTest handlers are erased from the global list and are - * poisoned, just in case the test program has retained a handle to one of the associated - * caches. + * 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. * @hide */ - @VisibleForTesting - public static void resetAfterTest() { - synchronized (sGlobalLock) { - for (Iterator<String> e = sHandlers.keys().asIterator(); e.hasNext(); ) { - String s = e.next(); - final NonceHandler h = sHandlers.get(s); - if (h instanceof NonceTest t) { - t.shutdown(); - sHandlers.remove(s); + @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); } - // Read the nonce associated with the current cache. - @GuardedBy("mLock") - private long getCurrentNonce() { - return mNonce.getNonce(); + // Write the nonce in a static context. No handle is available. + private static void setNonce(String name, long val) { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(name); + if (n != null) { + sTestingPropertyMap.put(name, val); + return; + } + } + } + RuntimeException failure = null; + for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) { + try { + SystemProperties.set(name, Long.toString(val)); + if (attempt > 0) { + // This log is not guarded. Based on known bug reports, it should + // occur once a week or less. The purpose of the log message is to + // identify the retries as a source of delay that might be otherwise + // be attributed to the cache itself. + Log.w(TAG, "Nonce set after " + attempt + " tries"); + } + return; + } catch (RuntimeException e) { + if (failure == null) { + failure = e; + } + try { + Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS); + } catch (InterruptedException x) { + // Ignore this exception. The desired delay is only approximate and + // there is no issue if the sleep sometimes terminates early. + } + } + } + // This point is reached only if SystemProperties.set() fails at least once. + // Rethrow the first exception that was received. + throw failure; + } + + // Set the nonce in a static context. No handle is available. + private static long getNonce(String name) { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(name); + if (n != null) { + return n; + } + } + } + return SystemProperties.getLong(name, NONCE_UNSET); } /** - * 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. + * Forget all cached values. + * TODO(216112648) remove this as a public API. Clients should invalidate caches, not clear + * them. * @hide */ public final void clear() { @@ -894,7 +674,7 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Disable the use of this cache in this process. This method is used internally and during + * Disable the use of this cache in this process. This method is using internally and during * testing. To disable a cache in normal code, use disableLocal(). A disabled cache cannot * be re-enabled. * @hide @@ -1003,7 +783,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (DEBUG) { if (!mDisabled) { - Log.d(TAG, formatSimple( + Log.d(TAG, TextUtils.formatSimple( "cache %s %s for %s", cacheName(), sNonceName[(int) currentNonce], queryToString(query))); } @@ -1018,7 +798,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (cachedResult != null) mHits++; } else { if (DEBUG) { - Log.d(TAG, formatSimple( + Log.d(TAG, TextUtils.formatSimple( "clearing cache %s of %d entries because nonce changed [%s] -> [%s]", cacheName(), mCache.size(), mLastSeenNonce, currentNonce)); @@ -1044,7 +824,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (currentNonce != afterRefreshNonce) { currentNonce = afterRefreshNonce; if (DEBUG) { - Log.d(TAG, formatSimple( + Log.d(TAG, TextUtils.formatSimple( "restarting %s %s because nonce changed in refresh", cacheName(), queryToString(query))); @@ -1115,7 +895,10 @@ public class PropertyInvalidatedCache<Query, Result> { * @param name Name of the cache-key property to invalidate */ private static void disableSystemWide(@NonNull String name) { - getNonceHandler(name).disable(); + if (!sEnabled) { + return; + } + setNonce(name, NONCE_DISABLED); } /** @@ -1125,7 +908,7 @@ public class PropertyInvalidatedCache<Query, Result> { */ @TestApi public void invalidateCache() { - mNonce.invalidate(); + invalidateCache(mPropertyName); } /** @@ -1148,7 +931,59 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void invalidateCache(@NonNull String name) { - getNonceHandler(name).invalidate(); + 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); } /** @@ -1165,7 +1000,43 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void corkInvalidations(@NonNull String name) { - getNonceHandler(name).cork(); + 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); + } + } } /** @@ -1177,7 +1048,34 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void uncorkInvalidations(@NonNull String name) { - getNonceHandler(name).uncork(); + 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); + } + } } /** @@ -1206,8 +1104,6 @@ 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); } @@ -1221,35 +1117,31 @@ 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); } - mNonce.invalidate(); + PropertyInvalidatedCache.invalidateCache(mPropertyName); return; } synchronized (mLock) { boolean alreadyQueued = mUncorkDeadlineMs >= 0; if (DEBUG) { - Log.d(TAG, formatSimple( + Log.w(TAG, TextUtils.formatSimple( "autoCork %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs; if (!alreadyQueued) { getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs); - mNonce.cork(); + PropertyInvalidatedCache.corkInvalidations(mPropertyName); } else { - // Count this as a corked invalidation. - mNonce.invalidate(); + synchronized (sCorkLock) { + final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); + sCorkedInvalidates.put(mPropertyName, count + 1); + } } } } @@ -1257,7 +1149,7 @@ public class PropertyInvalidatedCache<Query, Result> { private void handleMessage(Message msg) { synchronized (mLock) { if (DEBUG) { - Log.d(TAG, formatSimple( + Log.w(TAG, TextUtils.formatSimple( "handleMsesage %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } @@ -1269,7 +1161,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (mUncorkDeadlineMs > nowMs) { mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs; if (DEBUG) { - Log.d(TAG, formatSimple( + Log.w(TAG, TextUtils.formatSimple( "scheduling uncork at %s", mUncorkDeadlineMs)); } @@ -1277,10 +1169,10 @@ public class PropertyInvalidatedCache<Query, Result> { return; } if (DEBUG) { - Log.d(TAG, "automatic uncorking " + mPropertyName); + Log.w(TAG, "automatic uncorking " + mPropertyName); } mUncorkDeadlineMs = -1; - mNonce.uncork(); + PropertyInvalidatedCache.uncorkInvalidations(mPropertyName); } } @@ -1315,7 +1207,7 @@ public class PropertyInvalidatedCache<Query, Result> { Result resultToCompare = recompute(query); boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce); if (!nonceChanged && !resultEquals(proposedResult, resultToCompare)) { - Log.e(TAG, formatSimple( + Log.e(TAG, TextUtils.formatSimple( "cache %s inconsistent for %s is %s should be %s", cacheName(), queryToString(query), proposedResult, resultToCompare)); @@ -1392,9 +1284,17 @@ public class PropertyInvalidatedCache<Query, Result> { /** * Returns a list of caches alive at the current time. */ + @GuardedBy("sGlobalLock") private static @NonNull ArrayList<PropertyInvalidatedCache> getActiveCaches() { - synchronized (sGlobalLock) { - return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet()); + 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()); } } @@ -1461,27 +1361,32 @@ public class PropertyInvalidatedCache<Query, Result> { return; } - NonceHandler.Stats stats = mNonce.getStats(); + long invalidateCount; + long corkedInvalidates; + synchronized (sCorkLock) { + invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0); + corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); + } synchronized (mLock) { - pw.println(formatSimple(" Cache Name: %s", cacheName())); - pw.println(formatSimple(" Property: %s", mPropertyName)); + pw.println(TextUtils.formatSimple(" Cache Name: %s", cacheName())); + pw.println(TextUtils.formatSimple(" Property: %s", mPropertyName)); final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED] + mSkips[NONCE_BYPASS]; - pw.println(formatSimple( + pw.println(TextUtils.formatSimple( " Hits: %d, Misses: %d, Skips: %d, Clears: %d", mHits, mMisses, skips, mClears)); - pw.println(formatSimple( + pw.println(TextUtils.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(formatSimple( + pw.println(TextUtils.formatSimple( " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", - mLastSeenNonce, stats.invalidated, stats.corkedInvalidates)); - pw.println(formatSimple( + mLastSeenNonce, invalidateCount, corkedInvalidates)); + pw.println(TextUtils.formatSimple( " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); - pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); + pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); pw.println(""); // No specific cache was requested. This is the default, and no details @@ -1499,7 +1404,23 @@ public class PropertyInvalidatedCache<Query, Result> { String key = Objects.toString(entry.getKey()); String value = Objects.toString(entry.getValue()); - pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value)); + 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())); } } } @@ -1520,7 +1441,14 @@ public class PropertyInvalidatedCache<Query, Result> { // then only that cache is reported. boolean detail = anyDetailed(args); - ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches(); + ArrayList<PropertyInvalidatedCache> activeCaches; + synchronized (sGlobalLock) { + activeCaches = getActiveCaches(); + if (!detail) { + dumpCorkInfo(pw); + } + } + 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 cad96e348f4b..cc90ce582048 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -76,7 +76,6 @@ import android.companion.virtual.VirtualDeviceManager; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; -import android.compat.annotation.EnabledSince; import android.content.ClipboardManager; import android.content.ContentCaptureOptions; import android.content.Context; @@ -172,8 +171,7 @@ import android.net.NetworkWatchlistManager; import android.net.PacProxyManager; import android.net.TetheringManager; import android.net.VpnManager; -import android.net.vcn.IVcnManagementService; -import android.net.vcn.VcnManager; +import android.net.vcn.VcnFrameworkInitializer; import android.net.wifi.WifiFrameworkInitializer; import android.net.wifi.nl80211.WifiNl80211Manager; import android.net.wifi.sharedconnectivity.app.SharedConnectivityManager; @@ -208,7 +206,6 @@ import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.StatsFrameworkInitializer; import android.os.SystemConfigManager; -import android.os.SystemProperties; import android.os.SystemUpdateManager; import android.os.SystemVibrator; import android.os.SystemVibratorManager; @@ -302,18 +299,6 @@ public final class SystemServiceRegistry { public static boolean sEnableServiceNotFoundWtf = false; /** - * Starting with {@link VANILLA_ICE_CREAM}, Telephony feature flags - * (e.g. {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}) are being checked before - * returning managers that depend on them. If the feature is missing, - * {@link Context#getSystemService} will return null. - * - * This change is specific to VcnManager. - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) - static final long ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN = 330902016; - - /** * After {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, Wear devices will be allowed to publish * no {@link GameManager} instance. This is because the respective system service is no longer * started for Wear devices given that the applications of the service do not currently apply to @@ -323,16 +308,6 @@ public final class SystemServiceRegistry { @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) static final long NULL_GAME_MANAGER_IN_WEAR = 340929737; - /** - * The corresponding vendor API for Android V - * - * <p>Starting with Android V, the vendor API format has switched to YYYYMM. - * - * @see <a href="https://preview.source.android.com/docs/core/architecture/api-flags">Vendor API - * level</a> - */ - private static final int VENDOR_API_FOR_ANDROID_V = 202404; - // Service registry information. // This information is never changed once static initialization has completed. private static final Map<Class<?>, String> SYSTEM_SERVICE_NAMES = @@ -499,22 +474,6 @@ public final class SystemServiceRegistry { return new VpnManager(ctx, service); }}); - registerService(Context.VCN_MANAGEMENT_SERVICE, VcnManager.class, - new CachedServiceFetcher<VcnManager>() { - @Override - public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException { - final String telephonyFeatureToCheck = getVcnFeatureDependency(); - - if (telephonyFeatureToCheck != null - && !ctx.getPackageManager().hasSystemFeature(telephonyFeatureToCheck)) { - return null; - } - - IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE); - IVcnManagementService service = IVcnManagementService.Stub.asInterface(b); - return new VcnManager(ctx, service); - }}); - registerService(Context.COUNTRY_DETECTOR, CountryDetector.class, new StaticServiceFetcher<CountryDetector>() { @Override @@ -1829,6 +1788,8 @@ public final class SystemServiceRegistry { OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); DeviceLockFrameworkInitializer.registerServiceWrappers(); VirtualizationFrameworkInitializer.registerServiceWrappers(); + VcnFrameworkInitializer.registerServiceWrappers(); + if (com.android.server.telecom.flags.Flags.telecomMainlineBlockedNumbersManager()) { ProviderFrameworkInitializer.registerServiceWrappers(); } @@ -1890,30 +1851,6 @@ public final class SystemServiceRegistry { return manager.hasSystemFeature(featureName); } - // Suppressing AndroidFrameworkCompatChange because we're querying vendor - // partition SDK level, not application's target SDK version (which BTW we - // also check through Compatibility framework a few lines below). - @SuppressWarnings("AndroidFrameworkCompatChange") - @Nullable - private static String getVcnFeatureDependency() { - // Check SDK version of the client app. Apps targeting pre-V SDK might - // have not checked for existence of these features. - if (!Compatibility.isChangeEnabled(ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN)) { - return null; - } - - // Check SDK version of the vendor partition. Pre-V devices might have - // incorrectly under-declared telephony features. - final int vendorApiLevel = SystemProperties.getInt( - "ro.vendor.api_level", Build.VERSION.DEVICE_INITIAL_SDK_INT); - if (vendorApiLevel < VENDOR_API_FOR_ANDROID_V) { - return PackageManager.FEATURE_TELEPHONY; - } else { - return PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION; - } - - } - /** * Gets a system service from a given context. * @hide diff --git a/core/java/android/hardware/DataSpace.java b/core/java/android/hardware/DataSpace.java index 312bfdf777d6..611738435f7e 100644 --- a/core/java/android/hardware/DataSpace.java +++ b/core/java/android/hardware/DataSpace.java @@ -15,9 +15,12 @@ */ package android.hardware; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.view.SurfaceControl; +import com.android.graphics.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -639,6 +642,18 @@ public final class DataSpace { */ public static final int DATASPACE_SRGB_LINEAR = 138477568; + /** + * Display BT. 2020 encoding. + * + * <p>Composed of the following -</p> + * <pre> + * Primaries: STANDARD_BT2020 + * Transfer: TRANSFER_SRGB + * Range: RANGE_FULL</pre> + */ + @FlaggedApi(Flags.FLAG_DISPLAY_BT2020_COLORSPACE) + public static final int DATASPACE_DISPLAY_BT2020 = 142999552; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { @@ -660,7 +675,8 @@ public final class DataSpace { DATASPACE_BT2020, DATASPACE_BT709, DATASPACE_DCI_P3, - DATASPACE_SRGB_LINEAR + DATASPACE_SRGB_LINEAR, + DATASPACE_DISPLAY_BT2020 }) public @interface NamedDataSpace {}; diff --git a/core/java/android/net/vcn/VcnFrameworkInitializer.java b/core/java/android/net/vcn/VcnFrameworkInitializer.java new file mode 100644 index 000000000000..8cb213b306be --- /dev/null +++ b/core/java/android/net/vcn/VcnFrameworkInitializer.java @@ -0,0 +1,106 @@ +/* + * 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.net.vcn; + +import android.annotation.Nullable; +import android.app.SystemServiceRegistry; +import android.compat.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.SystemProperties; + +/** + * Class for performing registration for VCN service. + * + * @hide + */ +// TODO: Expose it as @SystemApi(client = MODULE_LIBRARIES) +public final class VcnFrameworkInitializer { + /** + * Starting with {@link VANILLA_ICE_CREAM}, Telephony feature flags (e.g. {@link + * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}) are being checked before returning managers + * that depend on them. If the feature is missing, {@link Context#getSystemService} will return + * null. + * + * <p>This change is specific to VcnManager. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + private static final long ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN = 330902016; + + /** + * The corresponding vendor API for Android V + * + * <p>Starting with Android V, the vendor API format has switched to YYYYMM. + * + * @see <a href="https://preview.source.android.com/docs/core/architecture/api-flags">Vendor API + * level</a> + */ + private static final int VENDOR_API_FOR_ANDROID_V = 202404; + + private VcnFrameworkInitializer() {} + + // Suppressing AndroidFrameworkCompatChange because we're querying vendor + // partition SDK level, not application's target SDK version (which BTW we + // also check through Compatibility framework a few lines below). + @Nullable + private static String getVcnFeatureDependency() { + // Check SDK version of the client app. Apps targeting pre-V SDK might + // have not checked for existence of these features. + if (!Compatibility.isChangeEnabled(ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN)) { + return null; + } + + // Check SDK version of the vendor partition. Pre-V devices might have + // incorrectly under-declared telephony features. + final int vendorApiLevel = + SystemProperties.getInt( + "ro.vendor.api_level", Build.VERSION.DEVICE_INITIAL_SDK_INT); + if (vendorApiLevel < VENDOR_API_FOR_ANDROID_V) { + return PackageManager.FEATURE_TELEPHONY; + } else { + return PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION; + } + } + + /** + * Register VCN service to {@link Context}, so that {@link Context#getSystemService} can return + * a VcnManager. + * + * @throws IllegalStateException if this is called anywhere besides {@link + * SystemServiceRegistry}. + */ + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService( + VcnManager.VCN_MANAGEMENT_SERVICE_STRING, + VcnManager.class, + (context, serviceBinder) -> { + final String telephonyFeatureToCheck = getVcnFeatureDependency(); + if (telephonyFeatureToCheck != null + && !context.getPackageManager() + .hasSystemFeature(telephonyFeatureToCheck)) { + return null; + } + IVcnManagementService service = + IVcnManagementService.Stub.asInterface(serviceBinder); + return new VcnManager(context, service); + }); + } +} diff --git a/core/java/android/view/RoundScrollbarRenderer.java b/core/java/android/view/RoundScrollbarRenderer.java index 5f6d5e29570e..59c2598f00f0 100644 --- a/core/java/android/view/RoundScrollbarRenderer.java +++ b/core/java/android/view/RoundScrollbarRenderer.java @@ -16,17 +16,26 @@ package android.view; +import static android.util.MathUtils.acos; + +import static java.lang.Math.sin; + import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; +import android.os.SystemProperties; import android.util.DisplayMetrics; +import android.view.flags.Flags; /** * Helper class for drawing round scroll bars on round Wear devices. + * + * @hide */ -class RoundScrollbarRenderer { +public class RoundScrollbarRenderer { + private static final String BLUECHIP_ENABLED_SYSPROP = "persist.cw_build.bluechip.enabled"; // The range of the scrollbar position represented as an angle in degrees. private static final float SCROLLBAR_ANGLE_RANGE = 28.8f; private static final float MAX_SCROLLBAR_ANGLE_SWIPE = 26.3f; // 90% @@ -45,12 +54,15 @@ class RoundScrollbarRenderer { private final Paint mTrackPaint = new Paint(); private final RectF mRect = new RectF(); private final View mParent; - private final int mMaskThickness; + private final float mInset; private float mPreviousMaxScroll = 0; private float mMaxScrollDiff = 0; private float mPreviousCurrentScroll = 0; private float mCurrentScrollDiff = 0; + private float mThumbStrokeWidthAsDegrees = 0; + private boolean mDrawToLeft; + private boolean mUseRefactoredRoundScrollbar; public RoundScrollbarRenderer(View parent) { // Paints for the round scrollbar. @@ -69,29 +81,36 @@ class RoundScrollbarRenderer { // Fetch the resource indicating the thickness of CircularDisplayMask, rounding in the same // way WindowManagerService.showCircularMask does. The scroll bar is inset by this amount so // that it doesn't get clipped. - mMaskThickness = parent.getContext().getResources().getDimensionPixelSize( - com.android.internal.R.dimen.circular_display_mask_thickness); - } + int maskThickness = + parent.getContext() + .getResources() + .getDimensionPixelSize( + com.android.internal.R.dimen.circular_display_mask_thickness); - public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) { - if (alpha == 0) { - return; - } - // Get information about the current scroll state of the parent view. - float maxScroll = mParent.computeVerticalScrollRange(); - float scrollExtent = mParent.computeVerticalScrollExtent(); - float newScroll = mParent.computeVerticalScrollOffset(); + float thumbWidth = dpToPx(THUMB_WIDTH_DP); + mThumbPaint.setStrokeWidth(thumbWidth); + mTrackPaint.setStrokeWidth(thumbWidth); + mInset = thumbWidth / 2 + maskThickness; + + mUseRefactoredRoundScrollbar = + Flags.useRefactoredRoundScrollbar() + && SystemProperties.getBoolean(BLUECHIP_ENABLED_SYSPROP, false); + } + private float computeScrollExtent(float scrollExtent, float maxScroll) { if (scrollExtent <= 0) { if (!mParent.canScrollVertically(1) && !mParent.canScrollVertically(-1)) { - return; + return -1f; } else { - scrollExtent = 0; + return 0f; } } else if (maxScroll <= scrollExtent) { - return; + return -1f; } + return scrollExtent; + } + private void resizeGradually(float maxScroll, float newScroll) { // Make changes to the VerticalScrollRange happen gradually if (Math.abs(maxScroll - mPreviousMaxScroll) > RESIZING_THRESHOLD_PX && mPreviousMaxScroll != 0) { @@ -106,51 +125,81 @@ class RoundScrollbarRenderer { || Math.abs(mCurrentScrollDiff) > RESIZING_THRESHOLD_PX) { mMaxScrollDiff *= RESIZING_RATE; mCurrentScrollDiff *= RESIZING_RATE; - - maxScroll -= mMaxScrollDiff; - newScroll -= mCurrentScrollDiff; } else { mMaxScrollDiff = 0; mCurrentScrollDiff = 0; } + } - float currentScroll = Math.max(0, newScroll); - float linearThumbLength = scrollExtent; - float thumbWidth = dpToPx(THUMB_WIDTH_DP); - mThumbPaint.setStrokeWidth(thumbWidth); - mTrackPaint.setStrokeWidth(thumbWidth); + public void drawRoundScrollbars(Canvas canvas, float alpha, Rect bounds, boolean drawToLeft) { + if (alpha == 0) { + return; + } + // Get information about the current scroll state of the parent view. + float maxScroll = mParent.computeVerticalScrollRange(); + float scrollExtent = mParent.computeVerticalScrollExtent(); + float newScroll = mParent.computeVerticalScrollOffset(); - setThumbColor(applyAlpha(DEFAULT_THUMB_COLOR, alpha)); - setTrackColor(applyAlpha(DEFAULT_TRACK_COLOR, alpha)); + scrollExtent = computeScrollExtent(scrollExtent, maxScroll); + if (scrollExtent < 0f) { + return; + } - // Normalize the sweep angle for the scroll bar. - float sweepAngle = (linearThumbLength / maxScroll) * SCROLLBAR_ANGLE_RANGE; - sweepAngle = clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE); - // Normalize the start angle so that it falls on the track. - float startAngle = (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle)) - / (maxScroll - linearThumbLength) - SCROLLBAR_ANGLE_RANGE / 2f; - startAngle = clamp(startAngle, -SCROLLBAR_ANGLE_RANGE / 2f, - SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle); + // Make changes to the VerticalScrollRange happen gradually + resizeGradually(maxScroll, newScroll); + maxScroll -= mMaxScrollDiff; + newScroll -= mCurrentScrollDiff; - // Draw the track and the thumb. - float inset = thumbWidth / 2 + mMaskThickness; - mRect.set( - bounds.left + inset, - bounds.top + inset, - bounds.right - inset, - bounds.bottom - inset); - - if (drawToLeft) { - canvas.drawArc(mRect, 180 + SCROLLBAR_ANGLE_RANGE / 2f, -SCROLLBAR_ANGLE_RANGE, false, - mTrackPaint); - canvas.drawArc(mRect, 180 - startAngle, -sweepAngle, false, mThumbPaint); + applyThumbColor(alpha); + + float sweepAngle = computeSweepAngle(scrollExtent, maxScroll); + float startAngle = + computeStartAngle(Math.max(0, newScroll), sweepAngle, maxScroll, scrollExtent); + + updateBounds(bounds); + + mDrawToLeft = drawToLeft; + drawRoundScrollbars(canvas, startAngle, sweepAngle, alpha); + } + + private void drawRoundScrollbars( + Canvas canvas, float startAngle, float sweepAngle, float alpha) { + if (mUseRefactoredRoundScrollbar) { + draw(canvas, startAngle, sweepAngle, alpha); } else { - canvas.drawArc(mRect, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, false, - mTrackPaint); - canvas.drawArc(mRect, startAngle, sweepAngle, false, mThumbPaint); + applyTrackColor(alpha); + drawArc(canvas, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE, mTrackPaint); + drawArc(canvas, startAngle, sweepAngle, mThumbPaint); } } + /** Returns true if horizontal bounds are updated */ + private void updateBounds(Rect bounds) { + mRect.set( + bounds.left + mInset, + bounds.top + mInset, + bounds.right - mInset, + bounds.bottom - mInset); + mThumbStrokeWidthAsDegrees = + getVertexAngle((mRect.right - mRect.left) / 2f, mThumbPaint.getStrokeWidth() / 2f); + } + + private float computeSweepAngle(float scrollExtent, float maxScroll) { + // Normalize the sweep angle for the scroll bar. + float sweepAngle = (scrollExtent / maxScroll) * SCROLLBAR_ANGLE_RANGE; + return clamp(sweepAngle, MIN_SCROLLBAR_ANGLE_SWIPE, MAX_SCROLLBAR_ANGLE_SWIPE); + } + + private float computeStartAngle( + float currentScroll, float sweepAngle, float maxScroll, float scrollExtent) { + // Normalize the start angle so that it falls on the track. + float startAngle = + (currentScroll * (SCROLLBAR_ANGLE_RANGE - sweepAngle)) / (maxScroll - scrollExtent) + - SCROLLBAR_ANGLE_RANGE / 2f; + return clamp( + startAngle, -SCROLLBAR_ANGLE_RANGE / 2f, SCROLLBAR_ANGLE_RANGE / 2f - sweepAngle); + } + void getRoundVerticalScrollBarBounds(Rect bounds) { float padding = dpToPx(OUTER_PADDING_DP); final int width = mParent.mRight - mParent.mLeft; @@ -164,10 +213,8 @@ class RoundScrollbarRenderer { private static float clamp(float val, float min, float max) { if (val < min) { return min; - } else if (val > max) { - return max; } else { - return val; + return Math.min(val, max); } } @@ -176,15 +223,17 @@ class RoundScrollbarRenderer { return Color.argb(alphaByte, Color.red(color), Color.green(color), Color.blue(color)); } - private void setThumbColor(int thumbColor) { - if (mThumbPaint.getColor() != thumbColor) { - mThumbPaint.setColor(thumbColor); + private void applyThumbColor(float alpha) { + int color = applyAlpha(DEFAULT_THUMB_COLOR, alpha); + if (mThumbPaint.getColor() != color) { + mThumbPaint.setColor(color); } } - private void setTrackColor(int trackColor) { - if (mTrackPaint.getColor() != trackColor) { - mTrackPaint.setColor(trackColor); + private void applyTrackColor(float alpha) { + int color = applyAlpha(DEFAULT_TRACK_COLOR, alpha); + if (mTrackPaint.getColor() != color) { + mTrackPaint.setColor(color); } } @@ -192,4 +241,88 @@ class RoundScrollbarRenderer { return dp * ((float) mParent.getContext().getResources().getDisplayMetrics().densityDpi) / DisplayMetrics.DENSITY_DEFAULT; } + + private static float getVertexAngle(float edge, float base) { + float edgeSquare = edge * edge * 2; + float baseSquare = base * base; + float gapInRadians = acos(((edgeSquare - baseSquare) / edgeSquare)); + return (float) Math.toDegrees(gapInRadians); + } + + private static float getKiteEdge(float knownEdge, float angleBetweenKnownEdgesInDegrees) { + return (float) (2 * knownEdge * sin(Math.toRadians(angleBetweenKnownEdgesInDegrees / 2))); + } + + private void draw(Canvas canvas, float thumbStartAngle, float thumbSweepAngle, float alpha) { + // Draws the top arc + drawTrack( + canvas, + // The highest point of the top track on a vertical scale. Here the thumb width is + // reduced to account for the arc formed by ROUND stroke style + -SCROLLBAR_ANGLE_RANGE / 2f - mThumbStrokeWidthAsDegrees, + // The lowest point of the top track on a vertical scale. Here the thumb width is + // reduced twice to (a) account for the arc formed by ROUND stroke style (b) gap + // between thumb and top track + thumbStartAngle - mThumbStrokeWidthAsDegrees * 2, + alpha); + // Draws the thumb + drawArc(canvas, thumbStartAngle, thumbSweepAngle, mThumbPaint); + // Draws the bottom arc + drawTrack( + canvas, + // The highest point of the bottom track on a vertical scale. Here the thumb width + // is added twice to (a) account for the arc formed by ROUND stroke style (b) gap + // between thumb and bottom track + (thumbStartAngle + thumbSweepAngle) + mThumbStrokeWidthAsDegrees * 2, + // The lowest point of the top track on a vertical scale. Here the thumb width is + // added to account for the arc formed by ROUND stroke style + SCROLLBAR_ANGLE_RANGE / 2f + mThumbStrokeWidthAsDegrees, + alpha); + } + + private void drawTrack(Canvas canvas, float beginAngle, float endAngle, float alpha) { + // Angular distance between end and begin + float angleBetweenEndAndBegin = endAngle - beginAngle; + // The sweep angle for the track is the angular distance between end and begin less the + // thumb width twice to account for top and bottom arc formed by the ROUND stroke style + float sweepAngle = angleBetweenEndAndBegin - 2 * mThumbStrokeWidthAsDegrees; + + float startAngle = -1f; + float strokeWidth = -1f; + if (sweepAngle > 0f) { + // The angle is greater than 0 which means a normal arc should be drawn with stroke + // width same as the thumb. The ROUND stroke style will cover the top/bottom arc of the + // track + startAngle = beginAngle + mThumbStrokeWidthAsDegrees; + strokeWidth = mThumbPaint.getStrokeWidth(); + } else if (Math.abs(sweepAngle) < 2 * mThumbStrokeWidthAsDegrees) { + // The sweep angle is less than 0 but is still relevant in creating a circle for the + // top/bottom track. The start angle is adjusted to account for being the mid point of + // begin / end angle. + startAngle = beginAngle + angleBetweenEndAndBegin / 2; + // The radius of this circle forms a kite with the radius of the arc drawn for the rect + // with the given angular difference between the arc radius which is used to compute the + // new stroke width. + strokeWidth = getKiteEdge(((mRect.right - mRect.left) / 2), angleBetweenEndAndBegin); + // The opacity is decreased proportionally, if the stroke width of the track is 50% or + // less that that of the thumb + alpha = alpha * Math.min(1f, 2 * strokeWidth / mThumbPaint.getStrokeWidth()); + // As we desire a circle to be drawn, the sweep angle is set to a minimal value + sweepAngle = Float.MIN_NORMAL; + } else { + return; + } + + applyTrackColor(alpha); + mTrackPaint.setStrokeWidth(strokeWidth); + drawArc(canvas, startAngle, sweepAngle, mTrackPaint); + } + + private void drawArc(Canvas canvas, float startAngle, float sweepAngle, Paint paint) { + if (mDrawToLeft) { + canvas.drawArc(mRect, /* startAngle= */ 180 - startAngle, -sweepAngle, false, paint); + } else { + canvas.drawArc(mRect, startAngle, sweepAngle, /* useCenter= */ false, paint); + } + } } diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index 1cf26ab64c09..bb61ae49259c 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -116,4 +116,12 @@ flag { description: "Add a SurfaceView composition order control API." bug: "341021569" is_fixed_read_only: true +} + +flag { + name: "use_refactored_round_scrollbar" + namespace: "wear_frameworks" + description: "Use refactored round scrollbar." + bug: "333417898" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java b/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java new file mode 100644 index 000000000000..262bd5cd6c01 --- /dev/null +++ b/core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 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 + * + * https://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.view; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.flags.Flags; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link RoundScrollbarRenderer}. + * + * <p>Build/Install/Run: atest FrameworksCoreTests:android.view.RoundScrollbarRendererTest + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class RoundScrollbarRendererTest { + + private static final int DEFAULT_VERTICAL_SCROLL_RANGE = 100; + private static final int DEFAULT_VERTICAL_SCROLL_EXTENT = 20; + private static final int DEFAULT_VERTICAL_SCROLL_OFFSET = 40; + private static final float DEFAULT_ALPHA = 0.5f; + private static final Rect BOUNDS = new Rect(0, 0, 200, 200); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Mock private Canvas mCanvas; + @Captor private ArgumentCaptor<Paint> mPaintCaptor; + private RoundScrollbarRenderer mScrollbar; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + MockView view = spy(new MockView(ApplicationProvider.getApplicationContext())); + when(view.canScrollVertically(anyInt())).thenReturn(true); + when(view.computeVerticalScrollRange()).thenReturn(DEFAULT_VERTICAL_SCROLL_RANGE); + when(view.computeVerticalScrollExtent()).thenReturn(DEFAULT_VERTICAL_SCROLL_EXTENT); + when(view.computeVerticalScrollOffset()).thenReturn(DEFAULT_VERTICAL_SCROLL_OFFSET); + mPaintCaptor = ArgumentCaptor.forClass(Paint.class); + + mScrollbar = new RoundScrollbarRenderer(view); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_USE_REFACTORED_ROUND_SCROLLBAR) + public void testScrollbarDrawn_legacy() { + mScrollbar.drawRoundScrollbars(mCanvas, DEFAULT_ALPHA, BOUNDS, /* drawToLeft= */ false); + + // The arc will be drawn twice, i.e. once for track and once for thumb + verify(mCanvas, times(2)) + .drawArc(any(), anyFloat(), anyFloat(), eq(false), mPaintCaptor.capture()); + + Paint thumbPaint = mPaintCaptor.getAllValues().getFirst(); + assertEquals(Paint.Cap.ROUND, thumbPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, thumbPaint.getStyle()); + Paint trackPaint = mPaintCaptor.getAllValues().get(1); + assertEquals(Paint.Cap.ROUND, trackPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, trackPaint.getStyle()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_USE_REFACTORED_ROUND_SCROLLBAR) + public void testScrollbarDrawn() { + mScrollbar.drawRoundScrollbars(mCanvas, DEFAULT_ALPHA, BOUNDS, /* drawToLeft= */ false); + + // The arc will be drawn thrice, i.e. twice for track and once for thumb + verify(mCanvas, times(3)) + .drawArc(any(), anyFloat(), anyFloat(), eq(false), mPaintCaptor.capture()); + + // Verify paint styles + Paint thumbPaint = mPaintCaptor.getAllValues().getFirst(); + assertEquals(Paint.Cap.ROUND, thumbPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, thumbPaint.getStyle()); + Paint trackPaint = mPaintCaptor.getAllValues().get(1); + assertEquals(Paint.Cap.ROUND, trackPaint.getStrokeCap()); + assertEquals(Paint.Style.STROKE, trackPaint.getStyle()); + } + + public static class MockView extends View { + + public MockView(Context context) { + super(context); + } + + @Override + public int computeVerticalScrollRange() { + return super.getHeight(); + } + + @Override + public int computeVerticalScrollOffset() { + return super.computeVerticalScrollOffset(); + } + + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + } +} diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig index 5ad97e6d8992..a63cbee4d707 100644 --- a/graphics/java/android/framework_graphics.aconfig +++ b/graphics/java/android/framework_graphics.aconfig @@ -33,3 +33,12 @@ flag { description: "Add OkLab ColorSpace support" bug: "344038816" } + +flag { + name: "display_bt2020_colorspace" + is_exported: true + is_fixed_read_only: true + namespace: "core_graphics" + description: "Add DISPLAY_BT2020 ColorSpace support" + bug: "344038816" +} diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 3752257da2bc..4c47de0ca754 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -768,7 +768,44 @@ public abstract class ColorSpace { * </table> */ @FlaggedApi(Flags.FLAG_OK_LAB_COLORSPACE) - OK_LAB + OK_LAB, + + /** + * <p>{@link ColorSpace.Rgb RGB} color space BT.2020 based on Rec. ITU-R BT.2020-1 and IEC 61966-2.1:1999.</p></p> + * <table summary="Color space definition"> + * <tr> + * <th>Chromaticity</th><th>Red</th><th>Green</th><th>Blue</th><th>White point</th> + * </tr> + * <tr><td>x</td><td>0.708</td><td>0.170</td><td>0.131</td><td>0.3127</td></tr> + * <tr><td>y</td><td>0.292</td><td>0.797</td><td>0.046</td><td>0.3290</td></tr> + * <tr><th>Property</th><th colspan="4">Value</th></tr> + * <tr><td>Name</td><td colspan="4">Rec. ITU-R BT.2020-1</td></tr> + * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> + * <tr> + * <td>Opto-electronic transfer function (OETF)</td> + * <td colspan="4">\(\begin{equation} + * C_{DisplayBT2020} = \begin{cases} 12.92 \times C_{linear} & C_{linear} \lt 0.0030186 \\\ + * 1.055 \times C_{linear}^{\frac{1}{2.4}} - 0.055 & C_{linear} \ge 0.0030186 \end{cases} + * \end{equation}\) + * </td> + * </tr> + * <tr> + * <td>Electro-optical transfer function (EOTF)</td> + * <td colspan="4">\(\begin{equation} + * C_{linear} = \begin{cases}\frac{C_{DisplayBT2020}}{12.92} & C_{sRGB} \lt 0.04045 \\\ + * \left( \frac{C_{DisplayBT2020} + 0.055}{1.055} \right) ^{2.4} & C_{sRGB} \ge 0.04045 \end{cases} + * \end{equation}\) + * </td> + * </tr> + * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> + * </table> + * <p> + * <img style="display: block; margin: 0 auto;" src="{@docRoot}reference/android/images/graphics/colorspace_bt2020.png" /> + * <figcaption style="text-align: center;">BT.2020 (orange) vs sRGB (white)</figcaption> + * </p> + */ + @FlaggedApi(Flags.FLAG_DISPLAY_BT2020_COLORSPACE) + DISPLAY_BT2020 // Update the initialization block next to #get(Named) when adding new values } @@ -1721,6 +1758,19 @@ public abstract class ColorSpace { Named.OK_LAB.ordinal() )); } + + if (Flags.displayBt2020Colorspace()) { + sNamedColorSpaceMap.put(Named.DISPLAY_BT2020.ordinal(), new ColorSpace.Rgb( + "BT 2020", + BT2020_PRIMARIES, + ILLUMINANT_D65, + null, + SRGB_TRANSFER_PARAMETERS, + Named.DISPLAY_BT2020.ordinal() + )); + sDataToColorSpaces.put(DataSpace.DATASPACE_DISPLAY_BT2020, + Named.DISPLAY_BT2020.ordinal()); + } } private static double transferHLGOETF(Rgb.TransferParameters params, double x) { diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index aeb734e2d2d3..5609663c01a0 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -184,6 +184,7 @@ android:layout_height="20dp" android:layout_gravity="end|center_vertical" android:layout_marginEnd="16dp" + android:layout_marginStart="10dp" android:contentDescription="@string/open_by_default_settings_text" android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings" android:tint="?androidprv:attr/materialColorOnSurface"/> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index d061ae1ef1a7..55cda783005f 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -43,7 +43,8 @@ <item name="android:layout_width">match_parent</item> <item name="android:layout_height">52dp</item> <item name="android:gravity">start|center_vertical</item> - <item name="android:padding">16dp</item> + <item name="android:paddingStart">16dp</item> + <item name="android:paddingEnd">0dp</item> <item name="android:textSize">14sp</item> <item name="android:textFontWeight">500</item> <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp index 9673c5f03642..23097f634464 100644 --- a/libs/hwui/utils/Color.cpp +++ b/libs/hwui/utils/Color.cpp @@ -17,6 +17,7 @@ #include "Color.h" #include <Properties.h> +#include <aidl/android/hardware/graphics/common/Dataspace.h> #include <android/hardware_buffer.h> #include <android/native_window.h> #include <ui/ColorSpace.h> @@ -25,6 +26,8 @@ #include <algorithm> #include <cmath> +#include "SkColorSpace.h" + namespace android { namespace uirenderer { @@ -215,9 +218,13 @@ android_dataspace ColorSpaceToADataSpace(SkColorSpace* colorSpace, SkColorType c return HAL_DATASPACE_ADOBE_RGB; } - if (nearlyEqual(fn, SkNamedTransferFn::kRec2020) && - nearlyEqual(gamut, SkNamedGamut::kRec2020)) { - return HAL_DATASPACE_BT2020; + if (nearlyEqual(gamut, SkNamedGamut::kRec2020)) { + if (nearlyEqual(fn, SkNamedTransferFn::kRec2020)) { + return HAL_DATASPACE_BT2020; + } else if (nearlyEqual(fn, SkNamedTransferFn::kSRGB)) { + return static_cast<android_dataspace>( + ::aidl::android::hardware::graphics::common::Dataspace::DISPLAY_BT2020); + } } if (nearlyEqual(fn, k2Dot6) && nearlyEqual(gamut, kDCIP3)) { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index bd015d041143..426b24dfe070 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1550,3 +1550,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "qs_tile_detailed_view" + namespace: "systemui" + description: "Enables the tile detailed view UI." + bug: "374173773" +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt index 4bbdfa44e087..d96e6643962d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt @@ -21,10 +21,11 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.testing.TestLifecycleOwner import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.testKosmos import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestResult import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.resetMain @@ -62,7 +63,7 @@ abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() { ): TestResult { return runTest { lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED) - lifecycleOwner.lifecycleScope.launch { underTest.activate() } + underTest.activateIn(kosmos.testScope) block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt index da166408b926..d16da1c359fb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt @@ -18,12 +18,13 @@ package com.android.systemui.qs.composefragment.viewmodel import android.testing.TestableLooper.RunWithLooper import androidx.test.filters.SmallTest -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.sysuiStatusBarStateController import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import org.junit.Test import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 @@ -32,6 +33,7 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) @RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) : AbstractQSFragmentComposeViewModelTest() { @@ -39,18 +41,18 @@ class QSFragmentComposeViewModelForceQSTest(private val testData: TestData) : fun forceQs_orRealExpansion() = with(kosmos) { testScope.testWithinLifecycle { - val expansionState by collectLastValue(underTest.expansionState) - with(testData) { sysuiStatusBarStateController.setState(statusBarState) - underTest.isQSExpanded = expanded + underTest.isQsExpanded = expanded underTest.isStackScrollerOverscrolling = stackScrollerOverScrolling fakeDeviceEntryRepository.setBypassEnabled(bypassEnabled) underTest.isTransitioningToFullShade = transitioningToFullShade underTest.isInSplitShade = inSplitShade - underTest.qsExpansionValue = EXPANSION - assertThat(expansionState!!.progress) + underTest.setQsExpansionValue(EXPANSION) + + runCurrent() + assertThat(underTest.expansionState.progress) .isEqualTo(if (expectedForceQS) 1f else EXPANSION) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index c19e4b834c7c..3b00f86c8f6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.qs.composefragment.viewmodel import android.app.StatusBarManager import android.content.testableContext import android.testing.TestableLooper.RunWithLooper +import androidx.compose.runtime.snapshots.Snapshot import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository @@ -33,6 +34,7 @@ import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository import com.android.systemui.statusbar.sysuiStatusBarStateController import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import org.junit.Test import org.junit.runner.RunWith @@ -40,22 +42,21 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() { @Test fun qsExpansionValueChanges_correctExpansionState() = with(kosmos) { testScope.testWithinLifecycle { - val expansionState by collectLastValue(underTest.expansionState) + underTest.setQsExpansionValue(0f) + assertThat(underTest.expansionState.progress).isEqualTo(0f) - underTest.qsExpansionValue = 0f - assertThat(expansionState!!.progress).isEqualTo(0f) + underTest.setQsExpansionValue(0.3f) + assertThat(underTest.expansionState.progress).isEqualTo(0.3f) - underTest.qsExpansionValue = 0.3f - assertThat(expansionState!!.progress).isEqualTo(0.3f) - - underTest.qsExpansionValue = 1f - assertThat(expansionState!!.progress).isEqualTo(1f) + underTest.setQsExpansionValue(1f) + assertThat(underTest.expansionState.progress).isEqualTo(1f) } } @@ -63,13 +64,11 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() fun qsExpansionValueChanges_clamped() = with(kosmos) { testScope.testWithinLifecycle { - val expansionState by collectLastValue(underTest.expansionState) - - underTest.qsExpansionValue = -1f - assertThat(expansionState!!.progress).isEqualTo(0f) + underTest.setQsExpansionValue(-1f) + assertThat(underTest.expansionState.progress).isEqualTo(0f) - underTest.qsExpansionValue = 2f - assertThat(expansionState!!.progress).isEqualTo(1f) + underTest.setQsExpansionValue(2f) + assertThat(underTest.expansionState.progress).isEqualTo(1f) } } @@ -77,15 +76,13 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() fun qqsHeaderHeight_largeScreenHeader_0() = with(kosmos) { testScope.testWithinLifecycle { - val qqsHeaderHeight by collectLastValue(underTest.qqsHeaderHeight) - testableContext.orCreateTestableResources.addOverride( R.bool.config_use_large_screen_shade_header, true, ) fakeConfigurationRepository.onConfigurationChange() - assertThat(qqsHeaderHeight).isEqualTo(0) + assertThat(underTest.qqsHeaderHeight).isEqualTo(0) } } @@ -93,15 +90,13 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() fun qqsHeaderHeight_noLargeScreenHeader_providedByHelper() = with(kosmos) { testScope.testWithinLifecycle { - val qqsHeaderHeight by collectLastValue(underTest.qqsHeaderHeight) - testableContext.orCreateTestableResources.addOverride( R.bool.config_use_large_screen_shade_header, false, ) fakeConfigurationRepository.onConfigurationChange() - assertThat(qqsHeaderHeight) + assertThat(underTest.qqsHeaderHeight) .isEqualTo(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) } } @@ -120,17 +115,17 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() fun statusBarState_followsController() = with(kosmos) { testScope.testWithinLifecycle { - val statusBarState by collectLastValue(underTest.statusBarState) - runCurrent() - sysuiStatusBarStateController.setState(StatusBarState.SHADE) - assertThat(statusBarState).isEqualTo(StatusBarState.SHADE) + runCurrent() + assertThat(underTest.statusBarState).isEqualTo(StatusBarState.SHADE) sysuiStatusBarStateController.setState(StatusBarState.KEYGUARD) - assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + runCurrent() + assertThat(underTest.statusBarState).isEqualTo(StatusBarState.KEYGUARD) sysuiStatusBarStateController.setState(StatusBarState.SHADE_LOCKED) - assertThat(statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED) + runCurrent() + assertThat(underTest.statusBarState).isEqualTo(StatusBarState.SHADE_LOCKED) } } @@ -138,17 +133,18 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() fun statusBarState_changesEarlyIfUpcomingStateIsKeyguard() = with(kosmos) { testScope.testWithinLifecycle { - val statusBarState by collectLastValue(underTest.statusBarState) - sysuiStatusBarStateController.setState(StatusBarState.SHADE) sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE_LOCKED) - assertThat(statusBarState).isEqualTo(StatusBarState.SHADE) + runCurrent() + assertThat(underTest.statusBarState).isEqualTo(StatusBarState.SHADE) sysuiStatusBarStateController.setUpcomingState(StatusBarState.KEYGUARD) - assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + runCurrent() + assertThat(underTest.statusBarState).isEqualTo(StatusBarState.KEYGUARD) sysuiStatusBarStateController.setUpcomingState(StatusBarState.SHADE) - assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD) + runCurrent() + assertThat(underTest.statusBarState).isEqualTo(StatusBarState.KEYGUARD) } } @@ -156,16 +152,16 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() fun qsEnabled_followsRepository() = with(kosmos) { testScope.testWithinLifecycle { - val qsEnabled by collectLastValue(underTest.qsEnabled) - fakeDisableFlagsRepository.disableFlags.value = DisableFlagsModel(disable2 = QS_DISABLE_FLAG) + runCurrent() - assertThat(qsEnabled).isFalse() + assertThat(underTest.isQsEnabled).isFalse() fakeDisableFlagsRepository.disableFlags.value = DisableFlagsModel() + runCurrent() - assertThat(qsEnabled).isTrue() + assertThat(underTest.isQsEnabled).isTrue() } } @@ -175,13 +171,16 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() testScope.testWithinLifecycle { val squishiness by collectLastValue(tileSquishinessInteractor.squishiness) - underTest.squishinessFractionValue = 0.3f + underTest.squishinessFraction = 0.3f + Snapshot.sendApplyNotifications() assertThat(squishiness).isWithin(epsilon).of(0.3f.constrainSquishiness()) - underTest.squishinessFractionValue = 0f + underTest.squishinessFraction = 0f + Snapshot.sendApplyNotifications() assertThat(squishiness).isWithin(epsilon).of(0f.constrainSquishiness()) - underTest.squishinessFractionValue = 1f + underTest.squishinessFraction = 1f + Snapshot.sendApplyNotifications() assertThat(squishiness).isWithin(epsilon).of(1f.constrainSquishiness()) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 49b44cb95e46..f4d91627fdab 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -45,6 +45,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -55,9 +56,11 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.layout.approachLayout import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.dimensionResource @@ -65,7 +68,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.round +import androidx.compose.ui.util.fastRoundToInt import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope @@ -143,9 +148,6 @@ constructor( private lateinit var viewModel: QSFragmentComposeViewModel - // Starting with a non-zero value makes it so that it has a non-zero height on first expansion - // This is important for `QuickSettingsControllerImpl.mMinExpansionHeight` to detect a "change". - private val qqsHeight = MutableStateFlow(1) private val qsHeight = MutableStateFlow(0) private val qqsVisible = MutableStateFlow(false) private val qqsPositionOnRoot = Rect() @@ -218,7 +220,7 @@ constructor( { notificationScrimClippingParams.params.top }, // Only allow scrolling when we are fully expanded. That way, we don't intercept // swipes in lockscreen (when somehow QS is receiving touches). - { scrollState.canScrollForward && viewModel.expansionState.value.progress >= 1f }, + { scrollState.canScrollForward && viewModel.isQsFullyExpanded }, ) frame.addView( composeView, @@ -231,16 +233,18 @@ constructor( @Composable private fun Content() { PlatformTheme { - val visible by viewModel.qsVisible.collectAsStateWithLifecycle() - AnimatedVisibility( - visible = visible, + visible = viewModel.isQsVisible, modifier = - Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf( - notificationScrimClippingParams.isEnabled - ) { - Modifier.notificationScrimClip { notificationScrimClippingParams.params } - }, + Modifier.windowInsetsPadding(WindowInsets.navigationBars) + .thenIf(notificationScrimClippingParams.isEnabled) { + Modifier.notificationScrimClip { + notificationScrimClippingParams.params + } + } + .offset { + IntOffset(x = 0, y = viewModel.viewTranslationY.fastRoundToInt()) + }, ) { val isEditing by viewModel.containerViewModel.editModeViewModel.isEditing @@ -254,7 +258,7 @@ constructor( label = "EditModeAnimatedContent", ) { editing -> if (editing) { - val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle() + val qqsPadding = viewModel.qqsHeaderHeight EditMode( viewModel = viewModel.containerViewModel.editModeViewModel, modifier = @@ -283,7 +287,7 @@ constructor( private fun CollapsableQuickSettingsSTL() { val sceneState = remember { MutableSceneTransitionLayoutState( - viewModel.expansionState.value.toIdleSceneKey(), + viewModel.expansionState.toIdleSceneKey(), transitions = transitions { from(QuickQuickSettings, QuickSettings) { @@ -294,7 +298,10 @@ constructor( } LaunchedEffect(Unit) { - synchronizeQsState(sceneState, viewModel.expansionState.map { it.progress }) + synchronizeQsState( + sceneState, + snapshotFlow { viewModel.expansionState }.map { it.progress }, + ) } SceneTransitionLayout(state = sceneState, modifier = Modifier.fillMaxSize()) { @@ -315,7 +322,7 @@ constructor( override fun getQsMinExpansionHeight(): Int { // TODO (b/353253277) implement split screen - return qqsHeight.value + return viewModel.qqsHeight } override fun getDesiredHeight(): Int { @@ -329,7 +336,7 @@ constructor( } override fun setHeightOverride(desiredHeight: Int) { - viewModel.heightOverrideValue = desiredHeight + viewModel.heightOverride = desiredHeight } override fun setHeaderClickable(qsExpansionEnabled: Boolean) { @@ -349,7 +356,7 @@ constructor( } override fun setExpanded(qsExpanded: Boolean) { - viewModel.isQSExpanded = qsExpanded + viewModel.isQsExpanded = qsExpanded } override fun setListening(listening: Boolean) { @@ -357,7 +364,7 @@ constructor( } override fun setQsVisible(qsVisible: Boolean) { - viewModel.isQSVisible = qsVisible + viewModel.isQsVisible = qsVisible } override fun isShowingDetail(): Boolean { @@ -378,11 +385,10 @@ constructor( headerTranslation: Float, squishinessFraction: Float, ) { - viewModel.qsExpansionValue = qsExpansionFraction - viewModel.panelExpansionFractionValue = panelExpansionFraction - viewModel.squishinessFractionValue = squishinessFraction - - // TODO(b/353254353) Handle header translation + viewModel.setQsExpansionValue(qsExpansionFraction) + viewModel.panelExpansionFraction = panelExpansionFraction + viewModel.squishinessFraction = squishinessFraction + viewModel.proposedTranslation = headerTranslation } override fun setHeaderListening(listening: Boolean) { @@ -402,7 +408,7 @@ constructor( } override fun getHeightDiff(): Int { - return 0 // For now TODO(b/353254353) + return viewModel.heightDiff } override fun getHeader(): View? { @@ -415,8 +421,8 @@ constructor( // TODO (b/353253280) } - override fun setInSplitShade(shouldTranslate: Boolean) { - // TODO (b/356435605) + override fun setInSplitShade(isInSplitShade: Boolean) { + viewModel.isInSplitShade = isInSplitShade } override fun setTransitionToFullShadeProgress( @@ -425,9 +431,9 @@ constructor( qsSquishinessFraction: Float, ) { viewModel.isTransitioningToFullShade = isTransitioningToFullShade - viewModel.lockscreenToShadeProgressValue = qsTransitionFraction + viewModel.lockscreenToShadeProgress = qsTransitionFraction if (isTransitioningToFullShade) { - viewModel.squishinessFractionValue = qsSquishinessFraction + viewModel.squishinessFraction = qsSquishinessFraction } } @@ -452,7 +458,7 @@ constructor( } override fun isFullyCollapsed(): Boolean { - return viewModel.qsExpansionValue <= 0f + return viewModel.isQsFullyCollapsed } override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) { @@ -464,11 +470,11 @@ constructor( } override fun setOverScrollAmount(overScrollAmount: Int) { - super.setOverScrollAmount(overScrollAmount) + viewModel.overScrollAmount = overScrollAmount } override fun setIsNotificationPanelFullWidth(isFullWidth: Boolean) { - viewModel.isSmallScreenValue = isFullWidth + viewModel.isSmallScreen = isFullWidth } override fun getHeaderTop(): Int { @@ -522,8 +528,8 @@ constructor( @Composable private fun SceneScope.QuickQuickSettingsElement() { - val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle() - val bottomPadding = dimensionResource(id = R.dimen.qqs_layout_padding_bottom) + val qqsPadding = viewModel.qqsHeaderHeight + val bottomPadding = viewModel.qqsBottomPadding DisposableEffect(Unit) { qqsVisible.value = true @@ -553,14 +559,13 @@ constructor( .approachLayout(isMeasurementApproachInProgress = { squishiness < 1f }) { measurable, constraints -> - qqsHeight.value = lookaheadSize.height + viewModel.qqsHeight = lookaheadSize.height val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.place(0, 0) } } - .padding(top = { qqsPadding }, bottom = { bottomPadding.roundToPx() }) + .padding(top = { qqsPadding }, bottom = { bottomPadding }) ) { - val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle() - if (qsEnabled) { + if (viewModel.isQsEnabled) { QuickQuickSettings( viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel, modifier = @@ -583,7 +588,7 @@ constructor( @Composable private fun SceneScope.QuickSettingsElement() { - val qqsPadding by viewModel.qqsHeaderHeight.collectAsStateWithLifecycle() + val qqsPadding = viewModel.qqsHeaderHeight val qsExtraPadding = dimensionResource(R.dimen.qs_panel_padding_top) Column( modifier = @@ -591,8 +596,7 @@ constructor( stringResource(id = R.string.accessibility_quick_settings_collapse) ) ) { - val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle() - if (qsEnabled) { + if (viewModel.isQsEnabled) { Box( modifier = Modifier.element(ElementKeys.QuickSettingsContent).fillMaxSize().weight(1f) @@ -602,7 +606,17 @@ constructor( onDispose { lifecycleScope.launch { scrollState.scrollTo(0) } } } - Column(modifier = Modifier.verticalScroll(scrollState)) { + Column( + modifier = + Modifier.offset { + IntOffset( + x = 0, + y = viewModel.qsScrollTranslationY.fastRoundToInt(), + ) + } + .onSizeChanged { viewModel.qsScrollHeight = it.height } + .verticalScroll(scrollState) + ) { Spacer( modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() } ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 7a8b2c2eb685..dd83fc928dad 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -20,19 +20,25 @@ import android.content.res.Resources import android.graphics.Rect import androidx.annotation.FloatRange import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.LifecycleCoroutineScope import com.android.systemui.Dumpable import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.FooterActionsController -import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel +import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState @@ -47,17 +53,15 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.io.PrintWriter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +@OptIn(ExperimentalCoroutinesApi::class) class QSFragmentComposeViewModel @AssistedInject constructor( @@ -75,262 +79,251 @@ constructor( private val paginatedGridViewModel: PaginatedGridViewModel, @Assisted private val lifecycleScope: LifecycleCoroutineScope, ) : Dumpable, ExclusiveActivatable() { + + private val hydrator = Hydrator("QSFragmentComposeViewModel.hydrator") + val footerActionsViewModel = footerActionsViewModelFactory.create(lifecycleScope).also { lifecycleScope.launch { footerActionsController.init() } } - private val _qsBounds = MutableStateFlow(Rect()) + var isQsExpanded by mutableStateOf(false) - private val _qsExpanded = MutableStateFlow(false) - var isQSExpanded: Boolean - get() = _qsExpanded.value - set(value) { - _qsExpanded.value = value - } - - private val _qsVisible = MutableStateFlow(false) - val qsVisible = _qsVisible.asStateFlow() - var isQSVisible: Boolean - get() = qsVisible.value - set(value) { - _qsVisible.value = value - } + var isQsVisible by mutableStateOf(false) // This can only be negative if undefined (in which case it will be -1f), else it will be // in [0, 1]. In some cases, it could be set back to -1f internally to indicate that it's // different to every value in [0, 1]. - @FloatRange(from = -1.0, to = 1.0) private val _qsExpansion = MutableStateFlow(-1f) - var qsExpansionValue: Float - get() = _qsExpansion.value - set(value) { - if (value < 0f) { - _qsExpansion.value = -1f - } - _qsExpansion.value = value.coerceIn(0f, 1f) - } + private var qsExpansion by mutableStateOf(-1f) - private val _panelFraction = MutableStateFlow(0f) - var panelExpansionFractionValue: Float - get() = _panelFraction.value - set(value) { - _panelFraction.value = value + fun setQsExpansionValue(value: Float) { + if (value < 0f) { + qsExpansion = -1f + } else { + qsExpansion = value.coerceIn(0f, 1f) } + } - private val _squishinessFraction = MutableStateFlow(1f) - var squishinessFractionValue: Float - get() = _squishinessFraction.value - set(value) { - _squishinessFraction.value = value - } + val isQsFullyCollapsed by derivedStateOf { qsExpansion <= 0f } - val qqsHeaderHeight = - configurationInteractor.onAnyConfigurationChange - .map { - if (LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)) { - 0 - } else { - largeScreenHeaderHelper.getLargeScreenHeaderHeight() - } - } - .stateIn(lifecycleScope, SharingStarted.WhileSubscribed(), 0) + var panelExpansionFraction by mutableStateOf(0f) - private val _headerAnimating = MutableStateFlow(false) + var squishinessFraction by mutableStateOf(1f) - private val _stackScrollerOverscrolling = MutableStateFlow(false) - var isStackScrollerOverscrolling: Boolean - get() = _stackScrollerOverscrolling.value - set(value) { - _stackScrollerOverscrolling.value = value - } + val qqsHeaderHeight by + hydrator.hydratedStateOf( + traceName = "qqsHeaderHeight", + initialValue = 0, + source = + configurationInteractor.onAnyConfigurationChange.map { + if (LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)) { + 0 + } else { + largeScreenHeaderHelper.getLargeScreenHeaderHeight() + } + }, + ) + + val qqsBottomPadding by + hydrator.hydratedStateOf( + traceName = "qqsBottomPadding", + initialValue = resources.getDimensionPixelSize(R.dimen.qqs_layout_padding_bottom), + source = configurationInteractor.dimensionPixelSize(R.dimen.qqs_layout_padding_bottom), + ) + + // Starting with a non-zero value makes it so that it has a non-zero height on first expansion + // This is important for `QuickSettingsControllerImpl.mMinExpansionHeight` to detect a "change". + var qqsHeight by mutableStateOf(1) + + var qsScrollHeight by mutableStateOf(0) + + val heightDiff: Int + get() = qsScrollHeight - qqsHeight + qqsBottomPadding + + var isStackScrollerOverscrolling by mutableStateOf(false) + + var proposedTranslation by mutableStateOf(0f) /** * Whether QS is enabled by policy. This is normally true, except when it's disabled by some * policy. See [DisableFlagsRepository]. */ - val qsEnabled = - disableFlagsRepository.disableFlags - .map { it.isQuickSettingsEnabled() } - .stateIn( - lifecycleScope, - SharingStarted.WhileSubscribed(), - disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(), - ) + val isQsEnabled by + hydrator.hydratedStateOf( + traceName = "isQsEnabled", + initialValue = disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(), + source = disableFlagsRepository.disableFlags.map { it.isQuickSettingsEnabled() }, + ) + + var isInSplitShade by mutableStateOf(false) + + var isTransitioningToFullShade by mutableStateOf(false) + + var lockscreenToShadeProgress by mutableStateOf(0f) + + var isSmallScreen by mutableStateOf(false) + + var heightOverride by mutableStateOf(-1) + + val expansionState by derivedStateOf { + if (forceQs) { + QSExpansionState(1f) + } else { + QSExpansionState(qsExpansion.coerceIn(0f, 1f)) + } + } + + val isQsFullyExpanded by derivedStateOf { expansionState.progress >= 1f } + + /** + * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for + * determining the correct action based on the expansion state. + */ + var collapseExpandAccessibilityAction: Runnable? = null + + val inFirstPage: Boolean + get() = paginatedGridViewModel.inFirstPage - private val _keyguardAndExpanded = MutableStateFlow(false) + var overScrollAmount by mutableStateOf(0) + + val viewTranslationY by derivedStateOf { + if (isOverscrolling) { + overScrollAmount.toFloat() + } else { + if (onKeyguardAndExpanded) { + translationScaleY * qqsHeight + } else { + headerTranslation + } + } + } + + val qsScrollTranslationY by derivedStateOf { + val panelTranslationY = translationScaleY * heightDiff + if (onKeyguardAndExpanded) panelTranslationY else 0f + } + + private var qsBounds by mutableStateOf(Rect()) + + private val constrainedSquishinessFraction: Float + get() = squishinessFraction.constrainSquishiness() + + private var _headerAnimating by mutableStateOf(false) + + private var keyguardAndExpanded by mutableStateOf(false) /** * Tracks the current [StatusBarState]. It will switch early if the upcoming state is * [StatusBarState.KEYGUARD] */ @get:VisibleForTesting - val statusBarState = - conflatedCallbackFlow { - val callback = - object : StatusBarStateController.StateListener { - override fun onStateChanged(newState: Int) { - trySend(newState) - } - - override fun onUpcomingStateChanged(upcomingState: Int) { - if (upcomingState == StatusBarState.KEYGUARD) { - trySend(upcomingState) + val statusBarState by + hydrator.hydratedStateOf( + traceName = "statusBarState", + initialValue = sysuiStatusBarStateController.state, + source = + conflatedCallbackFlow { + val callback = + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + trySend(newState) + } + + override fun onUpcomingStateChanged(upcomingState: Int) { + if (upcomingState == StatusBarState.KEYGUARD) { + trySend(upcomingState) + } + } } - } + sysuiStatusBarStateController.addCallback(callback) + + awaitClose { sysuiStatusBarStateController.removeCallback(callback) } } - sysuiStatusBarStateController.addCallback(callback) + .onStart { emit(sysuiStatusBarStateController.state) }, + ) - awaitClose { sysuiStatusBarStateController.removeCallback(callback) } - } - .onStart { emit(sysuiStatusBarStateController.state) } - .stateIn( - lifecycleScope, - SharingStarted.WhileSubscribed(), - sysuiStatusBarStateController.state, - ) - - private val isKeyguardState = - statusBarState - .map { it == StatusBarState.KEYGUARD } - .stateIn( - lifecycleScope, - SharingStarted.WhileSubscribed(), - statusBarState.value == StatusBarState.KEYGUARD, - ) - - private val _viewHeight = MutableStateFlow(0) - - private val _headerTranslation = MutableStateFlow(0f) - - private val _inSplitShade = MutableStateFlow(false) - var isInSplitShade: Boolean - get() = _inSplitShade.value - set(value) { - _inSplitShade.value = value - } + private val isKeyguardState: Boolean + get() = statusBarState == StatusBarState.KEYGUARD - private val _transitioningToFullShade = MutableStateFlow(false) - var isTransitioningToFullShade: Boolean - get() = _transitioningToFullShade.value - set(value) { - _transitioningToFullShade.value = value - } + private var viewHeight by mutableStateOf(0) - private val isBypassEnabled = deviceEntryInteractor.isBypassEnabled - - private val showCollapsedOnKeyguard = - combine( - isBypassEnabled, - _transitioningToFullShade, - _inSplitShade, - ::calculateShowCollapsedOnKeyguard, - ) - .stateIn( - lifecycleScope, - SharingStarted.WhileSubscribed(), - calculateShowCollapsedOnKeyguard( - isBypassEnabled.value, - isTransitioningToFullShade, - isInSplitShade, - ), - ) - - private val _lockscreenToShadeProgress = MutableStateFlow(0.0f) - var lockscreenToShadeProgressValue: Float - get() = _lockscreenToShadeProgress.value - set(value) { - _lockscreenToShadeProgress.value = value - } + private val isBypassEnabled by + hydrator.hydratedStateOf( + traceName = "isBypassEnabled", + source = deviceEntryInteractor.isBypassEnabled, + ) - private val _overscrolling = MutableStateFlow(false) + private val showCollapsedOnKeyguard by derivedStateOf { + isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade) + } - private val _isSmallScreen = MutableStateFlow(false) - var isSmallScreenValue: Boolean - get() = _isSmallScreen.value - set(value) { - _isSmallScreen.value = value - } + private val onKeyguardAndExpanded: Boolean + get() = isKeyguardState && !showCollapsedOnKeyguard - private val _shouldUpdateMediaSquishiness = MutableStateFlow(false) + private val isOverscrolling: Boolean + get() = overScrollAmount != 0 - private val _heightOverride = MutableStateFlow(-1) - val heightOverride = _heightOverride.asStateFlow() - var heightOverrideValue: Int - get() = heightOverride.value - set(value) { - _heightOverride.value = value - } + private var shouldUpdateMediaSquishiness by mutableStateOf(false) - private val forceQS = - combine( - _qsExpanded, - _stackScrollerOverscrolling, - isKeyguardState, - showCollapsedOnKeyguard, - ::calculateForceQs, - ) - .stateIn( - lifecycleScope, - SharingStarted.WhileSubscribed(), - calculateForceQs( - isQSExpanded, - isStackScrollerOverscrolling, - isKeyguardState.value, - showCollapsedOnKeyguard.value, - ), - ) - - val expansionState: StateFlow<QSExpansionState> = - combine(_qsExpansion, forceQS, ::calculateExpansionState) - .stateIn( - lifecycleScope, - SharingStarted.WhileSubscribed(), - calculateExpansionState(_qsExpansion.value, forceQS.value), - ) + private val forceQs by derivedStateOf { + (isQsExpanded || isStackScrollerOverscrolling) && + (isKeyguardState && !showCollapsedOnKeyguard) + } - /** - * Accessibility action for collapsing/expanding QS. The provided runnable is responsible for - * determining the correct action based on the expansion state. - */ - var collapseExpandAccessibilityAction: Runnable? = null + private val translationScaleY: Float + get() = (qsExpansion - 1) * (if (isInSplitShade) 1f else SHORT_PARALLAX_AMOUNT) - val inFirstPage: Boolean - get() = paginatedGridViewModel.inFirstPage + private val headerTranslation by derivedStateOf { + if (isTransitioningToFullShade) 0f else proposedTranslation + } override suspend fun onActivated(): Nothing { - hydrateSquishinessInteractor() + coroutineScope { + launch { hydrateSquishinessInteractor() } + launch { hydrator.activate() } + awaitCancellation() + } } - private suspend fun hydrateSquishinessInteractor(): Nothing { - _squishinessFraction.collect { - squishinessInteractor.setSquishinessValue(it.constrainSquishiness()) - } + private suspend fun hydrateSquishinessInteractor() { + snapshotFlow { constrainedSquishinessFraction } + .collect { squishinessInteractor.setSquishinessValue(it) } } override fun dump(pw: PrintWriter, args: Array<out String>) { pw.asIndenting().run { printSection("Quick Settings state") { - println("isQSExpanded", isQSExpanded) - println("isQSVisible", isQSVisible) - println("isQSEnabled", qsEnabled.value) + println("isQSExpanded", isQsExpanded) + println("isQSVisible", isQsVisible) + println("isQSEnabled", isQsEnabled) println("isCustomizing", containerViewModel.editModeViewModel.isEditing.value) } printSection("Expansion state") { - println("qsExpansion", qsExpansionValue) - println("panelExpansionFraction", panelExpansionFractionValue) - println("squishinessFraction", squishinessFractionValue) - println("expansionState", expansionState.value) - println("forceQS", forceQS.value) + println("qsExpansion", qsExpansion) + println("panelExpansionFraction", panelExpansionFraction) + println("squishinessFraction", squishinessFraction) + println("proposedTranslation", proposedTranslation) + println("expansionState", expansionState) + println("forceQS", forceQs) + printSection("Derived values") { + println("headerTranslation", headerTranslation) + println("translationScaleY", translationScaleY) + println("viewTranslationY", viewTranslationY) + println("qsScrollTranslationY", qsScrollTranslationY) + } } printSection("Shade state") { println("stackOverscrolling", isStackScrollerOverscrolling) - println("statusBarState", StatusBarState.toString(statusBarState.value)) - println("isKeyguardState", isKeyguardState.value) - println("isSmallScreen", isSmallScreenValue) - println("heightOverride", "${heightOverrideValue}px") - println("qqsHeaderHeight", "${qqsHeaderHeight.value}px") + println("statusBarState", StatusBarState.toString(statusBarState)) + println("isKeyguardState", isKeyguardState) + println("isSmallScreen", isSmallScreen) + println("heightOverride", "${heightOverride}px") + println("qqsHeaderHeight", "${qqsHeaderHeight}px") + println("qqsBottomPadding", "${qqsBottomPadding}px") println("isSplitShade", isInSplitShade) - println("showCollapsedOnKeyguard", showCollapsedOnKeyguard.value) + println("showCollapsedOnKeyguard", showCollapsedOnKeyguard) + println("qqsHeight", "${qqsHeight}px") + println("qsScrollHeight", "${qsScrollHeight}px") } } } @@ -340,7 +333,7 @@ constructor( fun create(lifecycleScope: LifecycleCoroutineScope): QSFragmentComposeViewModel } - // In the future, this will have other relevant elements like squishiness. + // In the future, this may have other relevant elements. data class QSExpansionState(@FloatRange(0.0, 1.0) val progress: Float) } @@ -348,30 +341,4 @@ private fun Float.constrainSquishiness(): Float { return (0.1f + this * 0.9f).coerceIn(0f, 1f) } -// Helper methods for combining flows. - -private fun calculateExpansionState(expansion: Float, forceQs: Boolean): QSExpansionState { - return if (forceQs) { - QSExpansionState(1f) - } else { - QSExpansionState(expansion.coerceIn(0f, 1f)) - } -} - -private fun calculateForceQs( - isQSExpanded: Boolean, - isStackOverScrolling: Boolean, - isKeyguardShowing: Boolean, - shouldShowCollapsedOnKeyguard: Boolean, -): Boolean { - return (isQSExpanded || isStackOverScrolling) && - (isKeyguardShowing && !shouldShowCollapsedOnKeyguard) -} - -private fun calculateShowCollapsedOnKeyguard( - isBypassEnabled: Boolean, - isTransitioningToFullShade: Boolean, - isInSplitShade: Boolean, -): Boolean { - return isBypassEnabled || (isTransitioningToFullShade && !isInSplitShade) -} +private val SHORT_PARALLAX_AMOUNT = 0.1f diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 73b7b35ba9a7..3441d94facda 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -35,7 +35,6 @@ import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; import static com.android.server.pm.UserManagerService.enforceCurrentUserIfVisibleBackgroundEnabled; -import static com.android.window.flags.Flags.deleteCaptureDisplay; import android.accessibilityservice.AccessibilityGestureEvent; import android.accessibilityservice.AccessibilityService; @@ -62,7 +61,6 @@ import android.graphics.ParcelableColorSpace; import android.graphics.Region; import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManager; -import android.hardware.display.DisplayManagerInternal; import android.hardware.usb.UsbDevice; import android.os.Binder; import android.os.Build; @@ -104,7 +102,6 @@ import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.os.SomeArgs; import com.android.internal.util.DumpUtils; import com.android.internal.util.function.pooled.PooledLambda; -import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection; import com.android.server.accessibility.magnification.MagnificationProcessor; import com.android.server.wm.WindowManagerInternal; @@ -1513,68 +1510,31 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ return; } final long identity = Binder.clearCallingIdentity(); - if (deleteCaptureDisplay()) { - try { - ScreenCapture.ScreenCaptureListener screenCaptureListener = new - ScreenCapture.ScreenCaptureListener( - (screenshotBuffer, result) -> { - if (screenshotBuffer != null && result == 0) { - sendScreenshotSuccess(screenshotBuffer, callback); - } else { - sendScreenshotFailure( - AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, - callback); - } + try { + ScreenCapture.ScreenCaptureListener screenCaptureListener = new + ScreenCapture.ScreenCaptureListener( + (screenshotBuffer, result) -> { + if (screenshotBuffer != null && result == 0) { + sendScreenshotSuccess(screenshotBuffer, callback); + } else { + sendScreenshotFailure( + AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, + callback); } - ); - mWindowManagerService.captureDisplay(displayId, null, screenCaptureListener); - } catch (Exception e) { - sendScreenshotFailure(AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, - callback); - } finally { - Binder.restoreCallingIdentity(identity); - } - } else { - try { - mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> { - final ScreenshotHardwareBuffer screenshotBuffer = LocalServices - .getService(DisplayManagerInternal.class).userScreenshot(displayId); - if (screenshotBuffer != null) { - sendScreenshotSuccess(screenshotBuffer, callback); - } else { - sendScreenshotFailure( - AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, - callback); } - }, null).recycleOnUse()); - } finally { - Binder.restoreCallingIdentity(identity); - } + ); + mWindowManagerService.captureDisplay(displayId, null, screenCaptureListener); + } catch (Exception e) { + sendScreenshotFailure(AccessibilityService.ERROR_TAKE_SCREENSHOT_INVALID_DISPLAY, + callback); + } finally { + Binder.restoreCallingIdentity(identity); } } private void sendScreenshotSuccess(ScreenshotHardwareBuffer screenshotBuffer, RemoteCallback callback) { - if (deleteCaptureDisplay()) { - mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> { - final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); - final ParcelableColorSpace colorSpace = - new ParcelableColorSpace(screenshotBuffer.getColorSpace()); - - final Bundle payload = new Bundle(); - payload.putInt(KEY_ACCESSIBILITY_SCREENSHOT_STATUS, - AccessibilityService.TAKE_SCREENSHOT_SUCCESS); - payload.putParcelable(KEY_ACCESSIBILITY_SCREENSHOT_HARDWAREBUFFER, - hardwareBuffer); - payload.putParcelable(KEY_ACCESSIBILITY_SCREENSHOT_COLORSPACE, colorSpace); - payload.putLong(KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP, - SystemClock.uptimeMillis()); - - // Send back the result. - callback.sendResult(payload); - hardwareBuffer.close(); - }, null).recycleOnUse()); - } else { + mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> { final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); final ParcelableColorSpace colorSpace = new ParcelableColorSpace(screenshotBuffer.getColorSpace()); @@ -1591,7 +1551,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // Send back the result. callback.sendResult(payload); hardwareBuffer.close(); - } + }, null).recycleOnUse()); } private void sendScreenshotFailure(@AccessibilityService.ScreenshotErrorCode int errorCode, diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index 0d3fa1b48b4c..4bd294be22b6 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -32,7 +32,6 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATI import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix; -import static com.android.window.flags.Flags.deleteCaptureDisplay; import android.animation.ArgbEvaluator; import android.content.Context; @@ -41,11 +40,9 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; -import android.os.IBinder; import android.os.Trace; import android.util.Slog; import android.util.proto.ProtoOutputStream; -import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.Surface; import android.view.Surface.OutOfResourcesException; @@ -58,7 +55,6 @@ import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; -import com.android.server.display.DisplayControl; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; @@ -171,33 +167,7 @@ class ScreenRotationAnimation { try { final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer; - if (isSizeChanged && !deleteCaptureDisplay()) { - final DisplayAddress address = displayInfo.address; - if (!(address instanceof DisplayAddress.Physical)) { - Slog.e(TAG, "Display does not have a physical address: " + displayId); - return; - } - final DisplayAddress.Physical physicalAddress = - (DisplayAddress.Physical) address; - final IBinder displayToken = DisplayControl.getPhysicalDisplayToken( - physicalAddress.getPhysicalDisplayId()); - if (displayToken == null) { - Slog.e(TAG, "Display token is null."); - return; - } - // Temporarily not skip screenshot for the rounded corner overlays and screenshot - // the whole display to include the rounded corner overlays. - setSkipScreenshotForRoundedCornerOverlays(false, t); - mRoundedCornerOverlay = displayContent.findRoundedCornerOverlays(); - final ScreenCapture.DisplayCaptureArgs captureArgs = - new ScreenCapture.DisplayCaptureArgs.Builder(displayToken) - .setSourceCrop(new Rect(0, 0, width, height)) - .setAllowProtected(true) - .setCaptureSecureLayers(true) - .setHintForSeamlessTransition(true) - .build(); - screenshotBuffer = ScreenCapture.captureDisplay(captureArgs); - } else if (isSizeChanged) { + if (isSizeChanged) { // Temporarily not skip screenshot for the rounded corner overlays and screenshot // the whole display to include the rounded corner overlays. setSkipScreenshotForRoundedCornerOverlays(false, t); diff --git a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java index 6e8094ac21d7..1b42e13039be 100644 --- a/services/core/java/com/android/server/wm/WindowTracingPerfetto.java +++ b/services/core/java/com/android/server/wm/WindowTracingPerfetto.java @@ -159,22 +159,28 @@ class WindowTracingPerfetto extends WindowTracing { void onStart(WindowTracingDataSource.Config config) { if (config.mLogFrequency == WindowTracingLogFrequency.FRAME) { + Log.i(TAG, "Started session (frequency=FRAME, log level=" + config.mLogFrequency + ")"); mCountSessionsOnFrame.incrementAndGet(); } else if (config.mLogFrequency == WindowTracingLogFrequency.TRANSACTION) { + Log.i(TAG, "Started session (frequency=TRANSACTION, log level=" + + config.mLogFrequency + ")"); mCountSessionsOnTransaction.incrementAndGet(); } - Log.i(TAG, "Started with logLevel: " + config.mLogLevel - + " logFrequency: " + config.mLogFrequency); + Log.i(TAG, getStatus()); + log(WHERE_START_TRACING); } void onStop(WindowTracingDataSource.Config config) { if (config.mLogFrequency == WindowTracingLogFrequency.FRAME) { + Log.i(TAG, "Stopped session (frequency=FRAME)"); mCountSessionsOnFrame.decrementAndGet(); + Log.i(TAG, "Stopped session (frequency=TRANSACTION)"); } else if (config.mLogFrequency == WindowTracingLogFrequency.TRANSACTION) { mCountSessionsOnTransaction.decrementAndGet(); } - Log.i(TAG, "Stopped"); + + Log.i(TAG, getStatus()); } } |