summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt2
-rw-r--r--core/java/android/app/Notification.java2
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java736
-rw-r--r--core/java/android/app/SystemServiceRegistry.java69
-rw-r--r--core/java/android/hardware/DataSpace.java18
-rw-r--r--core/java/android/net/vcn/VcnFrameworkInitializer.java106
-rw-r--r--core/java/android/view/RoundScrollbarRenderer.java249
-rw-r--r--core/java/android/view/flags/view_flags.aconfig8
-rw-r--r--core/tests/coretests/src/android/view/RoundScrollbarRendererTest.java146
-rw-r--r--graphics/java/android/framework_graphics.aconfig9
-rw-r--r--graphics/java/android/graphics/ColorSpace.java52
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml1
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml3
-rw-r--r--libs/hwui/utils/Color.cpp13
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelForceQSTest.kt14
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt75
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt98
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt437
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java78
-rw-r--r--services/core/java/com/android/server/wm/ScreenRotationAnimation.java32
-rw-r--r--services/core/java/com/android/server/wm/WindowTracingPerfetto.java12
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());
}
}