diff options
103 files changed, 2272 insertions, 920 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index f59b57528ef6..14a91c9033c5 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8781,7 +8781,6 @@ package android.app.admin { package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { - method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); @@ -8793,9 +8792,7 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); - method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/core/java/Android.bp b/core/java/Android.bp index 65cd984ac1ba..d12e1bf77e17 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -19,6 +19,7 @@ filegroup { srcs: [ "**/*.java", "**/*.aidl", + ":systemfeatures-gen-srcs", ":framework-nfc-non-updatable-sources", ":messagequeue-gen", ":ranging_stack_mock_initializer", @@ -665,3 +666,29 @@ java_library { } // protolog end + +// Whether to enable read-only system feature codegen. +gen_readonly_feature_apis = select(release_flag("RELEASE_USE_SYSTEM_FEATURE_BUILD_FLAGS"), { + true: "true", + false: "false", + default: "false", +}) + +// Generates com.android.internal.pm.RoSystemFeatures, optionally compiling in +// details about fixed system features defined by build flags. When disabled, +// the APIs are simply passthrough stubs with no meaningful side effects. +genrule { + name: "systemfeatures-gen-srcs", + cmd: "$(location systemfeatures-gen-tool) com.android.internal.pm.RoSystemFeatures " + + // --readonly=false (default) makes the codegen an effective no-op passthrough API. + " --readonly=" + gen_readonly_feature_apis + + // For now, only export "android.hardware.type.*" system features APIs. + // TODO(b/203143243): Use an intermediate soong var that aggregates all declared + // RELEASE_SYSTEM_FEATURE_* declarations into a single arg. + " --feature-apis=AUTOMOTIVE,WATCH,TELEVISION,EMBEDDED,PC" + + " > $(out)", + out: [ + "RoSystemFeatures.java", + ], + tools: ["systemfeatures-gen-tool"], +} diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 7273e64846c0..36fc65a76d53 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1031,7 +1031,9 @@ public class ActivityManager { | PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL; /** - * All implicit capabilities. There are capabilities that process automatically have. + * All implicit capabilities. This capability set is currently only used for processes under + * active instrumentation. The intent is to allow CTS tests to always have these capabilities + * so that every test doesn't need to launch FGS. * @hide */ @TestApi diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 3bd121a4a19b..f80121d0c9b6 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -1328,7 +1328,8 @@ public abstract class ActivityManagerInternal { * Add a creator token for all embedded intents (stored as extra) of the given intent. * * @param intent The given intent + * @param creatorPackage the package name of the creator app. * @hide */ - public abstract void addCreatorToken(Intent intent); + public abstract void addCreatorToken(Intent intent, String creatorPackage); } diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index d363e19bcc19..ba71afb49629 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -97,6 +97,7 @@ per-file InstantAppResolveInfo.aidl = file:/services/core/java/com/android/serve # Performance per-file PropertyInvalidatedCache.java = file:/PERFORMANCE_OWNERS +per-file performance.aconfig = file:/PERFORMANCE_OWNERS # Pinner per-file pinner-client.aconfig = file:/core/java/android/app/pinner/OWNERS diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index c17da249f322..55296ebbf18e 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -16,6 +16,8 @@ package android.app; +import static android.text.TextUtils.formatSimple; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -30,11 +32,10 @@ import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.FastPrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -42,12 +43,14 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; /** @@ -224,12 +227,24 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note - * that all values cause the cache to be skipped. + * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note that all + * reserved values cause the cache to be skipped. */ + // This is the initial value of all cache keys. It is changed when a cache is invalidated. private static final int NONCE_UNSET = 0; + // This value is used in two ways. First, it is used internally to indicate that the cache is + // disabled for the current query. Secondly, it is used to 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) { @@ -237,7 +252,7 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * The names of the nonces + * The names of the reserved nonces. */ private static final String[] sNonceName = new String[]{ "unset", "disabled", "corked", "bypass" }; @@ -277,32 +292,17 @@ public class PropertyInvalidatedCache<Query, Result> { private static final Object sCorkLock = new Object(); /** - * Record the number of invalidate or cork calls that were nops because the cache was already - * corked. This is static because invalidation is done in a static context. Entries are - * indexed by the cache property. - */ - @GuardedBy("sCorkLock") - private static final HashMap<String, Long> sCorkedInvalidates = new HashMap<>(); - - /** - * A map of cache keys that we've "corked". (The values are counts.) When a cache key is - * corked, we skip the cache invalidate when the cache key is in the unset state --- that - * is, when a cache key is corked, an invalidation does not enable the cache if somebody - * else hasn't disabled it. - */ - @GuardedBy("sCorkLock") - private static final HashMap<String, Integer> sCorks = new HashMap<>(); - - /** * A lock for the global list of caches and cache keys. This must never be taken inside mLock * or sCorkLock. */ private static final Object sGlobalLock = new Object(); /** - * A map of cache keys that have been disabled in the local process. When a key is - * disabled locally, existing caches are disabled and the key is saved in this map. - * Future cache instances that use the same key will be disabled in their constructor. + * A map of cache keys that have been disabled in the local process. When a key is disabled + * locally, existing caches are disabled and the key is saved in this map. Future cache + * instances that use the same key will be disabled in their constructor. Note that "disabled" + * means the cache is not used in this process. Invalidation still proceeds normally, because + * the cache may be used in other processes. */ @GuardedBy("sGlobalLock") private static final HashSet<String> sDisabledKeys = new HashSet<>(); @@ -315,14 +315,6 @@ public class PropertyInvalidatedCache<Query, Result> { private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches = new WeakHashMap<>(); /** - * Counts of the number of times a cache key was invalidated. Invalidation occurs in a static - * context with no cache object available, so this is a static map. Entries are indexed by - * the cache property. - */ - @GuardedBy("sGlobalLock") - private static final HashMap<String, Long> sInvalidates = new HashMap<>(); - - /** * If sEnabled is false then all cache operations are stubbed out. Set * it to false inside test processes. */ @@ -334,12 +326,6 @@ public class PropertyInvalidatedCache<Query, Result> { private final String mPropertyName; /** - * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the - * property exists on the system. - */ - private volatile SystemProperties.Handle mPropertyHandle; - - /** * The name by which this cache is known. This should normally be the * binder call that is being cached, but the constructors default it to * the property name. @@ -369,7 +355,13 @@ public class PropertyInvalidatedCache<Query, Result> { private final LinkedHashMap<Query, Result> mCache; /** - * The last value of the {@code mPropertyHandle} that we observed. + * The nonce handler for this cache. + */ + @GuardedBy("mLock") + private final NonceHandler mNonce; + + /** + * The last nonce value that was observed. */ @GuardedBy("mLock") private long mLastSeenNonce = NONCE_UNSET; @@ -385,6 +377,297 @@ 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. @@ -417,6 +700,7 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = propertyName; validateCacheKey(mPropertyName); mCacheName = cacheName; + mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = new DefaultComputer<>(this); mCache = createMap(); @@ -441,6 +725,7 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = createPropertyName(module, api); validateCacheKey(mPropertyName); mCacheName = cacheName; + mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = computer; mCache = createMap(); @@ -484,130 +769,58 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * SystemProperties are protected and cannot be written (or read, usually) by random - * processes. So, for testing purposes, the methods have a bypass mode that reads and - * writes to a HashMap and does not go out to the SystemProperties at all. - */ - - // If true, the cache might be under test. If false, there is no testing in progress. - private static volatile boolean sTesting = false; - - // If sTesting is true then keys that are under test are in this map. - private static final HashMap<String, Long> sTestingPropertyMap = new HashMap<>(); - - /** - * Enable or disable testing. The testing property map is cleared every time this - * method is called. + * Enable or disable testing. At this time, no action is taken when testing begins. * @hide */ @TestApi public static void setTestMode(boolean mode) { - sTesting = mode; - synchronized (sTestingPropertyMap) { - sTestingPropertyMap.clear(); - } - } - - /** - * 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. - */ - private static void testPropertyName(@NonNull String name) { - synchronized (sTestingPropertyMap) { - sTestingPropertyMap.put(name, (long) NONCE_UNSET); + if (mode) { + // No action when testing begins. + } else { + resetAfterTest(); } } /** - * Enable testing the specific cache key. Only keys in the map are subject to testing. - * There is no method to stop testing a property name. Just disable the test mode. + * Enable testing the specific cache key. This is a legacy API that will be removed as part of + * b/360897450. * @hide */ @TestApi public void testPropertyName() { - testPropertyName(mPropertyName); - } - - // Read the system property associated with the current cache. This method uses the - // handle for faster reading. - private long getCurrentNonce() { - if (sTesting) { - synchronized (sTestingPropertyMap) { - Long n = sTestingPropertyMap.get(mPropertyName); - if (n != null) { - return n; - } - } - } - - SystemProperties.Handle handle = mPropertyHandle; - if (handle == null) { - handle = SystemProperties.find(mPropertyName); - if (handle == null) { - return NONCE_UNSET; - } - mPropertyHandle = handle; - } - return handle.getLong(NONCE_UNSET); } - // Write the nonce in a static context. No handle is available. - private static void setNonce(String name, long val) { - if (sTesting) { - synchronized (sTestingPropertyMap) { - Long n = sTestingPropertyMap.get(name); - if (n != null) { - sTestingPropertyMap.put(name, val); - return; - } - } - } - RuntimeException failure = null; - for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) { - try { - SystemProperties.set(name, Long.toString(val)); - if (attempt > 0) { - // This log is not guarded. Based on known bug reports, it should - // occur once a week or less. The purpose of the log message is to - // identify the retries as a source of delay that might be otherwise - // be attributed to the cache itself. - Log.w(TAG, "Nonce set after " + attempt + " tries"); - } - return; - } catch (RuntimeException e) { - if (failure == null) { - failure = e; - } - try { - Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS); - } catch (InterruptedException x) { - // Ignore this exception. The desired delay is only approximate and - // there is no issue if the sleep sometimes terminates early. + /** + * 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. + * @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); } } } - // This point is reached only if SystemProperties.set() fails at least once. - // Rethrow the first exception that was received. - throw failure; } - // Set the nonce in a static context. No handle is available. - private static long getNonce(String name) { - if (sTesting) { - synchronized (sTestingPropertyMap) { - Long n = sTestingPropertyMap.get(name); - if (n != null) { - return n; - } - } - } - return SystemProperties.getLong(name, NONCE_UNSET); + // Read the nonce associated with the current cache. + @GuardedBy("mLock") + private long getCurrentNonce() { + return mNonce.getNonce(); } /** - * Forget all cached values. - * TODO(216112648) remove this as a public API. Clients should invalidate caches, not clear - * them. + * Forget all cached values. This is used by a client when the server exits. Since the + * server has exited, the cache values are no longer valid, but the server is no longer + * present to invalidate the cache. Note that this is not necessary if the server is + * system_server, because the entire operating system reboots if that process exits. * @hide */ public final void clear() { @@ -674,7 +887,7 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Disable the use of this cache in this process. This method is using internally and during + * Disable the use of this cache in this process. This method is used internally and during * testing. To disable a cache in normal code, use disableLocal(). A disabled cache cannot * be re-enabled. * @hide @@ -783,7 +996,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (DEBUG) { if (!mDisabled) { - Log.d(TAG, TextUtils.formatSimple( + Log.d(TAG, formatSimple( "cache %s %s for %s", cacheName(), sNonceName[(int) currentNonce], queryToString(query))); } @@ -798,7 +1011,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (cachedResult != null) mHits++; } else { if (DEBUG) { - Log.d(TAG, TextUtils.formatSimple( + Log.d(TAG, formatSimple( "clearing cache %s of %d entries because nonce changed [%s] -> [%s]", cacheName(), mCache.size(), mLastSeenNonce, currentNonce)); @@ -824,7 +1037,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (currentNonce != afterRefreshNonce) { currentNonce = afterRefreshNonce; if (DEBUG) { - Log.d(TAG, TextUtils.formatSimple( + Log.d(TAG, formatSimple( "restarting %s %s because nonce changed in refresh", cacheName(), queryToString(query))); @@ -895,10 +1108,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @param name Name of the cache-key property to invalidate */ private static void disableSystemWide(@NonNull String name) { - if (!sEnabled) { - return; - } - setNonce(name, NONCE_DISABLED); + getNonceHandler(name).disable(); } /** @@ -908,7 +1118,7 @@ public class PropertyInvalidatedCache<Query, Result> { */ @TestApi public void invalidateCache() { - invalidateCache(mPropertyName); + mNonce.invalidate(); } /** @@ -931,59 +1141,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void invalidateCache(@NonNull String name) { - if (!sEnabled) { - if (DEBUG) { - Log.w(TAG, TextUtils.formatSimple( - "cache invalidate %s suppressed", name)); - } - return; - } - - // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't - // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork. - // The property service is single-threaded anyway, so we don't lose any concurrency by - // taking the cork lock around cache invalidations. If we see contention on this lock, - // we're invalidating too often. - synchronized (sCorkLock) { - Integer numberCorks = sCorks.get(name); - if (numberCorks != null && numberCorks > 0) { - if (DEBUG) { - Log.d(TAG, "ignoring invalidation due to cork: " + name); - } - final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); - sCorkedInvalidates.put(name, count + 1); - return; - } - invalidateCacheLocked(name); - } - } - - @GuardedBy("sCorkLock") - private static void invalidateCacheLocked(@NonNull String name) { - // There's no race here: we don't require that values strictly increase, but instead - // only that each is unique in a single runtime-restart session. - final long nonce = getNonce(name); - if (nonce == NONCE_DISABLED) { - if (DEBUG) { - Log.d(TAG, "refusing to invalidate disabled cache: " + name); - } - return; - } - - long newValue; - do { - newValue = NoPreloadHolder.next(); - } while (isReservedNonce(newValue)); - if (DEBUG) { - Log.d(TAG, TextUtils.formatSimple( - "invalidating cache [%s]: [%s] -> [%s]", - name, nonce, Long.toString(newValue))); - } - // There is a small race with concurrent disables here. A compare-and-exchange - // property operation would be required to eliminate the race condition. - setNonce(name, newValue); - long invalidateCount = sInvalidates.getOrDefault(name, (long) 0); - sInvalidates.put(name, ++invalidateCount); + getNonceHandler(name).invalidate(); } /** @@ -1000,43 +1158,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void corkInvalidations(@NonNull String name) { - if (!sEnabled) { - if (DEBUG) { - Log.w(TAG, TextUtils.formatSimple( - "cache cork %s suppressed", name)); - } - return; - } - - synchronized (sCorkLock) { - int numberCorks = sCorks.getOrDefault(name, 0); - if (DEBUG) { - Log.d(TAG, TextUtils.formatSimple( - "corking %s: numberCorks=%s", name, numberCorks)); - } - - // If we're the first ones to cork this cache, set the cache to the corked state so - // existing caches talk directly to their services while we've corked updates. - // Make sure we don't clobber a disabled cache value. - - // TODO(dancol): we can skip this property write and leave the cache enabled if the - // caller promises not to make observable changes to the cache backing state before - // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair. - // Implement this more dangerous mode of operation if necessary. - if (numberCorks == 0) { - final long nonce = getNonce(name); - if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) { - setNonce(name, NONCE_CORKED); - } - } else { - final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); - sCorkedInvalidates.put(name, count + 1); - } - sCorks.put(name, numberCorks + 1); - if (DEBUG) { - Log.d(TAG, "corked: " + name); - } - } + getNonceHandler(name).cork(); } /** @@ -1048,34 +1170,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void uncorkInvalidations(@NonNull String name) { - if (!sEnabled) { - if (DEBUG) { - Log.w(TAG, TextUtils.formatSimple( - "cache uncork %s suppressed", name)); - } - return; - } - - synchronized (sCorkLock) { - int numberCorks = sCorks.getOrDefault(name, 0); - if (DEBUG) { - Log.d(TAG, TextUtils.formatSimple( - "uncorking %s: numberCorks=%s", name, numberCorks)); - } - - if (numberCorks < 1) { - throw new AssertionError("cork underflow: " + name); - } - if (numberCorks == 1) { - sCorks.remove(name); - invalidateCacheLocked(name); - if (DEBUG) { - Log.d(TAG, "uncorked: " + name); - } - } else { - sCorks.put(name, numberCorks - 1); - } - } + getNonceHandler(name).uncork(); } /** @@ -1104,6 +1199,8 @@ public class PropertyInvalidatedCache<Query, Result> { @GuardedBy("mLock") private Handler mHandler; + private NonceHandler mNonce; + public AutoCorker(@NonNull String propertyName) { this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS); } @@ -1117,31 +1214,35 @@ public class PropertyInvalidatedCache<Query, Result> { } public void autoCork() { + synchronized (mLock) { + if (mNonce == null) { + mNonce = getNonceHandler(mPropertyName); + } + } + if (getLooper() == null) { // We're not ready to auto-cork yet, so just invalidate the cache immediately. if (DEBUG) { Log.w(TAG, "invalidating instead of autocorking early in init: " + mPropertyName); } - PropertyInvalidatedCache.invalidateCache(mPropertyName); + mNonce.invalidate(); return; } synchronized (mLock) { boolean alreadyQueued = mUncorkDeadlineMs >= 0; if (DEBUG) { - Log.w(TAG, TextUtils.formatSimple( + Log.d(TAG, formatSimple( "autoCork %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs; if (!alreadyQueued) { getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs); - PropertyInvalidatedCache.corkInvalidations(mPropertyName); + mNonce.cork(); } else { - synchronized (sCorkLock) { - final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); - sCorkedInvalidates.put(mPropertyName, count + 1); - } + // Count this as a corked invalidation. + mNonce.invalidate(); } } } @@ -1149,7 +1250,7 @@ public class PropertyInvalidatedCache<Query, Result> { private void handleMessage(Message msg) { synchronized (mLock) { if (DEBUG) { - Log.w(TAG, TextUtils.formatSimple( + Log.d(TAG, formatSimple( "handleMsesage %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } @@ -1161,7 +1262,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (mUncorkDeadlineMs > nowMs) { mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs; if (DEBUG) { - Log.w(TAG, TextUtils.formatSimple( + Log.d(TAG, formatSimple( "scheduling uncork at %s", mUncorkDeadlineMs)); } @@ -1169,10 +1270,10 @@ public class PropertyInvalidatedCache<Query, Result> { return; } if (DEBUG) { - Log.w(TAG, "automatic uncorking " + mPropertyName); + Log.d(TAG, "automatic uncorking " + mPropertyName); } mUncorkDeadlineMs = -1; - PropertyInvalidatedCache.uncorkInvalidations(mPropertyName); + mNonce.uncork(); } } @@ -1207,7 +1308,7 @@ public class PropertyInvalidatedCache<Query, Result> { Result resultToCompare = recompute(query); boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce); if (!nonceChanged && !resultEquals(proposedResult, resultToCompare)) { - Log.e(TAG, TextUtils.formatSimple( + Log.e(TAG, formatSimple( "cache %s inconsistent for %s is %s should be %s", cacheName(), queryToString(query), proposedResult, resultToCompare)); @@ -1284,17 +1385,9 @@ public class PropertyInvalidatedCache<Query, Result> { /** * Returns a list of caches alive at the current time. */ - @GuardedBy("sGlobalLock") private static @NonNull ArrayList<PropertyInvalidatedCache> getActiveCaches() { - return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet()); - } - - /** - * Returns a list of the active corks in a process. - */ - private static @NonNull ArrayList<Map.Entry<String, Integer>> getActiveCorks() { - synchronized (sCorkLock) { - return new ArrayList<Map.Entry<String, Integer>>(sCorks.entrySet()); + synchronized (sGlobalLock) { + return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet()); } } @@ -1361,32 +1454,27 @@ public class PropertyInvalidatedCache<Query, Result> { return; } - long invalidateCount; - long corkedInvalidates; - synchronized (sCorkLock) { - invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0); - corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); - } + NonceHandler.Stats stats = mNonce.getStats(); synchronized (mLock) { - pw.println(TextUtils.formatSimple(" Cache Name: %s", cacheName())); - pw.println(TextUtils.formatSimple(" Property: %s", mPropertyName)); + pw.println(formatSimple(" Cache Name: %s", cacheName())); + pw.println(formatSimple(" Property: %s", mPropertyName)); final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED] + mSkips[NONCE_BYPASS]; - pw.println(TextUtils.formatSimple( + pw.println(formatSimple( " Hits: %d, Misses: %d, Skips: %d, Clears: %d", mHits, mMisses, skips, mClears)); - pw.println(TextUtils.formatSimple( + pw.println(formatSimple( " Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d", mSkips[NONCE_CORKED], mSkips[NONCE_UNSET], mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED])); - pw.println(TextUtils.formatSimple( + pw.println(formatSimple( " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", - mLastSeenNonce, invalidateCount, corkedInvalidates)); - pw.println(TextUtils.formatSimple( + mLastSeenNonce, stats.invalidated, stats.corkedInvalidates)); + pw.println(formatSimple( " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); - pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); + pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); pw.println(""); // No specific cache was requested. This is the default, and no details @@ -1404,23 +1492,7 @@ public class PropertyInvalidatedCache<Query, Result> { String key = Objects.toString(entry.getKey()); String value = Objects.toString(entry.getValue()); - pw.println(TextUtils.formatSimple(" Key: %s\n Value: %s\n", key, value)); - } - } - } - - /** - * Dump the corking status. - */ - @GuardedBy("sCorkLock") - private static void dumpCorkInfo(PrintWriter pw) { - ArrayList<Map.Entry<String, Integer>> activeCorks = getActiveCorks(); - if (activeCorks.size() > 0) { - pw.println(" Corking Status:"); - for (int i = 0; i < activeCorks.size(); i++) { - Map.Entry<String, Integer> entry = activeCorks.get(i); - pw.println(TextUtils.formatSimple(" Property Name: %s Count: %d", - entry.getKey(), entry.getValue())); + pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value)); } } } @@ -1441,14 +1513,7 @@ public class PropertyInvalidatedCache<Query, Result> { // then only that cache is reported. boolean detail = anyDetailed(args); - ArrayList<PropertyInvalidatedCache> activeCaches; - synchronized (sGlobalLock) { - activeCaches = getActiveCaches(); - if (!detail) { - dumpCorkInfo(pw); - } - } - + ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches(); for (int i = 0; i < activeCaches.size(); i++) { PropertyInvalidatedCache currentCache = activeCaches.get(i); currentCache.dumpContents(pw, detail, args); diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 439d988e2588..dca433696fe7 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -110,40 +110,6 @@ public final class AppFunctionManager { * * @param request the request to execute the app function * @param executor the executor to run the callback - * @param callback the callback to receive the function execution result. if the calling app - * does not own the app function or does not have {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code - * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code - * ExecuteAppFunctionResponse.RESULT_DENIED}. - * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) - @UserHandleAware - @Deprecated - public void executeAppFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull @CallbackExecutor Executor executor, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - executeAppFunction(request, executor, new CancellationSignal(), callback); - } - - /** - * Executes the app function. - * - * <p>Note: Applications can execute functions they define. To execute functions defined in - * another component, apps would need to have {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code - * android.permission.EXECUTE_APP_FUNCTIONS}. - * - * @param request the request to execute the app function - * @param executor the executor to run the callback * @param cancellationSignal the cancellation signal to cancel the execution. * @param callback the callback to receive the function execution result. if the calling app * does not own the app function or does not have {@code diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index ceca850a1037..63d187aa11ef 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -158,74 +158,6 @@ public abstract class AppFunctionService extends Service { * thread and dispatch the result with the given callback. You should always report back the * result using the callback, no matter if the execution was successful or not. * - * @param request The function execution request. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, - * Consumer)} instead. This method will be removed once usage references are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - Log.w( - "AppFunctionService", - "Calling deprecated default implementation of onExecuteFunction"); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel - * the execution of function if requested by the system. - * - * @param request The function execution request. - * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel * the execution of function if requested by the system. * @@ -235,11 +167,9 @@ public abstract class AppFunctionService extends Service { * @param callback A callback to report back the result. */ @MainThread - public void onExecuteFunction( + public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, cancellationSignal, callback); - } + @NonNull Consumer<ExecuteAppFunctionResponse> callback); } diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 7f30d7cccb57..124973489dd1 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -287,7 +287,7 @@ public final class AssociationInfo implements Parcelable { /** * Get the device icon of the associated device. The device icon represents the device type. * - * @return the device icon, or {@code null} if no device icon is has been set for the + * @return the device icon, or {@code null} if no device icon has been set for the * associated device. * * @see AssociationRequest.Builder#setDeviceIcon(Icon) diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index 41a6791d8a7b..f368935a74c8 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -475,8 +475,8 @@ public final class AssociationRequest implements Parcelable { } /** - * Set the device icon for the self-managed device and this icon will be - * displayed in the self-managed association dialog. + * Set the device icon for the self-managed device and to display the icon in the + * self-managed association dialog. * * @throws IllegalArgumentException if the icon is not exactly 24dp by 24dp * or if it is {@link Icon#TYPE_URI} or {@link Icon#TYPE_URI_ADAPTIVE_BITMAP}. diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index dfad6de4ba16..4472c3d13d7c 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -478,6 +478,15 @@ public final class CompanionDeviceManager { Objects.requireNonNull(callback, "Callback cannot be null"); handler = Handler.mainIfNull(handler); + if (Flags.associationDeviceIcon()) { + final Icon deviceIcon = request.getDeviceIcon(); + + if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { + throw new IllegalArgumentException("The size of the device icon must be " + + "24dp x 24dp to ensure proper display"); + } + } + try { mService.associate(request, new AssociationRequestCallbackProxy(handler, callback), mContext.getOpPackageName(), mContext.getUserId()); @@ -542,11 +551,13 @@ public final class CompanionDeviceManager { Objects.requireNonNull(executor, "Executor cannot be null"); Objects.requireNonNull(callback, "Callback cannot be null"); - final Icon deviceIcon = request.getDeviceIcon(); + if (Flags.associationDeviceIcon()) { + final Icon deviceIcon = request.getDeviceIcon(); - if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { - throw new IllegalArgumentException("The size of the device icon must be 24dp x 24dp to" - + "ensure proper display"); + if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { + throw new IllegalArgumentException("The size of the device icon must be " + + "24dp x 24dp to ensure proper display"); + } } try { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 0bb0027fb0c3..f71952849872 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -888,6 +888,22 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_ACTIVITY_RECOGNIZER = "android.intent.action.ACTIVITY_RECOGNIZER"; + /** @hide */ + public static void maybeMarkAsMissingCreatorToken(Object object) { + if (object instanceof Intent intent) { + maybeMarkAsMissingCreatorTokenInternal(intent); + } + } + + private static void maybeMarkAsMissingCreatorTokenInternal(Intent intent) { + boolean isForeign = (intent.mLocalFlags & LOCAL_FLAG_FROM_PARCEL) != 0; + boolean isWithoutTrustedCreatorToken = + (intent.mLocalFlags & Intent.LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT) == 0; + if (isForeign && isWithoutTrustedCreatorToken) { + intent.addExtendedFlags(EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN); + } + } + /** * Represents a shortcut/live folder icon resource. * @@ -7684,10 +7700,8 @@ public class Intent implements Parcelable, Cloneable { /** * This flag indicates the creator token of this intent has been verified. - * - * @hide */ - public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6; + private static final int LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT = 1 << 6; /** @hide */ @IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = { @@ -12243,6 +12257,30 @@ public class Intent implements Parcelable, Cloneable { } } + /** @hide */ + public void checkCreatorToken() { + if (mExtras == null) return; + if (mCreatorTokenInfo != null && mCreatorTokenInfo.mExtraIntentKeys != null) { + for (String key : mCreatorTokenInfo.mExtraIntentKeys) { + try { + Intent extraIntent = mExtras.getParcelable(key, Intent.class); + if (extraIntent == null) { + Log.w(TAG, "The key {" + key + + "} does not correspond to an intent in the bundle."); + continue; + } + extraIntent.mLocalFlags |= LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT; + } catch (Exception e) { + Log.e(TAG, "Failed to validate creator token. key: " + key + ".", e); + } + } + } + // mark the bundle as intent extras after calls to getParcelable. + // otherwise, the logic to mark missing token would run before + // mark trusted creator token present. + mExtras.setIsIntentExtra(); + } + public void writeToParcel(Parcel out, int flags) { out.writeString8(mAction); Uri.writeToParcel(out, mData); @@ -12730,6 +12768,7 @@ public class Intent implements Parcelable, Cloneable { } mLocalFlags |= localFlags; + checkCreatorToken(); // Special attribution fix-up logic for any BluetoothDevice extras // passed via Bluetooth intents diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 49ab15a40a8e..50121278f0e6 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Intent; import android.util.ArrayMap; import android.util.Log; import android.util.MathUtils; @@ -401,6 +402,9 @@ public class BaseBundle { synchronized (this) { object = unwrapLazyValueFromMapLocked(i, clazz, itemTypes); } + if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) { + Intent.maybeMarkAsMissingCreatorToken(object); + } } return (clazz != null) ? clazz.cast(object) : (T) object; } diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index ed4037c7d246..c18fb0c38b82 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -62,6 +62,12 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { @VisibleForTesting static final int FLAG_HAS_BINDERS = 1 << 12; + /** + * Indicates there may be intents with creator tokens contained in this bundle. When unparceled, + * they should be verified if tokens are missing or invalid. + */ + static final int FLAG_VERIFY_TOKENS_PRESENT = 1 << 13; + /** * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain @@ -274,6 +280,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { return orig; } + /** {@hide} */ + public void setIsIntentExtra() { + mFlags |= FLAG_VERIFY_TOKENS_PRESENT; + } + /** * Mark if this Bundle is okay to "defuse." That is, it's okay for system * processes to ignore any {@link BadParcelableException} encountered when diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java index 9f54d9fca24b..3adbd686cd2c 100644 --- a/core/java/android/util/NtpTrustedTime.java +++ b/core/java/android/util/NtpTrustedTime.java @@ -24,7 +24,7 @@ import android.content.Context; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.Network; -import android.net.NetworkCapabilities; +import android.net.NetworkInfo; import android.net.SntpClient; import android.os.Build; import android.os.SystemClock; @@ -687,16 +687,8 @@ public abstract class NtpTrustedTime implements TrustedTime { if (connectivityManager == null) { return false; } - final NetworkCapabilities networkCapabilities = - connectivityManager.getNetworkCapabilities(network); - if (networkCapabilities == null) { - if (LOGD) Log.d(TAG, "getNetwork: failed to get network capabilities"); - return false; - } - final boolean isConnectedToInternet = networkCapabilities.hasCapability( - NetworkCapabilities.NET_CAPABILITY_INTERNET) - && networkCapabilities.hasCapability( - NetworkCapabilities.NET_CAPABILITY_VALIDATED); + final NetworkInfo ni = connectivityManager.getNetworkInfo(network); + // This connectivity check is to avoid performing a DNS lookup for the time server on a // unconnected network. There are races to obtain time in Android when connectivity // changes, which means that forceRefresh() can be called by various components before @@ -706,8 +698,8 @@ public abstract class NtpTrustedTime implements TrustedTime { // A side effect of check is that tests that run a fake NTP server on the device itself // will only be able to use it if the active network is connected, even though loopback // addresses are actually reachable. - if (!isConnectedToInternet) { - if (LOGD) Log.d(TAG, "getNetwork: no internet connectivity"); + if (ni == null || !ni.isConnected()) { + if (LOGD) Log.d(TAG, "getNetwork: no connectivity"); return false; } return true; diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 18129530978f..45f6480b0b7f 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -308,6 +308,13 @@ flag { } flag { + name: "enable_restore_to_previous_size_from_desktop_immersive" + namespace: "lse_desktop_experience" + description: "Restores the window bounds to their previous size when exiting desktop immersive" + bug: "372318163" +} + +flag { name: "enable_display_focus_in_shell_transitions" namespace: "lse_desktop_experience" description: "Creates a shell transition when display focus switches." diff --git a/core/tests/coretests/src/android/app/OWNERS b/core/tests/coretests/src/android/app/OWNERS index 5636f9bc436c..6d14ccb18c53 100644 --- a/core/tests/coretests/src/android/app/OWNERS +++ b/core/tests/coretests/src/android/app/OWNERS @@ -14,3 +14,5 @@ per-file KeyguardManagerTest.java = file:/services/core/java/com/android/server/ # Files related to background activity launches per-file Background*Start* = file:/BAL_OWNERS +# Files related to caching +per-file PropertyInvalidatedCache* = file:/PERFORMANCE_OWNERS diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index b5ee1302fc1d..228647ae9094 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -19,6 +19,7 @@ package android.app; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; @@ -26,6 +27,7 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -90,11 +92,10 @@ public class PropertyInvalidatedCacheTests { } } - // Clear the test mode after every test, in case this process is used for other - // tests. This also resets the test property map. + // Ensure all test nonces are cleared after the test ends. @After public void tearDown() throws Exception { - PropertyInvalidatedCache.setTestMode(false); + PropertyInvalidatedCache.resetAfterTest(); } // This test is disabled pending an sepolicy change that allows any app to set the @@ -111,9 +112,6 @@ public class PropertyInvalidatedCacheTests { new PropertyInvalidatedCache<>(4, MODULE, API, "cache1", new ServerQuery(tester)); - PropertyInvalidatedCache.setTestMode(true); - testCache.testPropertyName(); - tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -223,22 +221,16 @@ public class PropertyInvalidatedCacheTests { TestCache(String module, String api) { this(module, api, new TestQuery()); - setTestMode(true); - testPropertyName(); } TestCache(String module, String api, TestQuery query) { super(4, module, api, api, query); mQuery = query; - setTestMode(true); - testPropertyName(); } public int getRecomputeCount() { return mQuery.getRecomputeCount(); } - - } @Test @@ -375,4 +367,18 @@ public class PropertyInvalidatedCacheTests { PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState"); assertEquals(n1, "cache_key.bluetooth.get_state"); } + + // It is illegal to continue to use a cache with a test key after calling setTestMode(false). + // This test verifies the code detects errors in calling setTestMode(). + @Test + public void testTestMode() { + TestCache cache = new TestCache(); + cache.invalidateCache(); + PropertyInvalidatedCache.resetAfterTest(); + try { + cache.invalidateCache(); + fail("expected an IllegalStateException"); + } catch (IllegalStateException expected) { + } + } } diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java index 64f77b309829..5852bee53778 100644 --- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -17,6 +17,7 @@ package android.os; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import android.multiuser.Flags; import android.platform.test.annotations.IgnoreUnderRavenwood; @@ -26,6 +27,7 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -92,17 +94,17 @@ public class IpcDataCacheTest { public Boolean apply(Integer x) { return mServer.query(x); } + @Override public boolean shouldBypassCache(Integer x) { return x % 13 == 0; } } - // Clear the test mode after every test, in case this process is used for other - // tests. This also resets the test property map. + // Ensure all test nonces are cleared after the test ends. @After public void tearDown() throws Exception { - IpcDataCache.setTestMode(false); + IpcDataCache.resetAfterTest(); } // This test is disabled pending an sepolicy change that allows any app to set the @@ -119,9 +121,6 @@ public class IpcDataCacheTest { new IpcDataCache<>(4, MODULE, API, "testCache1", new ServerQuery(tester)); - IpcDataCache.setTestMode(true); - testCache.testPropertyName(); - tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -165,9 +164,6 @@ public class IpcDataCacheTest { IpcDataCache<Integer, Boolean> testCache = new IpcDataCache<>(config, (x) -> tester.query(x, x % 10 == 9)); - IpcDataCache.setTestMode(true); - testCache.testPropertyName(); - tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -205,9 +201,6 @@ public class IpcDataCacheTest { IpcDataCache<Integer, Boolean> testCache = new IpcDataCache<>(config, (x) -> tester.query(x), (x) -> x % 9 == 0); - IpcDataCache.setTestMode(true); - testCache.testPropertyName(); - tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -313,8 +306,6 @@ public class IpcDataCacheTest { TestCache(String module, String api, TestQuery query) { super(4, module, api, "testCache7", query); mQuery = query; - setTestMode(true); - testPropertyName(); } TestCache(IpcDataCache.Config c) { @@ -324,8 +315,6 @@ public class IpcDataCacheTest { TestCache(IpcDataCache.Config c, TestQuery query) { super(c, query); mQuery = query; - setTestMode(true); - testPropertyName(); } int getRecomputeCount() { @@ -456,4 +445,18 @@ public class IpcDataCacheTest { TestCache ec = new TestCache(e); assertEquals(ec.isDisabled(), true); } + + // It is illegal to continue to use a cache with a test key after calling setTestMode(false). + // This test verifies the code detects errors in calling setTestMode(). + @Test + public void testTestMode() { + TestCache cache = new TestCache(); + cache.invalidateCache(); + IpcDataCache.resetAfterTest(); + try { + cache.invalidateCache(); + fail("expected an IllegalStateException"); + } catch (IllegalStateException expected) { + } + } } diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS index 1c0073417c9a..6149382d0800 100644 --- a/core/tests/coretests/src/android/os/OWNERS +++ b/core/tests/coretests/src/android/os/OWNERS @@ -9,3 +9,6 @@ per-file PowerManager*.java = file:/services/core/java/com/android/server/power/ # PerformanceHintManager per-file PerformanceHintManagerTest.java = file:/ADPF_OWNERS + +# Caching +per-file IpcDataCache* = file:/PERFORMANCE_OWNERS diff --git a/errorprone/OWNERS b/errorprone/OWNERS index bddbdb364683..aa8c126a32e1 100644 --- a/errorprone/OWNERS +++ b/errorprone/OWNERS @@ -1,2 +1 @@ -jsharkey@android.com -jsharkey@google.com +colefaust@google.com diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 35ef2393bb9b..37596182f05b 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -38,7 +38,7 @@ <Button android:layout_width="94dp" android:layout_height="60dp" - android:id="@+id/maximize_menu_maximize_button" + android:id="@+id/maximize_menu_size_toggle_button" style="?android:attr/buttonBarButtonStyle" android:stateListAnimator="@null" android:importantForAccessibility="yes" @@ -48,7 +48,7 @@ android:alpha="0"/> <TextView - android:id="@+id/maximize_menu_maximize_window_text" + android:id="@+id/maximize_menu_size_toggle_button_text" android:layout_width="94dp" android:layout_height="18dp" android:textSize="11sp" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 1f1565160965..df1e2248872b 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -492,8 +492,12 @@ <dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen> <!-- The radius of the inner fill of the maximize menu buttons. --> <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen> - <!-- The padding between the outline and fill of the maximize menu buttons. --> - <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen> + <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. --> + <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding">4dp</dimen> + <!-- The vertical padding between the outline and fill of the maximize menu restore button. --> + <dimen name="desktop_mode_maximize_menu_restore_button_fill_vertical_padding">13dp</dimen> + <!-- The horizontal padding between the outline and fill of the maximize menu restore button. --> + <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">21dp</dimen> <!-- The corner radius of the maximize menu. --> <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 621e2aacd673..afac9f6433a3 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -319,6 +319,8 @@ <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string> <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> + <!-- Accessibility text for the Maximize Menu's restore button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_restore_button_text">Restore</string> <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string> <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index b723337cc894..e078c7e14bd4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1185,7 +1185,13 @@ class DesktopTasksController( val options = createNewWindowOptions(callingTask) if (options.launchWindowingMode == WINDOWING_MODE_FREEFORM) { wct.startTask(requestedTaskId, options.toBundle()) - transitions.startTransition(TRANSIT_OPEN, wct, null) + val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( + callingTask.displayId, wct, requestedTaskId) + val runOnTransit = immersiveTransitionHandler + .exitImmersiveIfApplicable(wct, callingTask.displayId) + val transition = transitions.startTransition(TRANSIT_OPEN, wct, null) + addPendingMinimizeTransition(transition, taskToMinimize) + runOnTransit?.invoke(transition) } else { val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) splitScreenController.startTask(requestedTaskId, splitPosition, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index cbb08b804dfe..1453886e056b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -665,17 +665,6 @@ public class PipTransition extends PipTransitionController { return null; } - @Nullable - private TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { - return change; - } - } - return null; - } - private void startExitAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 9b815817d4d3..94b344fb575a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_PIP; @@ -346,6 +347,21 @@ public abstract class PipTransitionController implements Transitions.TransitionH return false; } + /** + * Gets a change amongst the transition targets that is in a different final orientation than + * the display, signalling a potential fixed rotation transition. + */ + @Nullable + public TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { + return change; + } + } + return null; + } + /** End the currently-playing PiP animation. */ public void end() { } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java index f40a87c39aef..fcd5c3baab5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java @@ -16,10 +16,14 @@ package com.android.wm.shell.pip2.animation; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; import android.view.Surface; @@ -60,6 +64,11 @@ public class PipEnterAnimator extends ValueAnimator private final PointF mInitScale = new PointF(); private final PointF mInitPos = new PointF(); private final Rect mInitCrop = new Rect(); + private final PointF mInitActivityScale = new PointF(); + private final PointF mInitActivityPos = new PointF(); + + Matrix mTransformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; public PipEnterAnimator(Context context, @NonNull SurfaceControl leash, @@ -109,6 +118,10 @@ public class PipEnterAnimator extends ValueAnimator @Override public void onAnimationEnd(@NonNull Animator animation) { + if (mFinishTransaction != null) { + onEnterAnimationUpdate(mInitScale, mInitPos, mInitCrop, + 1f /* fraction */, mFinishTransaction); + } if (mAnimationEndCallback != null) { mAnimationEndCallback.run(); } @@ -126,16 +139,24 @@ public class PipEnterAnimator extends ValueAnimator float fraction, SurfaceControl.Transaction tx) { float scaleX = 1 + (initScale.x - 1) * (1 - fraction); float scaleY = 1 + (initScale.y - 1) * (1 - fraction); - tx.setScale(mLeash, scaleX, scaleY); - float posX = initPos.x + (mEndBounds.left - initPos.x) * fraction; float posY = initPos.y + (mEndBounds.top - initPos.y) * fraction; - tx.setPosition(mLeash, posX, posY); + + int normalizedRotation = mRotation; + if (normalizedRotation == ROTATION_270) { + normalizedRotation = -ROTATION_90; + } + float degrees = -normalizedRotation * 90f * fraction; Rect endCrop = new Rect(mEndBounds); endCrop.offsetTo(0, 0); mRectEvaluator.evaluate(fraction, initCrop, endCrop); tx.setCrop(mLeash, mAnimatedRect); + + mTransformTensor.setScale(scaleX, scaleY); + mTransformTensor.postTranslate(posX, posY); + mTransformTensor.postRotate(degrees); + tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp); } // no-ops @@ -153,7 +174,22 @@ public class PipEnterAnimator extends ValueAnimator * calculated differently from generic transitions. * @param pipChange PiP change received as a transition target. */ - public void setEnterStartState(@NonNull TransitionInfo.Change pipChange) { + public void setEnterStartState(@NonNull TransitionInfo.Change pipChange, + @NonNull TransitionInfo.Change pipActivityChange) { + PipUtils.calcEndTransform(pipActivityChange, pipChange, mInitActivityScale, + mInitActivityPos); + if (mStartTransaction != null && pipActivityChange.getLeash() != null) { + mStartTransaction.setCrop(pipActivityChange.getLeash(), null); + mStartTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x, + mInitActivityScale.y); + mStartTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x, + mInitActivityPos.y); + mFinishTransaction.setCrop(pipActivityChange.getLeash(), null); + mFinishTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x, + mInitActivityScale.y); + mFinishTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x, + mInitActivityPos.y); + } PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java index 8fa5aa933929..a93ef12cb7fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java @@ -157,6 +157,7 @@ public class PipExpandAnimator extends ValueAnimator .shadow(tx, mLeash, false /* applyCornerRadius */); tx.apply(); } + private Rect getInsets(float fraction) { final Rect startInsets = mSourceRectHintInsets; final Rect endInsets = mZeroInsets; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 73be8db0ea8a..0427294579dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -292,23 +292,34 @@ public class PipController implements ConfigurationChangeListener, setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); if (!mPipTransitionState.isInPip()) { + // Skip the PiP-relevant updates if we aren't in a valid PiP state. + if (mPipTransitionState.isInFixedRotation()) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Fixed rotation flag shouldn't be set while in an invalid PiP state"); + } return; } mPipTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio()); - // Update the caches to reflect the new display layout in the movement bounds; - // temporarily update bounds to be at the top left for the movement bounds calculation. - Rect toBounds = new Rect(0, 0, - (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale), - (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale)); - mPipBoundsState.setBounds(toBounds); - mPipTouchHandler.updateMovementBounds(); - - // The policy is to keep PiP snap fraction invariant. - mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); - mPipBoundsState.setBounds(toBounds); - t.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + if (mPipTransitionState.isInFixedRotation()) { + // Do not change the bounds when in fixed rotation, but do update the movement bounds + // based on the current bounds state and potentially new display layout. + mPipTouchHandler.updateMovementBounds(); + mPipTransitionState.setInFixedRotation(false); + } else { + Rect toBounds = new Rect(0, 0, + (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale), + (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale)); + // Update the caches to reflect the new display layout in the movement bounds; + // temporarily update bounds to be at the top left for the movement bounds calculation. + mPipBoundsState.setBounds(toBounds); + mPipTouchHandler.updateMovementBounds(); + // The policy is to keep PiP snap fraction invariant. + mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); + mPipBoundsState.setBounds(toBounds); + } + t.setBounds(mPipTransitionState.mPipTaskToken, mPipBoundsState.getBounds()); } private void setDisplayLayout(DisplayLayout layout) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index a4a7973ef4bb..4d0432e1066e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -406,12 +406,9 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // We need to remove the callback even if the shelf is visible, in case it the delayed // callback hasn't been executed yet to avoid the wrong final state. mMainExecutor.removeCallbacks(mMoveOnShelVisibilityChanged); - if (shelfVisible) { - mMoveOnShelVisibilityChanged.run(); - } else { - // Postpone moving in response to hide of Launcher in case there's another change - mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY); - } + + // Postpone moving in response to hide of Launcher in case there's another change + mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index ac1567aba6e9..779e4ea51347 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -16,7 +16,9 @@ package com.android.wm.shell.pip2.phone; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; @@ -36,6 +38,7 @@ import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -388,8 +391,15 @@ public class PipTransition extends PipTransitionController implements return false; } - Rect startBounds = pipChange.getStartAbsBounds(); + // We expect the PiP activity as a separate change in a config-at-end transition. + TransitionInfo.Change pipActivityChange = getDeferConfigActivityChange(info, + pipChange.getTaskInfo().getToken()); + if (pipActivityChange == null) { + return false; + } + Rect endBounds = pipChange.getEndAbsBounds(); + Rect activityEndBounds = pipActivityChange.getEndAbsBounds(); SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); @@ -411,14 +421,63 @@ public class PipTransition extends PipTransitionController implements } } + final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED; + final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + + if (delta != ROTATION_0) { + mPipTransitionState.setInFixedRotation(true); + handleBoundsTypeFixedRotation(pipChange, pipActivityChange, fixedRotationChange); + } + PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, - startTransaction, finishTransaction, endBounds, sourceRectHint, Surface.ROTATION_0); - animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange)); + startTransaction, finishTransaction, endBounds, sourceRectHint, delta); + animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange, + pipActivityChange)); animator.setAnimationEndCallback(this::finishInner); animator.start(); return true; } + private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange, + TransitionInfo.Change pipActivityChange, + TransitionInfo.Change fixedRotationChange) { + final Rect endBounds = pipTaskChange.getEndAbsBounds(); + final Rect endActivityBounds = pipActivityChange.getEndAbsBounds(); + int startRotation = pipTaskChange.getStartRotation(); + int endRotation = fixedRotationChange.getEndFixedRotation(); + + // Cache the task to activity offset to potentially restore later. + Point activityEndOffset = new Point(endActivityBounds.left - endBounds.left, + endActivityBounds.top - endBounds.top); + + // If we are running a fixed rotation bounds enter PiP animation, + // then update the display layout rotation, and recalculate the end rotation bounds. + // Update the endBounds in place, so that the PiP change is up-to-date. + mPipDisplayLayoutState.rotateTo(endRotation); + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsAlgorithm.getEntryDestinationBounds()); + mPipBoundsAlgorithm.applySnapFraction(endBounds, snapFraction); + mPipBoundsState.setBounds(endBounds); + + // Display bounds were already updated to represent the final orientation, + // so we just need to readjust the origin, and perform rotation about (0, 0). + boolean isClockwise = (endRotation - startRotation) == -ROTATION_270; + Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); + int originTranslateX = isClockwise ? 0 : -displayBounds.width(); + int originTranslateY = isClockwise ? -displayBounds.height() : 0; + endBounds.offset(originTranslateX, originTranslateY); + + // Update the activity end bounds in place as well, as this is used for transform + // calculation later. + endActivityBounds.offsetTo(endBounds.left + activityEndOffset.x, + endBounds.top + activityEndOffset.y); + } + + private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -533,6 +592,19 @@ public class PipTransition extends PipTransitionController implements } @Nullable + private TransitionInfo.Change getDeferConfigActivityChange(TransitionInfo info, + @NonNull WindowContainerToken parent) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.hasFlags(TransitionInfo.FLAG_CONFIG_AT_END) + && change.getParent() != null && change.getParent().equals(parent)) { + return change; + } + } + return null; + } + + @Nullable private TransitionInfo.Change getChangeByToken(TransitionInfo info, WindowContainerToken token) { for (TransitionInfo.Change change : info.getChanges()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index a132796f4a84..ccdd66b5d1a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -155,6 +155,8 @@ public class PipTransitionState { @Nullable private Runnable mOnIdlePipTransitionStateRunnable; + private boolean mInFixedRotation = false; + /** * An interface to track state updates as we progress through PiP transitions. */ @@ -256,7 +258,7 @@ public class PipTransitionState { private void maybeRunOnIdlePipTransitionStateCallback() { if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) { - mOnIdlePipTransitionStateRunnable.run(); + mMainHandler.post(mOnIdlePipTransitionStateRunnable); mOnIdlePipTransitionStateRunnable = null; } } @@ -303,6 +305,23 @@ public class PipTransitionState { } /** + * @return true if either in swipe or button-nav fixed rotation. + */ + public boolean isInFixedRotation() { + return mInFixedRotation; + } + + /** + * Sets the fixed rotation flag. + */ + public void setInFixedRotation(boolean inFixedRotation) { + mInFixedRotation = inFixedRotation; + if (!inFixedRotation) { + maybeRunOnIdlePipTransitionStateCallback(); + } + } + + /** * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. */ public boolean isInSwipePipToHomeTransition() { @@ -351,7 +370,7 @@ public class PipTransitionState { public boolean isPipStateIdle() { // This needs to be a valid in-PiP state that isn't a transient state. - return mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS; + return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 0cb219ae4b81..3ae5a1afc7e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -62,6 +62,7 @@ import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.calculateMaximizeBounds import com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_LINEAR_IN import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer @@ -73,7 +74,8 @@ import java.util.function.Supplier /** * Menu that appears when user long clicks the maximize button. Gives the user the option to - * maximize the task or snap the task to the right or left half of the screen. + * maximize the task or restore previous task bounds from the maximized state and to snap the task + * to the right or left half of the screen. */ class MaximizeMenu( private val syncQueue: SyncTransactionQueue, @@ -176,6 +178,7 @@ class MaximizeMenu( "MaximizeMenu") maximizeMenuView = MaximizeMenuView( context = decorWindowContext, + sizeToggleDirection = getSizeToggleDirection(), menuHeight = menuHeight, menuPadding = menuPadding, ).also { menuView -> @@ -202,6 +205,18 @@ class MaximizeMenu( } } + private fun getSizeToggleDirection(): MaximizeMenuView.SizeToggleDirection { + val maximizeBounds = calculateMaximizeBounds( + displayController.getDisplayLayout(taskInfo.displayId)!!, + taskInfo + ) + val maximized = taskInfo.configuration.windowConfiguration.bounds.equals(maximizeBounds) + return if (maximized) + MaximizeMenuView.SizeToggleDirection.RESTORE + else + MaximizeMenuView.SizeToggleDirection.MAXIMIZE + } + private fun loadDimensionPixelSize(resourceId: Int): Int { return if (resourceId == Resources.ID_NULL) { 0 @@ -236,18 +251,19 @@ class MaximizeMenu( * resizing a Task. */ class MaximizeMenuView( - context: Context, + private val context: Context, + private val sizeToggleDirection: SizeToggleDirection, private val menuHeight: Int, - private val menuPadding: Int, + private val menuPadding: Int ) { val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup private val container = requireViewById(R.id.container) private val overlay = requireViewById(R.id.maximize_menu_overlay) - private val maximizeText = - requireViewById(R.id.maximize_menu_maximize_window_text) as TextView - private val maximizeButton = - requireViewById(R.id.maximize_menu_maximize_button) as Button + private val sizeToggleButtonText = + requireViewById(R.id.maximize_menu_size_toggle_button_text) as TextView + private val sizeToggleButton = + requireViewById(R.id.maximize_menu_size_toggle_button) as Button private val snapWindowText = requireViewById(R.id.maximize_menu_snap_window_text) as TextView private val snapRightButton = @@ -263,8 +279,6 @@ class MaximizeMenu( .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius) private val outlineStroke = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke) - private val fillPadding = context.resources - .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding) private val fillRadius = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) @@ -324,7 +338,7 @@ class MaximizeMenu( return@setOnHoverListener false } - maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() } + sizeToggleButton.setOnClickListener { onMaximizeClickListener?.invoke() } snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() } snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() } rootView.setOnTouchListener { _, event -> @@ -335,9 +349,17 @@ class MaximizeMenu( true } + val btnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) + R.string.desktop_mode_maximize_menu_restore_button_text + else + R.string.desktop_mode_maximize_menu_maximize_button_text + val btnText = context.resources.getText(btnTextId) + sizeToggleButton.contentDescription = btnText + sizeToggleButtonText.text = btnText + // To prevent aliasing. - maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } /** Bind the menu views to the new [RunningTaskInfo] data. */ @@ -348,8 +370,8 @@ class MaximizeMenu( rootView.background.setTint(style.backgroundColor) // Maximize option. - maximizeButton.background = style.maximizeOption.drawable - maximizeText.setTextColor(style.textColor) + sizeToggleButton.background = style.maximizeOption.drawable + sizeToggleButtonText.setTextColor(style.textColor) // Snap options. snapWindowText.setTextColor(style.textColor) @@ -358,8 +380,8 @@ class MaximizeMenu( /** Animate the opening of the menu */ fun animateOpenMenu(onEnd: () -> Unit) { - maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) menuAnimatorSet = AnimatorSet() menuAnimatorSet?.playTogether( ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) @@ -388,9 +410,9 @@ class MaximizeMenu( // Scale up the children of the maximize menu so that the menu // scale is cancelled out and only the background is scaled. val value = animatedValue as Float - maximizeButton.scaleY = value + sizeToggleButton.scaleY = value snapButtonsLayout.scaleY = value - maximizeText.scaleY = value + sizeToggleButtonText.scaleY = value snapWindowText.scaleY = value } }, @@ -409,9 +431,9 @@ class MaximizeMenu( startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS addUpdateListener { val value = animatedValue as Float - maximizeButton.alpha = value + sizeToggleButton.alpha = value snapButtonsLayout.alpha = value - maximizeText.alpha = value + sizeToggleButtonText.alpha = value snapWindowText.alpha = value } }, @@ -423,8 +445,8 @@ class MaximizeMenu( ) menuAnimatorSet?.addListener( onEnd = { - maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) onEnd.invoke() } ) @@ -433,8 +455,8 @@ class MaximizeMenu( /** Animate the closing of the menu */ fun animateCloseMenu(onEnd: (() -> Unit)) { - maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) cancelAnimation() menuAnimatorSet = AnimatorSet() menuAnimatorSet?.playTogether( @@ -464,9 +486,9 @@ class MaximizeMenu( // Scale up the children of the maximize menu so that the menu // scale is cancelled out and only the background is scaled. val value = animatedValue as Float - maximizeButton.scaleY = value + sizeToggleButton.scaleY = value snapButtonsLayout.scaleY = value - maximizeText.scaleY = value + sizeToggleButtonText.scaleY = value snapWindowText.scaleY = value } }, @@ -485,9 +507,9 @@ class MaximizeMenu( duration = ALPHA_ANIMATION_DURATION_MS addUpdateListener { val value = animatedValue as Float - maximizeButton.alpha = value + sizeToggleButton.alpha = value snapButtonsLayout.alpha = value - maximizeText.alpha = value + sizeToggleButtonText.alpha = value snapWindowText.alpha = value } }, @@ -498,8 +520,8 @@ class MaximizeMenu( ) menuAnimatorSet?.addListener( onEnd = { - maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) onEnd?.invoke() } ) @@ -509,8 +531,8 @@ class MaximizeMenu( /** Request that the accessibility service focus on the menu. */ fun requestAccessibilityFocus() { // Focus the first button in the menu by default. - maximizeButton.post { - maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + sizeToggleButton.post { + sizeToggleButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } } @@ -685,15 +707,31 @@ class MaximizeMenu( paint.color = strokeAndFillColor paint.style = Paint.Style.FILL }) + + val (horizontalFillPadding, verticalFillPadding) = + if (sizeToggleDirection == SizeToggleDirection.MAXIMIZE) { + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) to + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) + } else { + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_restore_button_fill_horizontal_padding) to + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_restore_button_fill_vertical_padding) + } + return LayerDrawable(layers.toTypedArray()).apply { when (numberOfLayers) { 3 -> { setLayerInset(1, outlineStroke) - setLayerInset(2, fillPadding) + setLayerInset(2, horizontalFillPadding, verticalFillPadding, + horizontalFillPadding, verticalFillPadding) } 4 -> { setLayerInset(intArrayOf(1, 2), outlineStroke) - setLayerInset(3, fillPadding) + setLayerInset(3, horizontalFillPadding, verticalFillPadding, + horizontalFillPadding, verticalFillPadding) } else -> error("Unexpected number of layers: $numberOfLayers") } @@ -737,6 +775,11 @@ class MaximizeMenu( enum class SnapToHalfSelection { NONE, LEFT, RIGHT } + + /** The possible selection states of the size toggle button in the maximize menu. */ + enum class SizeToggleDirection { + MAXIMIZE, RESTORE + } } companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index b3c10d64c3a3..6531e2a04ada 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -2984,6 +2984,58 @@ class DesktopTasksControllerTest : ShellTestCase() { .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeform_minimizesIfNeeded() { + setUpLandscapeDisplay() + val homeTask = setUpHomeTask() + val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val oldestTask = freeformTasks.first() + val newestTask = freeformTasks.last() + + runOpenInstance(newestTask, freeformTasks[1].taskId) + + val wct = getLatestWct(type = TRANSIT_OPEN) + // Home is moved to front of everything. + assertThat( + wct.hierarchyOps.any { hop -> + hop.container == homeTask.token.asBinder() && hop.toTop + } + ).isTrue() + // And the oldest task isn't moved in front of home, effectively minimizing it. + assertThat( + wct.hierarchyOps.none { hop -> + hop.container == oldestTask.token.asBinder() && hop.toTop + } + ).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeform_exitsImmersiveIfNeeded() { + setUpLandscapeDisplay() + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val immersiveTask = setUpFreeformTask() + taskRepository.setTaskInFullImmersiveState( + displayId = immersiveTask.displayId, + taskId = immersiveTask.taskId, + immersive = true + ) + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull())) + .thenReturn(transition) + whenever(mockDesktopFullImmersiveTransitionHandler + .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))).thenReturn(runOnStartTransit) + + runOpenInstance(immersiveTask, freeformTask.taskId) + + verify(mockDesktopFullImmersiveTransitionHandler) + .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId)) + runOnStartTransit.assertOnlyInvocation(transition) + } + private fun runOpenInstance( callingTask: RunningTaskInfo, requestedTaskId: Int diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index e9845c1d9f13..27817e9eb984 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -4,7 +4,6 @@ package com.google.android.appfunctions.sidecar { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 @@ -15,9 +14,7 @@ package com.google.android.appfunctions.sidecar { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index d660926575d1..43377d8eb91c 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -126,33 +126,6 @@ public final class AppFunctionManager { } /** - * Executes the app function. - * - * <p>Proxies request and response to the underlying {@link - * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and - * response in the appropriate type required by the function. - * - * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @Deprecated - public void executeAppFunction( - @NonNull ExecuteAppFunctionRequest sidecarRequest, - @NonNull @CallbackExecutor Executor executor, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - Objects.requireNonNull(sidecarRequest); - Objects.requireNonNull(executor); - Objects.requireNonNull(callback); - - executeAppFunction( - sidecarRequest, - executor, - new CancellationSignal(), - callback); - } - - /** * Returns a boolean through a callback, indicating whether the app function is enabled. * * <p>* This method can only check app functions owned by the caller, or those where the caller diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java index 2a168e871713..0dc87e45b7e3 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -119,76 +119,9 @@ public abstract class AppFunctionService extends Service { * @param callback A callback to report back the result. */ @MainThread - public void onExecuteFunction( + public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, cancellationSignal, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * @param request The function execution request. - * @param cancellationSignal A {@link CancellationSignal} to cancel the request. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * @param request The function execution request. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, - * Consumer)} instead. This method will be removed once usage references are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - Log.w( - "AppFunctionService", - "Calling deprecated default implementation of onExecuteFunction"); - } + @NonNull Consumer<ExecuteAppFunctionResponse> callback); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index eeb4853afadc..961962f6a010 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -85,8 +85,7 @@ interface IMediaRouterService { void updateScanningState(IMediaRouter2Manager manager, @JavaPassthrough(annotation="@android.media.MediaRouter2.ScanningState") int scanningState); void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId, - in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route, - in UserHandle transferInitiatorUserHandle, in String transferInitiatorPackageName); + in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route); void selectRouteWithManager(IMediaRouter2Manager manager, int requestId, String sessionId, in MediaRoute2Info route); void deselectRouteWithManager(IMediaRouter2Manager manager, int requestId, diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index b84990b54bd5..3499c438086d 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -2770,7 +2770,7 @@ public final class MediaRouter2 { || isSystemRouteReselection) { transferToRoute(sessionInfo, route, mClientUser, mClientPackageName); } else { - requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName); + requestCreateSession(sessionInfo, route); } } @@ -2826,10 +2826,7 @@ public final class MediaRouter2 { * @param route The {@link MediaRoute2Info route} to transfer to. */ private void requestCreateSession( - @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName) { + @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { if (TextUtils.isEmpty(oldSession.getClientPackageName())) { Log.w(TAG, "requestCreateSession: Can't create a session without package name."); this.onTransferFailed(oldSession, route); @@ -2840,12 +2837,7 @@ public final class MediaRouter2 { try { mMediaRouterService.requestCreateSessionWithManager( - mClient, - requestId, - oldSession, - route, - transferInitiatorUserHandle, - transferInitiatorPackageName); + mClient, requestId, oldSession, route); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 8fa0e49e8b96..7e1dccf2d366 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -524,8 +524,7 @@ public final class MediaRouter2Manager { transferToRoute( sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName); } else { - requestCreateSession(sessionInfo, route, transferInitiatorUserHandle, - transferInitiatorPackageName); + requestCreateSession(sessionInfo, route); } } @@ -914,9 +913,7 @@ public final class MediaRouter2Manager { } } - private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiationPackageName) { + private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route) { if (TextUtils.isEmpty(oldSession.getClientPackageName())) { Log.w(TAG, "requestCreateSession: Can't create a session without package name."); notifyTransferFailed(oldSession, route); @@ -927,8 +924,7 @@ public final class MediaRouter2Manager { try { mMediaRouterService.requestCreateSessionWithManager( - mClient, requestId, oldSession, route, transferInitiatorUserHandle, - transferInitiationPackageName); + mClient, requestId, oldSession, route); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml index 33519cba2940..dd7eac776583 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml @@ -26,12 +26,12 @@ android:outlineAmbientShadowColor="@android:color/transparent" android:outlineSpotShadowColor="@android:color/transparent" android:background="@android:color/transparent" - android:theme="@style/Theme.CollapsingToolbar.Settings"> + android:theme="@style/ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar"> <Toolbar android:id="@+id/action_bar" android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" + android:layout_height="?android:attr/actionBarSize" android:theme="?android:attr/actionBarTheme" android:transitionName="shared_element_view" app:layout_collapseMode="pin"/> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml index c20beaf9bf93..02f171cf0d9e 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml @@ -21,4 +21,15 @@ <item name="colorPrimary">@color/settingslib_primary_dark_device_default_settings</item> <item name="colorAccent">@color/settingslib_accent_device_default_dark</item> </style> -</resources>
\ No newline at end of file + + <!-- + ~ TODO(b/349675008): Remove this theme overlay once the platform bridge theme properly sets + ~ the MaterialComponents colors based on the platform theme. + --> + <style name="ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar"> + <item name="elevationOverlayEnabled">true</item> + <item name="elevationOverlayColor">?attr/colorPrimary</item> + <item name="colorPrimary">@color/settingslib_primary_dark_device_default_settings</item> + <item name="colorAccent">@color/settingslib_accent_device_default_dark</item> + </style> +</resources> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml index 9ecc297c6d36..403931764d7e 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml @@ -21,4 +21,15 @@ <item name="colorPrimary">@color/settingslib_primary_device_default_settings_light</item> <item name="colorAccent">@color/settingslib_accent_device_default_light</item> </style> -</resources>
\ No newline at end of file + + <!-- + ~ TODO(b/349675008): Remove this theme overlay once the platform bridge theme properly sets + ~ the MaterialComponents colors based on the platform theme. + --> + <style name="ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar"> + <item name="elevationOverlayEnabled">true</item> + <item name="elevationOverlayColor">?attr/colorPrimary</item> + <item name="colorPrimary">@color/settingslib_primary_device_default_settings_light</item> + <item name="colorAccent">@color/settingslib_accent_device_default_light</item> + </style> +</resources> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml new file mode 100644 index 000000000000..bcb9baf94706 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- See appcompat/appcompat/THEMES for the theme structure. --> +<resources> + <!-- + ~ Bridge theme overlay to simulate AppCompat themes based on a platform theme. + ~ Only non-widget attributes are included here since we should still use the platform widgets. + ~ Only public theme attributes (as in platform public-final.xml) can be referenced here since + ~ this is used in modules. + --> + <style name="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" parent=""> + <!-- START Base.V7.Theme.AppCompat --> + + <item name="colorBackgroundFloating">?android:colorBackgroundFloating</item> + + <item name="isLightTheme">?android:isLightTheme</item> + + <item name="selectableItemBackground">?android:selectableItemBackground</item> + <item name="selectableItemBackgroundBorderless">?android:selectableItemBackgroundBorderless</item> + <item name="homeAsUpIndicator">?android:homeAsUpIndicator</item> + + <item name="dividerVertical">?android:dividerVertical</item> + <item name="dividerHorizontal">?android:dividerHorizontal</item> + + <!-- List attributes --> + <item name="textAppearanceListItem">?android:textAppearanceListItem</item> + <item name="textAppearanceListItemSmall">?android:textAppearanceListItemSmall</item> + <item name="textAppearanceListItemSecondary">?android:textAppearanceListItemSecondary</item> + <item name="listPreferredItemHeight">?android:listPreferredItemHeight</item> + <item name="listPreferredItemHeightSmall">?android:listPreferredItemHeightSmall</item> + <item name="listPreferredItemHeightLarge">?android:listPreferredItemHeightLarge</item> + <item name="listPreferredItemPaddingLeft">?android:listPreferredItemPaddingLeft</item> + <item name="listPreferredItemPaddingRight">?android:listPreferredItemPaddingRight</item> + <item name="listPreferredItemPaddingStart">?android:listPreferredItemPaddingStart</item> + <item name="listPreferredItemPaddingEnd">?android:listPreferredItemPaddingEnd</item> + + <!-- Color palette --> + <item name="colorPrimaryDark">?android:colorPrimaryDark</item> + <item name="colorPrimary">?android:colorPrimary</item> + <item name="colorAccent">?android:colorAccent</item> + + <item name="colorControlNormal">?android:colorControlNormal</item> + <item name="colorControlActivated">?android:colorControlActivated</item> + <item name="colorControlHighlight">?android:colorControlHighlight</item> + <item name="colorButtonNormal">?android:colorButtonNormal</item> + + <item name="colorError">?android:colorError</item> + + <!-- END Base.V7.Theme.AppCompat --> + </style> + <style name="Base.ThemeOverlay.AppCompat.PlatformBridge" parent="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" /> + <style name="ThemeOverlay.AppCompat.PlatformBridge" parent="Base.ThemeOverlay.AppCompat.PlatformBridge" /> + + <!-- + ~ Bridge theme overlay to simulate MaterialComponents themes based on a platform theme. + --> + <style name="Base.V31.ThemeOverlay.MaterialComponents.PlatformBridge" parent="ThemeOverlay.AppCompat.PlatformBridge"> + <!-- START Base.V14.Theme.MaterialComponents.Bridge --> + <!-- + ~ This is copied as-is from the original bridge theme since it is guaranteed to not affect + ~ existing widgets. + --> + + <item name="isMaterialTheme">true</item> + + <item name="colorPrimaryVariant">@color/design_dark_default_color_primary_variant</item> + <item name="colorSecondary">@color/design_dark_default_color_secondary</item> + <item name="colorSecondaryVariant">@color/design_dark_default_color_secondary_variant</item> + <item name="colorSurface">@color/design_dark_default_color_surface</item> + <item name="colorPrimarySurface">?attr/colorSurface</item> + <item name="colorOnPrimary">@color/design_dark_default_color_on_primary</item> + <item name="colorOnSecondary">@color/design_dark_default_color_on_secondary</item> + <item name="colorOnBackground">@color/design_dark_default_color_on_background</item> + <item name="colorOnError">@color/design_dark_default_color_on_error</item> + <item name="colorOnSurface">@color/design_dark_default_color_on_surface</item> + <item name="colorOnPrimarySurface">?attr/colorOnSurface</item> + + <item name="scrimBackground">@color/mtrl_scrim_color</item> + <item name="popupMenuBackground">@drawable/mtrl_popupmenu_background_overlay</item> + + <item name="minTouchTargetSize">@dimen/mtrl_min_touch_target_size</item> + + <!-- MaterialComponents Widget styles --> + <item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item> + <item name="bottomAppBarStyle">@style/Widget.MaterialComponents.BottomAppBar</item> + <item name="chipStyle">@style/Widget.MaterialComponents.Chip.Action</item> + <item name="chipGroupStyle">@style/Widget.MaterialComponents.ChipGroup</item> + <item name="chipStandaloneStyle">@style/Widget.MaterialComponents.Chip.Entry</item> + <item name="circularProgressIndicatorStyle">@style/Widget.MaterialComponents.CircularProgressIndicator</item> + <item name="extendedFloatingActionButtonStyle">@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon</item> + <item name="linearProgressIndicatorStyle">@style/Widget.MaterialComponents.LinearProgressIndicator</item> + <item name="materialButtonStyle">@style/Widget.MaterialComponents.Button</item> + <item name="materialButtonOutlinedStyle">@style/Widget.MaterialComponents.Button.OutlinedButton</item> + <item name="materialButtonToggleGroupStyle">@style/Widget.MaterialComponents.MaterialButtonToggleGroup</item> + <item name="materialCardViewStyle">@style/Widget.MaterialComponents.CardView</item> + <item name="navigationRailStyle">@style/Widget.MaterialComponents.NavigationRailView</item> + <item name="sliderStyle">@style/Widget.MaterialComponents.Slider</item> + + <!-- Type styles --> + <item name="textAppearanceHeadline1">@style/TextAppearance.MaterialComponents.Headline1</item> + <item name="textAppearanceHeadline2">@style/TextAppearance.MaterialComponents.Headline2</item> + <item name="textAppearanceHeadline3">@style/TextAppearance.MaterialComponents.Headline3</item> + <item name="textAppearanceHeadline4">@style/TextAppearance.MaterialComponents.Headline4</item> + <item name="textAppearanceHeadline5">@style/TextAppearance.MaterialComponents.Headline5</item> + <item name="textAppearanceHeadline6">@style/TextAppearance.MaterialComponents.Headline6</item> + <item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Subtitle1</item> + <item name="textAppearanceSubtitle2">@style/TextAppearance.MaterialComponents.Subtitle2</item> + <item name="textAppearanceBody1">@style/TextAppearance.MaterialComponents.Body1</item> + <item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item> + <item name="textAppearanceCaption">@style/TextAppearance.MaterialComponents.Caption</item> + <item name="textAppearanceButton">@style/TextAppearance.MaterialComponents.Button</item> + <item name="textAppearanceOverline">@style/TextAppearance.MaterialComponents.Overline</item> + + <!-- Shape styles --> + <item name="shapeAppearanceSmallComponent"> + @style/ShapeAppearance.MaterialComponents.SmallComponent + </item> + <item name="shapeAppearanceMediumComponent"> + @style/ShapeAppearance.MaterialComponents.MediumComponent + </item> + <item name="shapeAppearanceLargeComponent"> + @style/ShapeAppearance.MaterialComponents.LargeComponent + </item> + + <!-- Motion --> + <item name="motionEasingStandard">@string/material_motion_easing_standard</item> + <item name="motionEasingEmphasized">@string/material_motion_easing_emphasized</item> + <item name="motionEasingDecelerated">@string/material_motion_easing_decelerated</item> + <item name="motionEasingAccelerated">@string/material_motion_easing_accelerated</item> + <item name="motionEasingLinear">@string/material_motion_easing_linear</item> + + <item name="motionDurationShort1">@integer/material_motion_duration_short_1</item> + <item name="motionDurationShort2">@integer/material_motion_duration_short_2</item> + <item name="motionDurationMedium1">@integer/material_motion_duration_medium_1</item> + <item name="motionDurationMedium2">@integer/material_motion_duration_medium_2</item> + <item name="motionDurationLong1">@integer/material_motion_duration_long_1</item> + <item name="motionDurationLong2">@integer/material_motion_duration_long_2</item> + + <item name="motionPath">@integer/material_motion_path</item> + + <!-- Elevation Overlays --> + <item name="elevationOverlayEnabled">true</item> + <item name="elevationOverlayColor">?attr/colorOnSurface</item> + + <!-- END Base.V14.Theme.MaterialComponents.Bridge --> + + <!-- START Base.V14.Theme.MaterialComponents --> + <!-- + ~ Only a subset of widget attributes being actually used are included here since there are + ~ too many of them and they need to be investigated on a case-by-case basis. + --> + + <!-- Framework, AppCompat, or Design Widget styles --> + <item name="appBarLayoutStyle">@style/Widget.MaterialComponents.AppBarLayout.Surface</item> + + <!-- END Base.V14.Theme.MaterialComponents --> + </style> + <style name="Base.ThemeOverlay.MaterialComponents.PlatformBridge" parent="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" /> + <style name="ThemeOverlay.MaterialComponents.PlatformBridge" parent="Base.ThemeOverlay.AppCompat.PlatformBridge" /> +</resources> diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index 9dc93484a638..4cf264253bf8 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -486,8 +486,8 @@ class TransitionAnimator( endState: State, windowBackgroundLayer: GradientDrawable, fadeWindowBackgroundLayer: Boolean = true, - useSpring: Boolean = false, drawHole: Boolean = false, + useSpring: Boolean = false, ): Animation { val transitionContainer = controller.transitionContainer val transitionContainerOverlay = transitionContainer.overlay diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt index 9444664885c8..71230f9cde12 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt @@ -17,8 +17,9 @@ package com.android.systemui.keyguard.ui.composable.modifier import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -41,15 +42,17 @@ fun Modifier.burnInAware( params: BurnInParameters, isClock: Boolean = false, ): Modifier { - val translationYState = remember { mutableStateOf(0F) } - viewModel.updateBurnInParams(params.copy(translationY = { translationYState.value })) + val cachedYTranslation = remember { mutableFloatStateOf(0f) } + LaunchedEffect(Unit) { + viewModel.updateBurnInParams(params.copy(translationY = { cachedYTranslation.floatValue })) + } val burnIn = viewModel.movement val translationX by burnIn.map { it.translationX.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f) val translationY by burnIn.map { it.translationY.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f) - translationYState.value = translationY + cachedYTranslation.floatValue = translationY val scaleViewModel by burnIn .map { BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt index 40c3f221e2df..29e9ba752b36 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt @@ -61,6 +61,26 @@ class BackGestureRecognizerTest : SysuiTestCase() { } @Test + fun triggersProgressRelativeToDistance() { + assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE, expectedProgress = 1f) + assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE, expectedProgress = 1f) + } + + private fun assertProgressWhileMovingFingers(deltaX: Float, expectedProgress: Float) { + assertStateAfterEvents( + events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) }, + expectedState = InProgress(progress = expectedProgress), + ) + } + + @Test + fun triggeredProgressIsNoBiggerThanOne() { + assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE * 2, expectedProgress = 1f) + } + + @Test fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { assertStateAfterEvents( events = ThreeFingerGesture.swipeLeft(distancePx = SWIPE_DISTANCE / 2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt index 8406d3b99bac..ff0cec5e06e9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt @@ -104,7 +104,8 @@ class EasterEggGestureTest : SysuiTestCase() { } private fun assertStateAfterTwoFingerGesture(gesturePath: List<Point>, wasTriggered: Boolean) { - val events = TwoFingerGesture.createEvents { gesturePath.forEach { (x, y) -> move(x, y) } } + val events = + TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { (x, y) -> move(x, y) } } assertStateAfterEvents(events = events, wasTriggered = wasTriggered) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt index 043b77577978..7d3ed92cecc6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt @@ -56,6 +56,27 @@ class HomeGestureRecognizerTest : SysuiTestCase() { } @Test + fun triggersProgressRelativeToDistance() { + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE, expectedProgress = 1f) + } + + private fun assertProgressWhileMovingFingers(deltaY: Float, expectedProgress: Float) { + assertStateAfterEvents( + events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaY = deltaY) }, + expectedState = InProgress(progress = expectedProgress), + ) + } + + @Test + fun triggeredProgressIsBetweenZeroAndOne() { + // going in the wrong direction + assertProgressWhileMovingFingers(deltaY = SWIPE_DISTANCE / 2, expectedProgress = 0f) + // going further than required distance + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE * 2, expectedProgress = 1f) + } + + @Test fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { assertStateAfterEvents( events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt index 7095a91a4e5d..c5c0d59ea48b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt @@ -77,11 +77,32 @@ class RecentAppsGestureRecognizerTest : SysuiTestCase() { fun triggersGestureProgressForThreeFingerGestureStarted() { assertStateAfterEvents( events = ThreeFingerGesture.startEvents(x = 0f, y = 0f), - expectedState = InProgress(), + expectedState = InProgress(progress = 0f), ) } @Test + fun triggersProgressRelativeToDistance() { + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE, expectedProgress = 1f) + } + + private fun assertProgressWhileMovingFingers(deltaY: Float, expectedProgress: Float) { + assertStateAfterEvents( + events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaY = deltaY) }, + expectedState = InProgress(progress = expectedProgress), + ) + } + + @Test + fun triggeredProgressIsBetweenZeroAndOne() { + // going in the wrong direction + assertProgressWhileMovingFingers(deltaY = SWIPE_DISTANCE / 2, expectedProgress = 0f) + // going further than required distance + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE * 2, expectedProgress = 1f) + } + + @Test fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { assertStateAfterEvents( events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt index 296d4dce8ce4..42fe1e5d6bec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt @@ -25,11 +25,23 @@ import android.view.MotionEvent.ACTION_UP import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.DEFAULT_X import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.DEFAULT_Y -/** - * Interface for gesture builders which support creating list of [MotionEvent] for common swipe - * gestures. For simple usage see swipe* methods or use [createEvents] for more specific scenarios. - */ -interface MultiFingerGesture { +/** Given gesture move events can build list of [MotionEvent]s included in that gesture */ +interface GestureEventsBuilder { + /** + * Creates full gesture including provided move events. This means returned events include DOWN, + * MOVE and UP. Note that move event's x and y is always relative to the starting one. + */ + fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> + + /** + * Creates partial gesture including provided move events. This means returned events include + * DOWN and MOVE. Note that move event's x and y is always relative to the starting one. + */ + fun eventsForGestureInProgress(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> +} + +/** Support creating list of [MotionEvent] for common swipe gestures. */ +interface MultiFingerGesture : GestureEventsBuilder { companion object { const val SWIPE_DISTANCE = 100f @@ -37,27 +49,41 @@ interface MultiFingerGesture { const val DEFAULT_Y = 500f } - fun swipeUp(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaY = -distancePx) } - - fun swipeDown(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaY = distancePx) } + fun swipeUp(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaY = -distancePx) + } - fun swipeRight(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaX = distancePx) } + fun swipeDown(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaY = distancePx) + } - fun swipeLeft(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaX = -distancePx) } + fun swipeRight(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaX = distancePx) + } - /** - * Creates gesture with provided move events. Note that move event's x and y is always relative - * to the starting one - */ - fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> + fun swipeLeft(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaX = -distancePx) + } } object ThreeFingerGesture : MultiFingerGesture { - override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> { - return touchpadGesture( + + private val moveEventsBuilder = MoveEventsBuilder(::threeFingerEvent) + + override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), + endEvents = { x, y -> endEvents(x, y) }, + ) + } + + override fun eventsForGestureInProgress( + moveEvents: MoveEventsBuilder.() -> Unit + ): List<MotionEvent> { + return buildGesture( startEvents = { x, y -> startEvents(x, y) }, - moveEvents = GestureBuilder(::threeFingerEvent).apply { moveEvents() }.events, - endEvents = { x, y -> endEvents(x, y) } + moveEvents = moveEventsBuilder.getEvents(moveEvents), ) } @@ -65,7 +91,7 @@ object ThreeFingerGesture : MultiFingerGesture { return listOf( threeFingerEvent(ACTION_DOWN, x, y), threeFingerEvent(ACTION_POINTER_DOWN, x, y), - threeFingerEvent(ACTION_POINTER_DOWN, x, y) + threeFingerEvent(ACTION_POINTER_DOWN, x, y), ) } @@ -73,32 +99,43 @@ object ThreeFingerGesture : MultiFingerGesture { return listOf( threeFingerEvent(ACTION_POINTER_UP, x, y), threeFingerEvent(ACTION_POINTER_UP, x, y), - threeFingerEvent(ACTION_UP, x, y) + threeFingerEvent(ACTION_UP, x, y), ) } private fun threeFingerEvent( action: Int, x: Float = DEFAULT_X, - y: Float = DEFAULT_Y + y: Float = DEFAULT_Y, ): MotionEvent { return touchpadEvent( action = action, x = x, y = y, classification = MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE, - axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f) + axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f), ) } } object FourFingerGesture : MultiFingerGesture { - override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> { - return touchpadGesture( + private val moveEventsBuilder = MoveEventsBuilder(::fourFingerEvent) + + override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), + endEvents = { x, y -> endEvents(x, y) }, + ) + } + + override fun eventsForGestureInProgress( + moveEvents: MoveEventsBuilder.() -> Unit + ): List<MotionEvent> { + return buildGesture( startEvents = { x, y -> startEvents(x, y) }, - moveEvents = GestureBuilder(::fourFingerEvent).apply { moveEvents() }.events, - endEvents = { x, y -> endEvents(x, y) } + moveEvents = moveEventsBuilder.getEvents(moveEvents), ) } @@ -107,7 +144,7 @@ object FourFingerGesture : MultiFingerGesture { fourFingerEvent(ACTION_DOWN, x, y), fourFingerEvent(ACTION_POINTER_DOWN, x, y), fourFingerEvent(ACTION_POINTER_DOWN, x, y), - fourFingerEvent(ACTION_POINTER_DOWN, x, y) + fourFingerEvent(ACTION_POINTER_DOWN, x, y), ) } @@ -116,61 +153,74 @@ object FourFingerGesture : MultiFingerGesture { fourFingerEvent(ACTION_POINTER_UP, x, y), fourFingerEvent(ACTION_POINTER_UP, x, y), fourFingerEvent(ACTION_POINTER_UP, x, y), - fourFingerEvent(ACTION_UP, x, y) + fourFingerEvent(ACTION_UP, x, y), ) } private fun fourFingerEvent( action: Int, x: Float = DEFAULT_X, - y: Float = DEFAULT_Y + y: Float = DEFAULT_Y, ): MotionEvent { return touchpadEvent( action = action, x = x, y = y, classification = MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE, - axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f) + axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f), ) } } object TwoFingerGesture : MultiFingerGesture { - override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> { - return touchpadGesture( - startEvents = { x, y -> listOf(twoFingerEvent(ACTION_DOWN, x, y)) }, - moveEvents = GestureBuilder(::twoFingerEvent).apply { moveEvents() }.events, - endEvents = { x, y -> listOf(twoFingerEvent(ACTION_UP, x, y)) } + private val moveEventsBuilder = MoveEventsBuilder(::twoFingerEvent) + + override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), + endEvents = { x, y -> listOf(twoFingerEvent(ACTION_UP, x, y)) }, + ) + } + + override fun eventsForGestureInProgress( + moveEvents: MoveEventsBuilder.() -> Unit + ): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), ) } + private fun startEvents(x: Float, y: Float) = listOf(twoFingerEvent(ACTION_DOWN, x, y)) + private fun twoFingerEvent( action: Int, x: Float = DEFAULT_X, - y: Float = DEFAULT_Y + y: Float = DEFAULT_Y, ): MotionEvent { return touchpadEvent( action = action, x = x, y = y, classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE, - axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f) + axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f), ) } } -private fun touchpadGesture( +private fun buildGesture( startEvents: (Float, Float) -> List<MotionEvent>, moveEvents: List<MotionEvent>, - endEvents: (Float, Float) -> List<MotionEvent> + endEvents: (Float, Float) -> List<MotionEvent> = { _, _ -> emptyList() }, ): List<MotionEvent> { val lastX = moveEvents.last().x val lastY = moveEvents.last().y return startEvents(DEFAULT_X, DEFAULT_Y) + moveEvents + endEvents(lastX, lastY) } -class GestureBuilder internal constructor(val eventBuilder: (Int, Float, Float) -> MotionEvent) { +class MoveEventsBuilder internal constructor(val eventBuilder: (Int, Float, Float) -> MotionEvent) { val events = mutableListOf<MotionEvent>() @@ -178,3 +228,11 @@ class GestureBuilder internal constructor(val eventBuilder: (Int, Float, Float) events.add(eventBuilder(ACTION_MOVE, DEFAULT_X + deltaX, DEFAULT_Y + deltaY)) } } + +private fun MoveEventsBuilder.getEvents( + moveEvents: MoveEventsBuilder.() -> Unit +): List<MotionEvent> { + events.clear() + this.moveEvents() + return events +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt index 13ebb42531b8..64136775b4eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt @@ -54,7 +54,28 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { ACTION_MOVE, ACTION_POINTER_UP, ACTION_POINTER_UP, - ACTION_UP + ACTION_UP, + ) + .inOrder() + } + + @Test + fun threeFingerGestureInProgressProducesCorrectEvents() { + val events = + ThreeFingerGesture.eventsForGestureInProgress { + move(deltaX = 10f) + move(deltaX = 20f) + } + + val actions = events.map { it.actionMasked } + assertWithMessage("Events have expected action type") + .that(actions) + .containsExactly( + ACTION_DOWN, + ACTION_POINTER_DOWN, + ACTION_POINTER_DOWN, + ACTION_MOVE, + ACTION_MOVE, ) .inOrder() } @@ -80,7 +101,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { ACTION_POINTER_UP, ACTION_POINTER_UP, ACTION_POINTER_UP, - ACTION_UP + ACTION_UP, ) .inOrder() } @@ -109,7 +130,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { @Test fun gestureBuilderProducesCorrectEventCoordinates() { val events = - ThreeFingerGesture.createEvents { + ThreeFingerGesture.eventsForFullGesture { move(deltaX = 50f) move(deltaX = 100f) } @@ -127,7 +148,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { // up events DEFAULT_X + 100f to DEFAULT_Y, DEFAULT_X + 100f to DEFAULT_Y, - DEFAULT_X + 100f to DEFAULT_Y + DEFAULT_X + 100f to DEFAULT_Y, ) .inOrder() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt index a867eb38b44c..c302b40fc4d7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -85,7 +85,7 @@ class TouchpadGestureHandlerTest : SysuiTestCase() { } private fun backGestureEvents(): List<MotionEvent> { - return ThreeFingerGesture.createEvents { + return ThreeFingerGesture.eventsForFullGesture { move(deltaX = SWIPE_DISTANCE / 4) move(deltaX = SWIPE_DISTANCE / 2) move(deltaX = SWIPE_DISTANCE) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt index 2c026c0bb5ce..7337e5af51a1 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt @@ -30,6 +30,7 @@ import com.android.internal.util.EmergencyAffordanceManager import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -74,6 +75,7 @@ constructor( private val metricsLogger: MetricsLogger, private val dozeLogger: DozeLogger, private val sceneInteractor: Lazy<SceneInteractor>, + private val bouncerHapticPlayer: BouncerHapticPlayer, ) { /** The bouncer action button. If `null`, the button should not be shown. */ val actionButton: Flow<BouncerActionButtonModel?> = @@ -111,6 +113,8 @@ constructor( BouncerActionButtonModel( label = applicationContext.getString(R.string.lockscreen_emergency_call), onClick = { + // TODO(b/373930432): haptics should be played at the UI layer -> refactor + bouncerHapticPlayer.playEmergencyButtonClickFeedback() prepareToPerformAction() dozeLogger.logEmergencyCall() startEmergencyDialerActivity() diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt index d6b92115c64b..837390730c7a 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt @@ -81,4 +81,11 @@ class BouncerHapticPlayer @Inject constructor(private val msdlPlayer: dagger.Laz /** Deliver MSDL feedback when a numpad key is pressed on the pin bouncer */ fun playNumpadKeyFeedback() = msdlPlayer.get().playToken(MSDLToken.KEYPRESS_STANDARD) + + /** Deliver MSDL feedback when clicking on the emergency button */ + fun playEmergencyButtonClickFeedback() { + if (isEnabled) { + msdlPlayer.get().playToken(MSDLToken.KEYPRESS_RETURN) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 801a0ce4b744..537b56bccae8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -39,7 +39,7 @@ constructor(@Main private val resources: Resources, val theme: Resources.Theme) val loadedIcon: Icon.Loaded = when (val dataIcon = data.icon) { is Icon.Resource -> { - if (iconRes != dataIcon.res) { + if (data.iconResId != dataIcon.res) { Log.wtf( "ModesTileMapper", "Icon.Resource.res & iconResId are not identical", diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 75c66f234bdc..90c005139c56 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -67,7 +67,7 @@ class DistanceBasedGestureRecognizerProvider( val distanceThresholdPx = resources.getDimensionPixelSize( com.android.internal.R.dimen.system_gestures_distance_threshold - ) + ) * 5 return remember(distanceThresholdPx) { recognizerFactory(distanceThresholdPx, gestureStateChangedCallback) } @@ -77,7 +77,8 @@ class DistanceBasedGestureRecognizerProvider( fun GestureState.toTutorialActionState(): TutorialActionState { return when (this) { NotStarted -> TutorialActionState.NotStarted - is InProgress -> TutorialActionState.InProgress(progress) + // progress is disabled for now as views are not ready to handle varying progress + is InProgress -> TutorialActionState.InProgress(0f) Finished -> TutorialActionState.Finished } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt index 56e97a357d67..80f800390852 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt @@ -16,10 +16,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.util.MathUtils import android.view.MotionEvent import kotlin.math.abs -/** Recognizes touchpad back gesture, that is three fingers swiping left or right */ +/** + * Recognizes touchpad back gesture, that is - using three fingers on touchpad - swiping left or + * right. + */ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer { private val distanceTracker = DistanceTracker() @@ -36,7 +40,7 @@ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback, gestureState, isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx }, - progress = { 0f }, + progress = { MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx)) }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt index 3db9d7ccc8f7..2b84a4c50613 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt @@ -16,9 +16,10 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.util.MathUtils import android.view.MotionEvent -/** Recognizes touchpad home gesture, that is three fingers swiping up */ +/** Recognizes touchpad home gesture, that is - using three fingers on touchpad - swiping up. */ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer { private val distanceTracker = DistanceTracker() @@ -35,7 +36,7 @@ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback, gestureState, isFinished = { -it.deltaY >= gestureDistanceThresholdPx }, - progress = { 0f }, + progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt index a194ad6a8016..69b7c5edd750 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt @@ -16,13 +16,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.util.MathUtils import android.view.MotionEvent import kotlin.math.abs /** - * Recognizes apps gesture completion. That is - using three fingers on touchpad - swipe up over - * some distance threshold and then slow down gesture before fingers are lifted. Implementation is - * based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker] + * Recognizes recent apps gesture, that is - using three fingers on touchpad - swipe up over some + * distance threshold and then slow down gesture before fingers are lifted. Implementation is based + * on [com.android.quickstep.util.TriggerSwipeUpTouchTracker] */ class RecentAppsGestureRecognizer( private val gestureDistanceThresholdPx: Int, @@ -49,7 +50,7 @@ class RecentAppsGestureRecognizer( -state.deltaY >= gestureDistanceThresholdPx && abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs }, - progress = { 0f }, + progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) }, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt index 8b427fbc5fb8..071acfa44650 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt @@ -156,7 +156,7 @@ class TransitionAnimatorTest(val useSpring: Boolean) : SysuiTestCase() { createEndState(transitionContainer), backgroundLayer, fadeWindowBackgroundLayer, - useSpring, + useSpring = useSpring, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt index 3087d01a2479..77ec83871016 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.internal.logging.metricsLogger import com.android.internal.util.emergencyAffordanceManager import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.data.repository.emergencyServicesRepository +import com.android.systemui.haptics.msdl.bouncerHapticPlayer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher @@ -52,5 +53,6 @@ val Kosmos.bouncerActionButtonInteractor by Fixture { metricsLogger = metricsLogger, dozeLogger = mock(), sceneInteractor = { sceneInteractor }, + bouncerHapticPlayer, ) } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 034127c0420e..7057cc361a1a 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -45,6 +45,16 @@ flag { } flag { + name: "clear_shortcuts_when_activity_updates_to_service" + namespace: "accessibility" + description: "When an a11y activity is updated to an a11y service, clears the associated shortcuts so that we don't skip the AccessibilityServiceWarning." + bug: "358092445" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "compute_window_changes_on_a11y_v2" namespace: "accessibility" description: "Computes accessibility window changes in accessibility instead of wm package." @@ -114,10 +124,13 @@ flag { } flag { - name: "enable_magnification_follows_mouse" + name: "enable_magnification_follows_mouse_bugfix" namespace: "accessibility" description: "Whether to enable mouse following for fullscreen magnification" bug: "354696546" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 1451dfaa7964..ec8908bc7c91 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2513,6 +2513,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState, List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos) { if (!parsedAccessibilityShortcutInfos.equals(userState.mInstalledShortcuts)) { + if (Flags.clearShortcutsWhenActivityUpdatesToService()) { + List<String> componentNames = userState.mInstalledShortcuts.stream() + .filter(a11yActivity -> + !parsedAccessibilityShortcutInfos.contains(a11yActivity)) + .map(a11yActivity -> a11yActivity.getComponentName().flattenToString()) + .toList(); + if (!componentNames.isEmpty()) { + enableShortcutsForTargets( + /* enable= */ false, UserShortcutType.ALL, + componentNames, userState.mUserId); + } + } + userState.mInstalledShortcuts.clear(); userState.mInstalledShortcuts.addAll(parsedAccessibilityShortcutInfos); userState.updateTileServiceMapForAccessibilityActivityLocked(); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index a19fdddea49c..963334b07ea6 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -345,7 +345,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @Override void handleMouseOrStylusEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - if (Flags.enableMagnificationFollowsMouse()) { + if (Flags.enableMagnificationFollowsMouseBugfix()) { if (mFullScreenMagnificationController.isActivated(mDisplayId)) { // TODO(b/354696546): Allow mouse/stylus to activate whichever display they are // over, rather than only interacting with the current display. @@ -1206,7 +1206,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - if (Flags.enableMagnificationFollowsMouse() + if (Flags.enableMagnificationFollowsMouseBugfix() && !event.isFromSource(SOURCE_TOUCHSCREEN)) { // Only touch events need to be cached and sent later. return; diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java index 446123f07f64..fa86ba39bb1a 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java @@ -146,7 +146,8 @@ public abstract class MagnificationGestureHandler extends BaseEventStreamTransfo } break; case SOURCE_MOUSE: case SOURCE_STYLUS: { - if (magnificationShortcutExists() && Flags.enableMagnificationFollowsMouse()) { + if (magnificationShortcutExists() + && Flags.enableMagnificationFollowsMouseBugfix()) { handleMouseOrStylusEvent(event, rawEvent, policyFlags); } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 28e57775523b..89f14b09d397 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -68,7 +68,10 @@ import com.android.server.SystemService.TargetUser; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.Collections; +import java.util.Map; import java.util.Objects; +import java.util.WeakHashMap; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; @@ -81,7 +84,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { private final ServiceHelper mInternalServiceHelper; private final ServiceConfig mServiceConfig; private final Context mContext; - private final Object mLock = new Object(); + private final Map<String, Object> mLocks = new WeakHashMap<>(); + public AppFunctionManagerServiceImpl(@NonNull Context context) { this( @@ -316,9 +320,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { THREAD_POOL_EXECUTOR.execute( () -> { try { - // TODO(357551503): Instead of holding a global lock, hold a per-package - // lock. - synchronized (mLock) { + synchronized (getLockForPackage(callingPackage)) { setAppFunctionEnabledInternalLocked( callingPackage, functionIdentifier, userHandle, enabledState); } @@ -346,7 +348,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { * process. */ @WorkerThread - @GuardedBy("mLock") + @GuardedBy("getLockForPackage(callingPackage)") private void setAppFunctionEnabledInternalLocked( @NonNull String callingPackage, @NonNull String functionIdentifier, @@ -541,6 +543,26 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { }); } } + /** + * Retrieves the lock object associated with the given package name. + * + * This method returns the lock object from the {@code mLocks} map if it exists. + * If no lock is found for the given package name, a new lock object is created, + * stored in the map, and returned. + */ + @VisibleForTesting + @NonNull + Object getLockForPackage(String callingPackage) { + // Synchronized the access to mLocks to prevent race condition. + synchronized (mLocks) { + // By using a WeakHashMap, we allow the garbage collector to reclaim memory by removing + // entries associated with unused callingPackage keys. Therefore, we remove the null + // values before getting/computing a new value. The goal is to not let the size of this + // map grow without an upper bound. + mLocks.values().removeAll(Collections.singleton(null)); // Remove null values + return mLocks.computeIfAbsent(callingPackage, k -> new Object()); + } + } private static class AppFunctionMetadataObserver implements ObserverCallback { @Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter; diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index 504137a29977..6a1e319b4039 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -46,6 +46,7 @@ import android.util.TimingsTraceLog; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.pm.RoSystemFeatures; import com.android.internal.util.XmlUtils; import com.android.modules.utils.build.UnboundedSdkLevel; import com.android.server.pm.permission.PermissionAllowlist; @@ -212,6 +213,30 @@ public class SystemConfig { } } + /** + * Utility class for testing interaction with compile-time defined system features. + * @hide + */ + @VisibleForTesting + public static class Injector { + /** Whether a system feature is defined as enabled and available at compile-time. */ + public boolean isReadOnlySystemEnabledFeature(String featureName, int version) { + return Boolean.TRUE.equals(RoSystemFeatures.maybeHasFeature(featureName, version)); + } + + /** Whether a system feature is defined as disabled and unavailable at compile-time. */ + public boolean isReadOnlySystemDisabledFeature(String featureName, int version) { + return Boolean.FALSE.equals(RoSystemFeatures.maybeHasFeature(featureName, version)); + } + + /** The full set of system features defined as compile-time enabled and available. */ + public ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures() { + return RoSystemFeatures.getReadOnlySystemEnabledFeatures(); + } + } + + private final Injector mInjector; + // These are the built-in shared libraries that were read from the // system configuration files. Keys are the library names; values are // the individual entries that contain information such as filename @@ -220,7 +245,7 @@ public class SystemConfig { // These are the features this devices supports that were read from the // system configuration files. - final ArrayMap<String, FeatureInfo> mAvailableFeatures = new ArrayMap<>(); + final ArrayMap<String, FeatureInfo> mAvailableFeatures; // These are the features which this device doesn't support; the OEM // partition uses these to opt-out of features from the system image. @@ -602,12 +627,26 @@ public class SystemConfig { public ArrayMap<String, Integer> getOemDefinedUids() { return mOemDefinedUids; } + /** * Only use for testing. Do NOT use in production code. * @param readPermissions false to create an empty SystemConfig; true to read the permissions. */ @VisibleForTesting public SystemConfig(boolean readPermissions) { + this(readPermissions, new Injector()); + } + + /** + * Only use for testing. Do NOT use in production code. + * @param readPermissions false to create an empty SystemConfig; true to read the permissions. + * @param injector Additional dependency injection for testing. + */ + @VisibleForTesting + public SystemConfig(boolean readPermissions, Injector injector) { + mInjector = injector; + mAvailableFeatures = mInjector.getReadOnlySystemEnabledFeatures(); + if (readPermissions) { Slog.w(TAG, "Constructing a test SystemConfig"); readAllPermissions(); @@ -617,6 +656,9 @@ public class SystemConfig { } SystemConfig() { + mInjector = new Injector(); + mAvailableFeatures = mInjector.getReadOnlySystemEnabledFeatures(); + TimingsTraceLog log = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER); log.traceBegin("readAllPermissions"); try { @@ -1775,6 +1817,10 @@ public class SystemConfig { } private void addFeature(String name, int version) { + if (mInjector.isReadOnlySystemDisabledFeature(name, version)) { + Slog.w(TAG, "Skipping feature addition for compile-time disabled feature: " + name); + return; + } FeatureInfo fi = mAvailableFeatures.get(name); if (fi == null) { fi = new FeatureInfo(); @@ -1787,6 +1833,10 @@ public class SystemConfig { } private void removeFeature(String name) { + if (mInjector.isReadOnlySystemEnabledFeature(name, /*version=*/0)) { + Slog.w(TAG, "Skipping feature removal for compile-time enabled feature: " + name); + return; + } if (mAvailableFeatures.remove(name) != null) { Slog.d(TAG, "Removed unavailable feature " + name); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 7f1d912d9a79..746c55f8fc9d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5542,7 +5542,6 @@ public class ActivityManagerService extends IActivityManager.Stub public int sendIntentSender(IApplicationThread caller, IIntentSender target, IBinder allowlistToken, int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - addCreatorToken(intent); if (target instanceof PendingIntentRecord) { final PendingIntentRecord originalRecord = (PendingIntentRecord) target; @@ -5584,19 +5583,23 @@ public class ActivityManagerService extends IActivityManager.Stub intent = new Intent(Intent.ACTION_MAIN); } try { + final int callingUid = Binder.getCallingUid(); + final String packageName; + final long token = Binder.clearCallingIdentity(); + try { + packageName = AppGlobals.getPackageManager().getNameForUid(callingUid); + } finally { + Binder.restoreCallingIdentity(token); + } + if (allowlistToken != null) { - final int callingUid = Binder.getCallingUid(); - final String packageName; - final long token = Binder.clearCallingIdentity(); - try { - packageName = AppGlobals.getPackageManager().getNameForUid(callingUid); - } finally { - Binder.restoreCallingIdentity(token); - } Slog.wtf(TAG, "Send a non-null allowlistToken to a non-PI target." + " Calling package: " + packageName + "; intent: " + intent + "; options: " + options); } + + addCreatorToken(intent, packageName); + target.send(code, intent, resolvedType, null, null, requiredPermission, options); } catch (RemoteException e) { @@ -12371,7 +12374,7 @@ public class ActivityManagerService extends IActivityManager.Stub continue; } endTime = SystemClock.currentThreadTimeMillis(); - hasSwapPss = mi.hasSwappedOutPss; + hasSwapPss = hasSwapPss || mi.hasSwappedOutPss; memtrackGraphics = mi.getOtherPrivate(Debug.MemoryInfo.OTHER_GRAPHICS); memtrackGl = mi.getOtherPrivate(Debug.MemoryInfo.OTHER_GL); } else { @@ -13049,7 +13052,7 @@ public class ActivityManagerService extends IActivityManager.Stub continue; } endTime = SystemClock.currentThreadTimeMillis(); - hasSwapPss = mi.hasSwappedOutPss; + hasSwapPss = hasSwapPss || mi.hasSwappedOutPss; } else { reportType = ProcessStats.ADD_PSS_EXTERNAL; startTime = SystemClock.currentThreadTimeMillis(); @@ -13628,7 +13631,7 @@ public class ActivityManagerService extends IActivityManager.Stub throws TransactionTooLargeException { enforceNotIsolatedCaller("startService"); enforceAllowedToStartOrBindServiceIfSdkSandbox(service); - addCreatorToken(service); + addCreatorToken(service, callingPackage); if (service != null) { // Refuse possible leaked file descriptors if (service.hasFileDescriptors()) { @@ -13890,7 +13893,7 @@ public class ActivityManagerService extends IActivityManager.Stub validateServiceInstanceName(instanceName); - addCreatorToken(service); + addCreatorToken(service, callingPackage); try { if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { final ComponentName cn = service.getComponent(); @@ -17174,7 +17177,7 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.v(TAG_SERVICE, "startServiceInPackage: " + service + " type=" + resolvedType); } - addCreatorToken(service); + addCreatorToken(service, callingPackage); final long origId = Binder.clearCallingIdentity(); ComponentName res; try { @@ -18002,8 +18005,8 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void addCreatorToken(Intent intent) { - ActivityManagerService.this.addCreatorToken(intent); + public void addCreatorToken(Intent intent, String creatorPackage) { + ActivityManagerService.this.addCreatorToken(intent, creatorPackage); } } @@ -19160,9 +19163,9 @@ public class ActivityManagerService extends IActivityManager.Stub private final Key mKeyFields; private final WeakReference<IntentCreatorToken> mRef; - public IntentCreatorToken(int creatorUid, Intent intent) { + public IntentCreatorToken(int creatorUid, String creatorPackage, Intent intent) { super(); - this.mKeyFields = new Key(creatorUid, intent); + this.mKeyFields = new Key(creatorUid, creatorPackage, intent); mRef = new WeakReference<>(this); } @@ -19170,7 +19173,10 @@ public class ActivityManagerService extends IActivityManager.Stub return mKeyFields.mCreatorUid; } - /** {@hide} */ + public String getCreatorPackage() { + return mKeyFields.mCreatorPackage; + } + public static boolean isValid(@NonNull Intent intent) { IBinder binder = intent.getCreatorToken(); IntentCreatorToken token = null; @@ -19178,7 +19184,8 @@ public class ActivityManagerService extends IActivityManager.Stub token = (IntentCreatorToken) binder; } return token != null && token.mKeyFields.equals( - new Key(token.mKeyFields.mCreatorUid, intent)); + new Key(token.mKeyFields.mCreatorUid, token.mKeyFields.mCreatorPackage, + intent)); } @Override @@ -19202,8 +19209,9 @@ public class ActivityManagerService extends IActivityManager.Stub } private static class Key { - private Key(int creatorUid, Intent intent) { + private Key(int creatorUid, String creatorPackage, Intent intent) { this.mCreatorUid = creatorUid; + this.mCreatorPackage = creatorPackage; this.mAction = intent.getAction(); this.mData = intent.getData(); this.mType = intent.getType(); @@ -19220,6 +19228,7 @@ public class ActivityManagerService extends IActivityManager.Stub } private final int mCreatorUid; + private final String mCreatorPackage; private final String mAction; private final Uri mData; private final String mType; @@ -19233,17 +19242,20 @@ public class ActivityManagerService extends IActivityManager.Stub if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Key key = (Key) o; - return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals( - mAction, key.mAction) && Objects.equals(mData, key.mData) - && Objects.equals(mType, key.mType) && Objects.equals(mPackage, - key.mPackage) && Objects.equals(mComponent, key.mComponent) + return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags + && Objects.equals(mCreatorPackage, key.mCreatorPackage) + && Objects.equals(mAction, key.mAction) + && Objects.equals(mData, key.mData) + && Objects.equals(mType, key.mType) + && Objects.equals(mPackage, key.mPackage) + && Objects.equals(mComponent, key.mComponent) && Objects.equals(mClipDataUris, key.mClipDataUris); } @Override public int hashCode() { - return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent, - mFlags, mClipDataUris); + return Objects.hash(mCreatorUid, mCreatorPackage, mAction, mData, mType, mPackage, + mComponent, mFlags, mClipDataUris); } } } @@ -19254,7 +19266,7 @@ public class ActivityManagerService extends IActivityManager.Stub * @param intent The given intent * @hide */ - public void addCreatorToken(@Nullable Intent intent) { + public void addCreatorToken(@Nullable Intent intent, String creatorPackage) { if (!preventIntentRedirect()) return; if (intent == null || intent.getExtraIntentKeys() == null) return; @@ -19267,7 +19279,7 @@ public class ActivityManagerService extends IActivityManager.Stub continue; } Slog.wtf(TAG, "A creator token is added to an intent."); - IBinder creatorToken = createIntentCreatorToken(extraIntent); + IBinder creatorToken = createIntentCreatorToken(extraIntent, creatorPackage); if (creatorToken != null) { extraIntent.setCreatorToken(creatorToken); } @@ -19280,15 +19292,15 @@ public class ActivityManagerService extends IActivityManager.Stub } } - private IBinder createIntentCreatorToken(Intent intent) { + private IBinder createIntentCreatorToken(Intent intent, String creatorPackage) { if (IntentCreatorToken.isValid(intent)) return null; int creatorUid = getCallingUid(); - IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, intent); + IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, creatorPackage, intent); IntentCreatorToken token; synchronized (sIntentCreatorTokenCache) { WeakReference<IntentCreatorToken> ref = sIntentCreatorTokenCache.get(key); if (ref == null || ref.get() == null) { - token = new IntentCreatorToken(creatorUid, intent); + token = new IntentCreatorToken(creatorUid, creatorPackage, intent); sIntentCreatorTokenCache.put(key, token.mRef); } else { token = ref.get(); diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 776a3455acc4..f60ee66cb236 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -3283,7 +3283,12 @@ public class OomAdjuster { baseCapabilities = PROCESS_CAPABILITY_ALL; // BFSL allowed break; case PROCESS_STATE_BOUND_TOP: - baseCapabilities = PROCESS_CAPABILITY_BFSL; + if (app.getActiveInstrumentation() != null) { + baseCapabilities = PROCESS_CAPABILITY_BFSL | + PROCESS_CAPABILITY_ALL_IMPLICIT; + } else { + baseCapabilities = PROCESS_CAPABILITY_BFSL; + } break; case PROCESS_STATE_FOREGROUND_SERVICE: if (app.getActiveInstrumentation() != null) { diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 6857b6bcde15..3fb06a75d79f 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -432,6 +432,14 @@ public final class PendingIntentRecord extends IIntentSender.Stub { } } + /** + * get package name of the PendingIntent sender. + * @return package name of the PendingIntent sender. + */ + public String getPackageName() { + return key.packageName; + } + @Deprecated public int sendInner(int code, Intent intent, String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index d1d5d4868c6f..27bc1cf3e631 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -677,24 +677,15 @@ class MediaRouter2ServiceImpl { @NonNull IMediaRouter2Manager manager, int requestId, @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName) { + @NonNull MediaRoute2Info route) { Objects.requireNonNull(manager, "manager must not be null"); Objects.requireNonNull(oldSession, "oldSession must not be null"); Objects.requireNonNull(route, "route must not be null"); - Objects.requireNonNull(transferInitiatorUserHandle); final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { - requestCreateSessionWithManagerLocked( - requestId, - manager, - oldSession, - route, - transferInitiatorUserHandle, - transferInitiatorPackageName); + requestCreateSessionWithManagerLocked(requestId, manager, oldSession, route); } } finally { Binder.restoreCallingIdentity(token); @@ -1738,9 +1729,7 @@ class MediaRouter2ServiceImpl { int requestId, @NonNull IMediaRouter2Manager manager, @NonNull RoutingSessionInfo oldSession, - @NonNull MediaRoute2Info route, - @NonNull UserHandle transferInitiatorUserHandle, - @NonNull String transferInitiatorPackageName) { + @NonNull MediaRoute2Info route) { ManagerRecord managerRecord = mAllManagerRecords.get(manager.asBinder()); if (managerRecord == null) { return; diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 363b8e4228b0..68e195d7f079 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -607,16 +607,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub IMediaRouter2Manager manager, int requestId, RoutingSessionInfo oldSession, - MediaRoute2Info route, - UserHandle transferInitiatorUserHandle, - String transferInitiatorPackageName) { - mService2.requestCreateSessionWithManager( - manager, - requestId, - oldSession, - route, - transferInitiatorUserHandle, - transferInitiatorPackageName); + MediaRoute2Info route) { + mService2.requestCreateSessionWithManager(manager, requestId, oldSession, route); } // Binder call diff --git a/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java b/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java index 8f603fc34b32..075a31f3b24c 100644 --- a/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java +++ b/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java @@ -191,8 +191,7 @@ public class RotationResolverManagerService extends SensorPrivacyManager.Sensors.CAMERA); if (mIsServiceEnabled && isCameraAvailable) { final RotationResolverManagerPerUserService service = - getServiceForUserLocked( - UserHandle.getCallingUserId()); + getServiceForUserLocked(UserHandle.USER_CURRENT); final RotationResolutionRequest request; if (packageName == null) { request = new RotationResolutionRequest(/* packageName */ "", diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 91a17a9e1c31..4589d26261dc 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -381,10 +381,12 @@ public final class TvInputManagerService extends SystemService { // service to populate the hardware list. serviceState = new ServiceState(component, userId); userState.serviceStateMap.put(component, serviceState); - updateServiceConnectionLocked(component, userId); } else { inputList.addAll(serviceState.hardwareInputMap.values()); } + if (serviceState.needInit) { + updateServiceConnectionLocked(component, userId); + } } else { try { TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build(); @@ -489,6 +491,27 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") + private void cleanUpHdmiDevices(int userId) { + if (DEBUG) { + Slog.d(TAG, "cleanUpHdmiDevices: user " + userId); + } + UserState userState = getOrCreateUserStateLocked(userId); + for (ServiceState serviceState : userState.serviceStateMap.values()) { + for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) { + try { + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceRemoved(device); + } else { + serviceState.hdmiDeviceRemovedBuffer.add(device); + } + } catch (RemoteException e) { + Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e); + } + } + } + } + private void startUser(int userId) { synchronized (mLock) { if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { @@ -500,9 +523,13 @@ public final class TvInputManagerService extends SystemService { if (userInfo.isProfile() && parentInfo != null && parentInfo.id == mCurrentUserId) { - // only the children of the current user can be started in background + int prevUserId = mCurrentUserId; mCurrentUserId = userId; - startProfileLocked(userId); + // only the children of the current user can be started in background + releaseSessionOfUserLocked(prevUserId); + cleanUpHdmiDevices(prevUserId); + unbindServiceOfUserLocked(prevUserId); + startProfileLocked(mCurrentUserId); } } } @@ -515,6 +542,7 @@ public final class TvInputManagerService extends SystemService { } releaseSessionOfUserLocked(userId); + cleanUpHdmiDevices(userId); unbindServiceOfUserLocked(userId); mRunningProfiles.remove(userId); } @@ -543,15 +571,19 @@ public final class TvInputManagerService extends SystemService { unbindServiceOfUserLocked(runningId); } mRunningProfiles.clear(); - releaseSessionOfUserLocked(mCurrentUserId); - unbindServiceOfUserLocked(mCurrentUserId); + int prevUserId = mCurrentUserId; mCurrentUserId = userId; - buildTvInputListLocked(userId, null); - buildTvContentRatingSystemListLocked(userId); + + releaseSessionOfUserLocked(prevUserId); + cleanUpHdmiDevices(prevUserId); + unbindServiceOfUserLocked(prevUserId); + + buildTvInputListLocked(mCurrentUserId, null); + buildTvContentRatingSystemListLocked(mCurrentUserId); mMessageHandler .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER, - getContentResolverForUser(userId)) + getContentResolverForUser(mCurrentUserId)) .sendToTarget(); } } @@ -590,6 +622,9 @@ public final class TvInputManagerService extends SystemService { @GuardedBy("mLock") private void unbindServiceOfUserLocked(int userId) { + if (DEBUG) { + Slog.d(TAG, "unbindServiceOfUserLocked: user " + userId); + } UserState userState = getUserStateLocked(userId); if (userState == null) { return; @@ -600,7 +635,12 @@ public final class TvInputManagerService extends SystemService { ServiceState serviceState = userState.serviceStateMap.get(component); if (serviceState != null && serviceState.sessionTokens.isEmpty()) { unbindService(serviceState); - it.remove(); + if (!serviceState.isHardware) { + it.remove(); + } else { + serviceState.hardwareInputMap.clear(); + serviceState.needInit = true; + } } } } @@ -774,7 +814,7 @@ public final class TvInputManagerService extends SystemService { boolean shouldBind; if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { shouldBind = !serviceState.sessionTokens.isEmpty() - || (serviceState.isHardware && serviceState.neverConnected); + || (serviceState.isHardware && serviceState.needInit); } else { // For a non-current user, // if sessionTokens is not empty, it contains recording sessions only @@ -3404,13 +3444,13 @@ public final class TvInputManagerService extends SystemService { private ServiceCallback callback; private boolean bound; private boolean reconnecting; - private boolean neverConnected; + private boolean needInit; private ServiceState(ComponentName component, int userId) { this.component = component; this.connection = new InputServiceConnection(component, userId); this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component); - this.neverConnected = true; + this.needInit = true; } } @@ -3618,11 +3658,9 @@ public final class TvInputManagerService extends SystemService { } ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent(); ServiceState serviceState = getServiceStateLocked(component, userId); - boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; - if (removed) { - buildTvInputListLocked(userId, null); - mTvInputHardwareManager.removeHardwareInput(inputId); - } + serviceState.hardwareInputMap.remove(inputId); + buildTvInputListLocked(userId, null); + mTvInputHardwareManager.removeHardwareInput(inputId); } private final class InputServiceConnection implements ServiceConnection { @@ -3648,7 +3686,7 @@ public final class TvInputManagerService extends SystemService { } ServiceState serviceState = userState.serviceStateMap.get(mComponent); serviceState.service = ITvInputService.Stub.asInterface(service); - serviceState.neverConnected = false; + serviceState.needInit = false; // Register a callback, if we need to. if (serviceState.isHardware && serviceState.callback == null) { @@ -3841,9 +3879,12 @@ public final class TvInputManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - Slog.d(TAG, "ServiceCallback: removeHardwareInput, inputId: " + inputId + - " by " + mComponent + ", userId: " + mUserId); - removeHardwareInputLocked(inputId, mUserId); + if (mUserId == mCurrentUserId) { + Slog.d(TAG, + "ServiceCallback: removeHardwareInput, inputId: " + inputId + " by " + + mComponent + ", userId: " + mUserId); + removeHardwareInputLocked(inputId, mUserId); + } } } finally { Binder.restoreCallingIdentity(identity); @@ -4578,6 +4619,11 @@ public final class TvInputManagerService extends SystemService { private final class HardwareListener implements TvInputHardwareManager.Listener { @Override public void onStateChanged(String inputId, int state) { + if (DEBUG) { + Slog.d(TAG, + "onStateChanged: inputId " + (inputId != null ? inputId : "null") + + ", state " + state); + } synchronized (mLock) { setStateLocked(inputId, state, mCurrentUserId); } @@ -4585,6 +4631,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHardwareDeviceAdded(TvInputHardwareInfo info) { + if (DEBUG) { + Slog.d(TAG, + "onHardwareDeviceAdded: TvInputHardwareInfo " + + (info != null ? info.toString() : "null")); + } synchronized (mLock) { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. @@ -4607,6 +4658,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHardwareDeviceRemoved(TvInputHardwareInfo info) { + if (DEBUG) { + Slog.d(TAG, + "onHardwareDeviceRemoved: TvInputHardwareInfo " + + (info != null ? info.toString() : "null")); + } synchronized (mLock) { String relatedInputId = mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId()); @@ -4634,6 +4690,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) { + if (DEBUG) { + Slog.d(TAG, + "onHdmiDeviceAdded: HdmiDeviceInfo " + + (deviceInfo != null ? deviceInfo.toString() : "null")); + } synchronized (mLock) { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. @@ -4656,6 +4717,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { + if (DEBUG) { + Slog.d(TAG, + "onHdmiDeviceRemoved: HdmiDeviceInfo " + + (deviceInfo != null ? deviceInfo.toString() : "null")); + } synchronized (mLock) { String relatedInputId = mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId()); @@ -4683,6 +4749,12 @@ public final class TvInputManagerService extends SystemService { @Override public void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo deviceInfo) { + if (DEBUG) { + Slog.d(TAG, + "onHdmiDeviceUpdated: inputId " + (inputId != null ? inputId : "null") + + ", deviceInfo: " + + (deviceInfo != null ? deviceInfo.toString() : "null")); + } synchronized (mLock) { Integer state; switch (deviceInfo.getDevicePowerStatus()) { diff --git a/services/core/java/com/android/server/uri/NeededUriGrants.java b/services/core/java/com/android/server/uri/NeededUriGrants.java index 8c8f55304fbb..2fe61e00c97e 100644 --- a/services/core/java/com/android/server/uri/NeededUriGrants.java +++ b/services/core/java/com/android/server/uri/NeededUriGrants.java @@ -17,10 +17,13 @@ package com.android.server.uri; import android.util.ArraySet; +import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.server.am.NeededUriGrantsProto; +import java.util.Objects; + /** List of {@link GrantUri} a process needs. */ public class NeededUriGrants { final String targetPkg; @@ -35,6 +38,20 @@ public class NeededUriGrants { this.uris = new ArraySet<>(); } + public void merge(NeededUriGrants other) { + if (other == null) return; + if (!Objects.equals(this.targetPkg, other.targetPkg) + || this.targetUid != other.targetUid || this.flags != other.flags) { + Slog.wtf("NeededUriGrants", + "The other NeededUriGrants does not share the same targetUid, targetPkg or " + + "flags. It cannot be merged into this NeededUriGrants. This " + + "NeededUriGrants: " + this.toStringWithoutUri() + + ". Other NeededUriGrants: " + other.toStringWithoutUri()); + } else { + this.uris.addAll(other.uris); + } + } + public void dumpDebug(ProtoOutputStream proto, long fieldId) { long token = proto.start(fieldId); proto.write(NeededUriGrantsProto.TARGET_PACKAGE, targetPkg); @@ -47,4 +64,12 @@ public class NeededUriGrants { } proto.end(token); } + + public String toStringWithoutUri() { + return "NeededUriGrants{" + + "targetPkg='" + targetPkg + '\'' + + ", targetUid=" + targetUid + + ", flags=" + flags + + '}'; + } } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index ae30fcde39e1..d119a08b0c85 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -442,8 +442,6 @@ class ActivityClientController extends IActivityClientController.Stub { throw new IllegalArgumentException("File descriptors passed in Intent"); } - mService.mAmInternal.addCreatorToken(resultData); - final ActivityRecord r; synchronized (mGlobalLock) { r = ActivityRecord.isInRootTaskLocked(token); @@ -502,6 +500,8 @@ class ActivityClientController extends IActivityClientController.Stub { r.app.setLastActivityFinishTimeIfNeeded(SystemClock.uptimeMillis()); } + mService.mAmInternal.addCreatorToken(resultData, r.packageName); + final long origId = Binder.clearCallingIdentity(); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "finishActivity"); try { diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 0580d4a5a4a3..c1f5a27b81e7 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.FactoryTest.FACTORY_TEST_LOW_LEVEL; +import static com.android.server.wm.ActivityStarter.Request.DEFAULT_INTENT_CREATOR_UID; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP; @@ -441,6 +442,17 @@ public class ActivityStartController { 0 /* startFlags */, null /* profilerInfo */, userId, filterCallingUid, callingPid); aInfo = mService.mAmInternal.getActivityInfoForUser(aInfo, userId); + int creatorUid = DEFAULT_INTENT_CREATOR_UID; + String creatorPackage = null; + if (ActivityManagerService.IntentCreatorToken.isValid(intent)) { + ActivityManagerService.IntentCreatorToken creatorToken = + (ActivityManagerService.IntentCreatorToken) intent.getCreatorToken(); + if (creatorToken.getCreatorUid() != filterCallingUid) { + creatorUid = creatorToken.getCreatorUid(); + creatorPackage = creatorToken.getCreatorPackage(); + } + // leave creatorUid as -1 if the intent creator is the same as the launcher + } if (aInfo != null) { try { @@ -454,6 +466,24 @@ public class ActivityStartController { return START_CANCELED; } + if (creatorUid != DEFAULT_INTENT_CREATOR_UID) { + try { + NeededUriGrants creatorIntentGrants = mSupervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, creatorUid, + aInfo.applicationInfo.packageName, + UserHandle.getUserId(aInfo.applicationInfo.uid)); + if (intentGrants == null) { + intentGrants = creatorIntentGrants; + } else { + intentGrants.merge(creatorIntentGrants); + } + } catch (SecurityException securityException) { + ActivityStarter.logForIntentRedirect( + "Creator URI Grant Caused Exception.", intent, creatorUid, + creatorPackage, filterCallingUid, callingPackage); + // TODO b/368559093 - rethrow the securityException. + } + } if ((aInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) { throw new IllegalArgumentException( @@ -477,6 +507,8 @@ public class ActivityStartController { .setCallingUid(callingUid) .setCallingPackage(callingPackage) .setCallingFeatureId(callingFeatureId) + .setIntentCreatorUid(creatorUid) + .setIntentCreatorPackage(creatorPackage) .setRealCallingPid(realCallingPid) .setRealCallingUid(realCallingUid) .setActivityOptions(checkedOptions) diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 5b5bb88cac98..5d3ae54f0934 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -132,6 +132,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.app.IVoiceInteractor; import com.android.internal.protolog.ProtoLog; +import com.android.server.am.ActivityManagerService.IntentCreatorToken; import com.android.server.am.PendingIntentRecord; import com.android.server.pm.InstantAppResolver; import com.android.server.pm.PackageArchiver; @@ -384,6 +385,7 @@ class ActivityStarter { private static final int DEFAULT_CALLING_PID = 0; static final int DEFAULT_REAL_CALLING_UID = -1; static final int DEFAULT_REAL_CALLING_PID = 0; + static final int DEFAULT_INTENT_CREATOR_UID = -1; IApplicationThread caller; Intent intent; @@ -404,6 +406,8 @@ class ActivityStarter { @Nullable String callingFeatureId; int realCallingPid = DEFAULT_REAL_CALLING_PID; int realCallingUid = DEFAULT_REAL_CALLING_UID; + int intentCreatorUid = DEFAULT_INTENT_CREATOR_UID; + String intentCreatorPackage; int startFlags; SafeActivityOptions activityOptions; boolean ignoreTargetSecurity; @@ -464,6 +468,8 @@ class ActivityStarter { callingPid = DEFAULT_CALLING_PID; callingUid = DEFAULT_CALLING_UID; callingPackage = null; + intentCreatorUid = DEFAULT_INTENT_CREATOR_UID; + intentCreatorPackage = null; callingFeatureId = null; realCallingPid = DEFAULT_REAL_CALLING_PID; realCallingUid = DEFAULT_REAL_CALLING_UID; @@ -556,12 +562,14 @@ class ActivityStarter { // "resolved" calling UID, where we try our best to identify the // actual caller that is starting this activity int resolvedCallingUid = callingUid; + String resolvedCallingPackage = callingPackage; if (caller != null) { synchronized (supervisor.mService.mGlobalLock) { final WindowProcessController callerApp = supervisor.mService .getProcessController(caller); if (callerApp != null) { resolvedCallingUid = callerApp.mInfo.uid; + resolvedCallingPackage = callerApp.mInfo.packageName; } } } @@ -597,7 +605,23 @@ class ActivityStarter { // Collect information about the target of the Intent. activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags, profilerInfo); - + // Check if the Intent was redirected + if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN) + != 0) { + ActivityStarter.logForIntentRedirect( + "Unparceled intent does not have a creator token set.", intent, + intentCreatorUid, + intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage); + // TODO b/368559093 - eventually ramp up to throw SecurityException + } + if (IntentCreatorToken.isValid(intent)) { + IntentCreatorToken creatorToken = (IntentCreatorToken) intent.getCreatorToken(); + if (creatorToken.getCreatorUid() != resolvedCallingUid) { + intentCreatorUid = creatorToken.getCreatorUid(); + intentCreatorPackage = creatorToken.getCreatorPackage(); + } + // leave intentCreatorUid as -1 if the intent creator is the same as the launcher + } // Carefully collect grants without holding lock if (activityInfo != null) { if (android.security.Flags.contentUriPermissionApis()) { @@ -607,11 +631,52 @@ class ActivityStarter { UserHandle.getUserId(activityInfo.applicationInfo.uid), activityInfo.requireContentUriPermissionFromCaller, /* requestHashCode */ this.hashCode()); + if (intentCreatorUid != DEFAULT_INTENT_CREATOR_UID) { + try { + NeededUriGrants creatorIntentGrants = supervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, intentCreatorUid, + activityInfo.applicationInfo.packageName, + UserHandle.getUserId(activityInfo.applicationInfo.uid), + activityInfo.requireContentUriPermissionFromCaller, + /* requestHashCode */ this.hashCode()); + if (intentGrants == null) { + intentGrants = creatorIntentGrants; + } else { + intentGrants.merge(creatorIntentGrants); + } + } catch (SecurityException securityException) { + ActivityStarter.logForIntentRedirect( + "Creator URI Grant Caused Exception.", intent, intentCreatorUid, + intentCreatorPackage, resolvedCallingUid, + resolvedCallingPackage); + // TODO b/368559093 - rethrow the securityException. + } + } } else { intentGrants = supervisor.mService.mUgmInternal .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid, activityInfo.applicationInfo.packageName, UserHandle.getUserId(activityInfo.applicationInfo.uid)); + if (intentCreatorUid != DEFAULT_INTENT_CREATOR_UID && intentGrants != null) { + try { + NeededUriGrants creatorIntentGrants = supervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, intentCreatorUid, + activityInfo.applicationInfo.packageName, + UserHandle.getUserId( + activityInfo.applicationInfo.uid)); + if (intentGrants == null) { + intentGrants = creatorIntentGrants; + } else { + intentGrants.merge(creatorIntentGrants); + } + } catch (SecurityException securityException) { + ActivityStarter.logForIntentRedirect( + "Creator URI Grant Caused Exception.", intent, intentCreatorUid, + intentCreatorPackage, resolvedCallingUid, + resolvedCallingPackage); + // TODO b/368559093 - rethrow the securityException. + } + } } } } @@ -978,7 +1043,9 @@ class ActivityStarter { int requestCode = request.requestCode; int callingPid = request.callingPid; int callingUid = request.callingUid; - String callingPackage = request.callingPackage; + int intentCreatorUid = request.intentCreatorUid; + String intentCreatorPackage = request.intentCreatorPackage; + String intentCallingPackage = request.callingPackage; String callingFeatureId = request.callingFeatureId; final int realCallingPid = request.realCallingPid; final int realCallingUid = request.realCallingUid; @@ -1063,7 +1130,7 @@ class ActivityStarter { // launched in the app flow to redirect to an activity picked by the user, where // we want the final activity to consider it to have been launched by the // previous app activity. - callingPackage = sourceRecord.launchedFromPackage; + intentCallingPackage = sourceRecord.launchedFromPackage; callingFeatureId = sourceRecord.launchedFromFeatureId; } } @@ -1085,7 +1152,7 @@ class ActivityStarter { if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) { err = packageArchiver .requestUnarchiveOnActivityStart( - intent, callingPackage, mRequest.userId, realCallingUid); + intent, intentCallingPackage, mRequest.userId, realCallingUid); } } } @@ -1144,7 +1211,7 @@ class ActivityStarter { boolean abort; try { abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho, - requestCode, callingPid, callingUid, callingPackage, callingFeatureId, + requestCode, callingPid, callingUid, intentCallingPackage, callingFeatureId, request.ignoreTargetSecurity, inTask != null, callerApp, resultRecord, resultRootTask); } catch (SecurityException e) { @@ -1172,7 +1239,47 @@ class ActivityStarter { abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid, callingPid, resolvedType, aInfo.applicationInfo); abort |= !mService.getPermissionPolicyInternal().checkStartActivity(intent, callingUid, - callingPackage); + intentCallingPackage); + + if (intentCreatorUid != Request.DEFAULT_INTENT_CREATOR_UID) { + try { + if (!mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho, + requestCode, 0, intentCreatorUid, intentCreatorPackage, "", + request.ignoreTargetSecurity, inTask != null, null, resultRecord, + resultRootTask)) { + logForIntentRedirect("Creator checkStartAnyActivityPermission Caused abortion.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - set abort to true. + // abort = true; + } + } catch (SecurityException e) { + logForIntentRedirect("Creator checkStartAnyActivityPermission Caused Exception.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - rethrow the exception. + //throw e; + } + if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid, 0, + resolvedType, aInfo.applicationInfo)) { + logForIntentRedirect("Creator IntentFirewall.checkStartActivity Caused abortion.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - set abort to true. + // abort = true; + } + + if (!mService.getPermissionPolicyInternal().checkStartActivity(intent, intentCreatorUid, + intentCreatorPackage)) { + logForIntentRedirect( + "Creator PermissionPolicyService.checkStartActivity Caused abortion.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - set abort to true. + // abort = true; + } + intent.removeCreatorTokenInfo(); + } // Merge the two options bundles, while realCallerOptions takes precedence. ActivityOptions checkedOptions = options != null @@ -1189,7 +1296,7 @@ class ActivityStarter { balController.checkBackgroundActivityStart( callingUid, callingPid, - callingPackage, + intentCallingPackage, realCallingUid, realCallingPid, callerApp, @@ -1210,7 +1317,7 @@ class ActivityStarter { if (request.allowPendingRemoteAnimationRegistryLookup) { checkedOptions = mService.getActivityStartController() .getPendingRemoteAnimationRegistry() - .overrideOptionsIfNeeded(callingPackage, checkedOptions); + .overrideOptionsIfNeeded(intentCallingPackage, checkedOptions); } if (mService.mController != null) { try { @@ -1226,7 +1333,8 @@ class ActivityStarter { final TaskDisplayArea suggestedLaunchDisplayArea = computeSuggestedLaunchDisplayArea(inTask, sourceRecord, checkedOptions); - mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage, + mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, + intentCallingPackage, callingFeatureId); if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment, callingPid, callingUid, checkedOptions, suggestedLaunchDisplayArea)) { @@ -1264,7 +1372,8 @@ class ActivityStarter { if (mService.getPackageManagerInternalLocked().isPermissionsReviewRequired( aInfo.packageName, userId)) { final IIntentSender target = mService.getIntentSenderLocked( - ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, callingFeatureId, + ActivityManager.INTENT_SENDER_ACTIVITY, intentCallingPackage, + callingFeatureId, callingUid, userId, null, null, 0, new Intent[]{intent}, new String[]{resolvedType}, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT, null); @@ -1327,7 +1436,8 @@ class ActivityStarter { // app [on install success]. if (rInfo != null && rInfo.auxiliaryInfo != null) { intent = createLaunchIntent(rInfo.auxiliaryInfo, request.ephemeralIntent, - callingPackage, callingFeatureId, verificationBundle, resolvedType, userId); + intentCallingPackage, callingFeatureId, verificationBundle, resolvedType, + userId); resolvedType = null; callingUid = realCallingUid; callingPid = realCallingPid; @@ -1350,7 +1460,7 @@ class ActivityStarter { .setCaller(callerApp) .setLaunchedFromPid(callingPid) .setLaunchedFromUid(callingUid) - .setLaunchedFromPackage(callingPackage) + .setLaunchedFromPackage(intentCallingPackage) .setLaunchedFromFeature(callingFeatureId) .setIntent(intent) .setResolvedType(resolvedType) @@ -3308,6 +3418,16 @@ class ActivityStarter { return this; } + ActivityStarter setIntentCreatorUid(int uid) { + mRequest.intentCreatorUid = uid; + return this; + } + + ActivityStarter setIntentCreatorPackage(String intentCreatorPackage) { + mRequest.intentCreatorPackage = intentCreatorPackage; + return this; + } + /** * Sets the pid of the caller who requested to launch the activity. * @@ -3467,4 +3587,19 @@ class ActivityStarter { pw.print(" mInTaskFragment="); pw.println(mInTaskFragment); } + + static void logForIntentRedirect(String message, Intent intent, int intentCreatorUid, + String intentCreatorPackage, int callingUid, String callingPackage) { + String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid, + intentCreatorPackage, callingUid, callingPackage); + Slog.wtf(TAG, msg); + } + + private static String getIntentRedirectPreventedLogMessage(String message, Intent intent, + int intentCreatorUid, String intentCreatorPackage, int callingUid, + String callingPackage) { + return "[IntentRedirect]" + message + " intentCreatorUid: " + intentCreatorUid + + "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid + + "; callingPackage: " + callingPackage + "; intent: " + intent; + } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 4db478a13c92..5339753624d8 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1228,7 +1228,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) { - mAmInternal.addCreatorToken(intent); + mAmInternal.addCreatorToken(intent, callingPackage); return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, profilerInfo, bOptions, UserHandle.getCallingUserId()); @@ -1243,7 +1243,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { enforceNotIsolatedCaller(reason); if (intents != null) { for (Intent intent : intents) { - mAmInternal.addCreatorToken(intent); + mAmInternal.addCreatorToken(intent, callingPackage); } } userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, reason); @@ -1275,7 +1275,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Nullable String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) { - mAmInternal.addCreatorToken(intent); + mAmInternal.addCreatorToken(intent, callingPackage); final SafeActivityOptions opts = SafeActivityOptions.fromBundle(bOptions); assertPackageMatchesCallingUid(callingPackage); @@ -1330,7 +1330,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } // Remove existing mismatch flag so it can be properly updated later fillInIntent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); - mAmInternal.addCreatorToken(fillInIntent); } if (!(target instanceof PendingIntentRecord)) { @@ -1339,6 +1338,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { PendingIntentRecord pir = (PendingIntentRecord) target; + if (fillInIntent != null) { + mAmInternal.addCreatorToken(fillInIntent, pir.getPackageName()); + } + synchronized (mGlobalLock) { // If this is coming from the currently resumed activity, it is // effectively saying that app switches are allowed at this point. @@ -1349,6 +1352,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAppSwitchesState = APP_SWITCH_ALLOW; } } + return pir.sendInner(caller, 0, fillInIntent, resolvedType, allowlistToken, null, null, resultTo, resultWho, requestCode, flagsMask, flagsValues, bOptions); } @@ -1361,8 +1365,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { throw new IllegalArgumentException("File descriptors passed in Intent"); } - mAmInternal.addCreatorToken(intent); - SafeActivityOptions options = SafeActivityOptions.fromBundle(bOptions); synchronized (mGlobalLock) { @@ -1376,6 +1378,9 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { SafeActivityOptions.abort(options); return false; } + + mAmInternal.addCreatorToken(intent, r.packageName); + intent = new Intent(intent); // Remove existing mismatch flag so it can be properly updated later intent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index 4d17ed24e734..eee4c86bc483 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -160,7 +160,7 @@ class AppTaskImpl extends IAppTask.Stub { Intent intent, String resolvedType, Bundle bOptions) { checkCallerOrSystemOrRoot(); mService.assertPackageMatchesCallingUid(callingPackage); - mService.mAmInternal.addCreatorToken(intent); + mService.mAmInternal.addCreatorToken(intent, callingPackage); int callingUser = UserHandle.getCallingUserId(); Task task; diff --git a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java index ff901af3defa..30df4c821134 100644 --- a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java +++ b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java @@ -96,6 +96,8 @@ class CallLogQueryHelper { } catch (SecurityException ex) { Slog.e(TAG, "Query call log failed: " + ex); return false; + } catch (Exception e) { + Slog.e(TAG, "Exception when querying call log.", e); } return hasResults; } diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java index 2505abf2d160..2bd9d87b0124 100644 --- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java @@ -151,9 +151,11 @@ class ContactsQueryHelper { found = true; } } catch (SQLiteException exception) { - Slog.w("SQLite exception when querying contacts.", exception); + Slog.w(TAG, "SQLite exception when querying contacts.", exception); } catch (IllegalArgumentException exception) { - Slog.w("Illegal Argument exception when querying contacts.", exception); + Slog.w(TAG, "Illegal Argument exception when querying contacts.", exception); + } catch (Exception exception) { + Slog.e(TAG, "Exception when querying contacts.", exception); } if (found && lookupKey != null && hasPhoneNumber) { return queryPhoneNumber(lookupKey); @@ -181,6 +183,8 @@ class ContactsQueryHelper { mPhoneNumber = cursor.getString(phoneNumIdx); } } + } catch (Exception exception) { + Slog.e(TAG, "Exception when querying contact phone number.", exception); } return true; } diff --git a/services/people/java/com/android/server/people/data/MmsQueryHelper.java b/services/people/java/com/android/server/people/data/MmsQueryHelper.java index 39dba9c73ba2..414a523fb186 100644 --- a/services/people/java/com/android/server/people/data/MmsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/MmsQueryHelper.java @@ -100,6 +100,8 @@ class MmsQueryHelper { } } } + } catch (Exception e) { + Slog.e(TAG, "Exception when querying MMS table.", e); } finally { Binder.defaultBlockingForCurrentThread(); } @@ -133,6 +135,8 @@ class MmsQueryHelper { address = cursor.getString(addrIndex); } } + } catch (Exception e) { + Slog.e(TAG, "Exception when querying MMS address table.", e); } if (!Mms.isPhoneNumber(address)) { return null; diff --git a/services/people/java/com/android/server/people/data/SmsQueryHelper.java b/services/people/java/com/android/server/people/data/SmsQueryHelper.java index a5eb3a581616..f8ff3abc8e4c 100644 --- a/services/people/java/com/android/server/people/data/SmsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/SmsQueryHelper.java @@ -98,6 +98,8 @@ class SmsQueryHelper { } } } + } catch (Exception e) { + Slog.e(TAG, "Exception when querying SMS table.", e); } finally { Binder.defaultBlockingForCurrentThread(); } diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp index c841643c6654..836f90b992d6 100644 --- a/services/tests/appfunctions/Android.bp +++ b/services/tests/appfunctions/Android.bp @@ -36,7 +36,9 @@ android_test { "androidx.test.core", "androidx.test.runner", "androidx.test.ext.truth", + "androidx.core_core-ktx", "kotlin-test", + "kotlinx_coroutines_test", "platform-test-annotations", "services.appfunctions", "servicestests-core-utils", diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt new file mode 100644 index 000000000000..a69e9025bfa0 --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appfunctions + +import android.app.appfunctions.flags.Flags +import android.content.Context +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) +class AppFunctionManagerServiceImplTest { + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private val serviceImpl = AppFunctionManagerServiceImpl(context) + + @Test + fun testGetLockForPackage_samePackage() { + val packageName = "com.example.app" + val lock1 = serviceImpl.getLockForPackage(packageName) + val lock2 = serviceImpl.getLockForPackage(packageName) + + // Assert that the same lock object is returned for the same package name + assertThat(lock1).isEqualTo(lock2) + } + + @Test + fun testGetLockForPackage_differentPackages() { + val packageName1 = "com.example.app1" + val packageName2 = "com.example.app2" + val lock1 = serviceImpl.getLockForPackage(packageName1) + val lock2 = serviceImpl.getLockForPackage(packageName2) + + // Assert that different lock objects are returned for different package names + assertThat(lock1).isNotEqualTo(lock2) + } + + @Ignore("Hard to deterministically trigger the garbage collector.") + @Test + fun testWeakReference_garbageCollected_differentLockAfterGC() = runTest { + // Create a large number of temporary objects to put pressure on the GC + val tempObjects = MutableList<Any?>(10000000) { Any() } + var callingPackage: String? = "com.example.app" + var lock1: Any? = serviceImpl.getLockForPackage(callingPackage) + callingPackage = null // Set the key to null + val lock1Hash = lock1.hashCode() + lock1 = null + + // Create memory pressure + repeat(3) { + for (i in 1..100) { + "a".repeat(10000) + } + System.gc() // Suggest garbage collection + System.runFinalization() + } + // Get the lock again - it should be a different object now + val lock2 = serviceImpl.getLockForPackage("com.example.app") + // Assert that the lock objects are different + assertThat(lock1Hash).isNotEqualTo(lock2.hashCode()) + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 6ccc03709b4f..2a825f35bf62 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -107,6 +107,7 @@ import android.os.IBinder; import android.os.IProgressListener; import android.os.Looper; import android.os.Message; +import android.os.Parcel; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -1309,12 +1310,13 @@ public class ActivityManagerServiceTest { intent.putExtra("EXTRA_INTENT0", extraIntent); intent.collectExtraIntentKeys(); - mAms.addCreatorToken(intent); + mAms.addCreatorToken(intent, TEST_PACKAGE); ActivityManagerService.IntentCreatorToken token = (ActivityManagerService.IntentCreatorToken) extraIntent.getCreatorToken(); assertThat(token).isNotNull(); assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid()); + assertThat(token.getCreatorPackage()).isEqualTo(TEST_PACKAGE); } @Test @@ -1330,7 +1332,7 @@ public class ActivityManagerServiceTest { fillinIntent.collectExtraIntentKeys(); intent.fillIn(fillinIntent, FILL_IN_ACTION); - mAms.addCreatorToken(fillinIntent); + mAms.addCreatorToken(fillinIntent, TEST_PACKAGE); fillinExtraIntent = intent.getParcelableExtra("FILLIN_EXTRA_INTENT0", Intent.class); @@ -1338,6 +1340,49 @@ public class ActivityManagerServiceTest { (ActivityManagerService.IntentCreatorToken) fillinExtraIntent.getCreatorToken(); assertThat(token).isNotNull(); assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid()); + assertThat(token.getCreatorPackage()).isEqualTo(TEST_PACKAGE); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_PREVENT_INTENT_REDIRECT) + public void testCheckCreatorToken() { + Intent intent = new Intent(); + Intent extraIntent = new Intent("EXTRA_INTENT_ACTION"); + intent.putExtra("EXTRA_INTENT", extraIntent); + + intent.collectExtraIntentKeys(); + + // mimic client hack and sneak in an extra intent without going thru collectExtraIntentKeys. + Intent extraIntent2 = new Intent("EXTRA_INTENT_ACTION2"); + intent.putExtra("EXTRA_INTENT2", extraIntent2); + + // mock parceling on the client side, unparcling on the system server side, then + // addCreatorToken on system server side. + final Parcel parcel = Parcel.obtain(); + intent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Intent newIntent = new Intent(); + newIntent.readFromParcel(parcel); + intent = newIntent; + mAms.addCreatorToken(intent, TEST_PACKAGE); + // entering the target app's process. + intent.checkCreatorToken(); + + Intent extraIntent3 = new Intent("EXTRA_INTENT_ACTION3"); + intent.putExtra("EXTRA_INTENT3", extraIntent3); + + extraIntent = intent.getParcelableExtra("EXTRA_INTENT", Intent.class); + extraIntent2 = intent.getParcelableExtra("EXTRA_INTENT2", Intent.class); + extraIntent3 = intent.getParcelableExtra("EXTRA_INTENT3", Intent.class); + + assertThat(extraIntent.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); + // sneaked in intent should have EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN set. + assertThat(extraIntent2.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isNotEqualTo(0); + // local created intent should not have EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN set. + assertThat(extraIntent3.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); } private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index b745e6a7d4a5..e5831b326de5 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -1417,7 +1417,7 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseMoveEventsDoNotMoveMagnifierViewport() { runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); } @@ -1471,55 +1471,55 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseHoverMoveEventsDoNotMoveMagnifierViewport() { runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusHoverMoveEventsDoNotMoveMagnifierViewport() { runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseHoverMoveEventsMoveMagnifierViewport() { runHoverMovesViewportTest(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusHoverMoveEventsMoveMagnifierViewport() { runHoverMovesViewportTest(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseDownEventsDoNotMoveMagnifierViewport() { runDownDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusDownEventsDoNotMoveMagnifierViewport() { runDownDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseUpEventsDoNotMoveMagnifierViewport() { runUpDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusUpEventsDoNotMoveMagnifierViewport() { runUpDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseMoveEventsMoveMagnifierViewport() { final EventCaptor eventCaptor = new EventCaptor(); mMgh.setNext(eventCaptor); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java index d80a1f056e94..45c157d1c1a0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java @@ -93,7 +93,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromMouse_handleMouseOrStylusEvent() { final MotionEvent mouseEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); mouseEvent.setSource(InputDevice.SOURCE_MOUSE); @@ -108,7 +108,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromStylus_handleMouseOrStylusEvent() { final MotionEvent stylusEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); stylusEvent.setSource(InputDevice.SOURCE_STYLUS); @@ -123,7 +123,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromMouse_handleMouseOrStylusEventNotCalled() { final MotionEvent mouseEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); mouseEvent.setSource(InputDevice.SOURCE_MOUSE); @@ -138,7 +138,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromStylus_handleMouseOrStylusEventNotCalled() { final MotionEvent stylusEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); stylusEvent.setSource(InputDevice.SOURCE_STYLUS); diff --git a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java index a54501029712..f45eddcf4480 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.CallLog.Calls; import android.test.mock.MockContentProvider; @@ -58,6 +59,7 @@ public final class CallLogQueryHelperTest { private MatrixCursor mCursor; private EventConsumer mEventConsumer; private CallLogQueryHelper mHelper; + private CallLogContentProvider mCallLogContentProvider; @Before public void setUp() { @@ -66,7 +68,8 @@ public final class CallLogQueryHelperTest { mCursor = new MatrixCursor(CALL_LOG_COLUMNS); MockContentResolver contentResolver = new MockContentResolver(); - contentResolver.addProvider(CALL_LOG_AUTHORITY, new CallLogContentProvider()); + mCallLogContentProvider = new CallLogContentProvider(); + contentResolver.addProvider(CALL_LOG_AUTHORITY, mCallLogContentProvider); when(mContext.getContentResolver()).thenReturn(contentResolver); mEventConsumer = new EventConsumer(); @@ -80,6 +83,12 @@ public final class CallLogQueryHelperTest { } @Test + public void testQueryWithSQLiteException() { + mCallLogContentProvider.setThrowSQLiteException(true); + assertFalse(mHelper.querySince(50L)); + } + + @Test public void testQueryIncomingCall() { mCursor.addRow(new Object[] { NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 30L, @@ -159,11 +168,20 @@ public final class CallLogQueryHelperTest { } private class CallLogContentProvider extends MockContentProvider { + private boolean mThrowSQLiteException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (mThrowSQLiteException) { + throw new SQLiteException(); + } + return mCursor; } + + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java index 16a02b678511..1daee39ce9de 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java @@ -99,6 +99,14 @@ public final class ContactsQueryHelperTest { } @Test + public void testQueryOtherException_returnsFalse() { + contentProvider.setThrowOtherException(true); + + Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY); + assertFalse(mHelper.query(contactUri.toString())); + } + + @Test public void testQueryIllegalArgumentException_returnsFalse() { contentProvider.setThrowIllegalArgumentException(true); @@ -152,6 +160,13 @@ public final class ContactsQueryHelperTest { } @Test + public void testQueryWithPhoneNumber_otherExceptionReturnsFalse() { + contentProvider.setThrowOtherException(true); + String contactUri = "tel:" + PHONE_NUMBER; + assertFalse(mHelper.query(contactUri)); + } + + @Test public void testQueryWithEmail() { mContactsLookupCursor.addRow(new Object[] { /* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 0 }); @@ -188,6 +203,7 @@ public final class ContactsQueryHelperTest { private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>(); private boolean mThrowSQLiteException = false; private boolean mThrowIllegalArgumentException = false; + private boolean mThrowOtherException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, @@ -198,6 +214,9 @@ public final class ContactsQueryHelperTest { if (mThrowIllegalArgumentException) { throw new IllegalArgumentException(); } + if (mThrowOtherException) { + throw new ArrayIndexOutOfBoundsException(); + } for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) { if (uri.isPathPrefixMatch(prefixUri)) { @@ -215,6 +234,10 @@ public final class ContactsQueryHelperTest { this.mThrowIllegalArgumentException = throwException; } + public void setThrowOtherException(boolean throwException) { + this.mThrowOtherException = throwException; + } + private void registerCursor(Uri uriPrefix, Cursor cursor) { mUriPrefixToCursorMap.put(uriPrefix, cursor); } diff --git a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java index 7730890e1486..9f4a43df6de8 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.Telephony.BaseMmsColumns; import android.provider.Telephony.Mms; @@ -63,6 +64,7 @@ public final class MmsQueryHelperTest { private final List<MatrixCursor> mAddrCursors = new ArrayList<>(); private EventConsumer mEventConsumer; private MmsQueryHelper mHelper; + private MmsContentProvider mMmsContentProvider; @Before public void setUp() { @@ -73,7 +75,8 @@ public final class MmsQueryHelperTest { mAddrCursors.add(new MatrixCursor(ADDR_COLUMNS)); MockContentResolver contentResolver = new MockContentResolver(); - contentResolver.addProvider(MMS_AUTHORITY, new MmsContentProvider()); + mMmsContentProvider = new MmsContentProvider(); + contentResolver.addProvider(MMS_AUTHORITY, mMmsContentProvider); when(mContext.getContentResolver()).thenReturn(contentResolver); mEventConsumer = new EventConsumer(); @@ -87,6 +90,12 @@ public final class MmsQueryHelperTest { } @Test + public void testQueryWithSQLiteException() { + mMmsContentProvider.setThrowSQLiteException(true); + assertFalse(mHelper.querySince(50_000L)); + } + + @Test public void testQueryIncomingMessage() { mMmsCursor.addRow(new Object[] { /* id= */ 0, /* date= */ 100L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_INBOX }); @@ -159,10 +168,15 @@ public final class MmsQueryHelperTest { } private class MmsContentProvider extends MockContentProvider { + private boolean mThrowSQLiteException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (mThrowSQLiteException) { + throw new SQLiteException(); + } + List<String> segments = uri.getPathSegments(); if (segments.size() == 2 && "addr".equals(segments.get(1))) { int messageId = Integer.valueOf(segments.get(0)); @@ -170,5 +184,9 @@ public final class MmsQueryHelperTest { } return mMmsCursor; } + + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java index 5cb8cb4fe9f1..09a0dff77eb0 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.Telephony.Sms; import android.provider.Telephony.TextBasedSmsColumns; @@ -59,6 +60,7 @@ public final class SmsQueryHelperTest { private MatrixCursor mSmsCursor; private EventConsumer mEventConsumer; private SmsQueryHelper mHelper; + private SmsContentProvider mSmsContentProvider; @Before public void setUp() { @@ -67,7 +69,8 @@ public final class SmsQueryHelperTest { mSmsCursor = new MatrixCursor(SMS_COLUMNS); MockContentResolver contentResolver = new MockContentResolver(); - contentResolver.addProvider(SMS_AUTHORITY, new SmsContentProvider()); + mSmsContentProvider = new SmsContentProvider(); + contentResolver.addProvider(SMS_AUTHORITY, mSmsContentProvider); when(mContext.getContentResolver()).thenReturn(contentResolver); mEventConsumer = new EventConsumer(); @@ -130,6 +133,12 @@ public final class SmsQueryHelperTest { assertEquals(110L, events.get(1).getTimestamp()); } + @Test + public void testQueryWithSQLiteException() { + mSmsContentProvider.setThrowSQLiteException(true); + assertFalse(mHelper.querySince(50L)); + } + private class EventConsumer implements BiConsumer<String, Event> { private final Map<String, List<Event>> mEventMap = new ArrayMap<>(); @@ -141,11 +150,19 @@ public final class SmsQueryHelperTest { } private class SmsContentProvider extends MockContentProvider { + private boolean mThrowSQLiteException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (mThrowSQLiteException) { + throw new SQLiteException(); + } return mSmsCursor; } + + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } } } diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigReadOnlyFeaturesTest.kt b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigReadOnlyFeaturesTest.kt new file mode 100644 index 000000000000..22d894a5a739 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigReadOnlyFeaturesTest.kt @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.systemconfig + +import android.content.Context +import android.content.pm.FeatureInfo +import android.util.ArrayMap +import android.util.Xml + +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.SystemConfig +import com.google.common.truth.Truth.assertThat +import org.junit.runner.RunWith +import org.junit.Rule + +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class SystemConfigReadOnlyFeaturesTest { + + companion object { + private const val FEATURE_ONE = "feature.test.1" + private const val FEATURE_TWO = "feature.test.2" + private const val FEATURE_RUNTIME_AVAILABLE_TEMPLATE = + """ + <permissions> + <feature name="%s" /> + </permissions> + """ + private const val FEATURE_RUNTIME_DISABLED_TEMPLATE = + """ + <permissions> + <Disabled-feature name="%s" /> + </permissions> + """ + + fun featureInfo(featureName: String) = FeatureInfo().apply { name = featureName } + } + + private val context: Context = InstrumentationRegistry.getInstrumentation().context + + @get:Rule + val tempFolder = TemporaryFolder(context.filesDir) + + private val injector = TestInjector() + + private var uniqueCounter = 0 + + @Test + fun empty() { + assertFeatures().isEmpty() + } + + @Test + fun readOnlyEnabled() { + addReadOnlyEnabledFeature(FEATURE_ONE) + addReadOnlyEnabledFeature(FEATURE_TWO) + + assertFeatures().containsAtLeast(FEATURE_ONE, FEATURE_TWO) + } + + @Test + fun readOnlyAndRuntimeEnabled() { + addReadOnlyEnabledFeature(FEATURE_ONE) + addRuntimeEnabledFeature(FEATURE_TWO) + + // No issues with matching availability. + assertFeatures().containsAtLeast(FEATURE_ONE, FEATURE_TWO) + } + + @Test + fun readOnlyEnabledRuntimeDisabled() { + addReadOnlyEnabledFeature(FEATURE_ONE) + addRuntimeDisabledFeature(FEATURE_ONE) + + // Read-only feature availability should take precedence. + assertFeatures().contains(FEATURE_ONE) + } + + @Test + fun readOnlyDisabled() { + addReadOnlyDisabledFeature(FEATURE_ONE) + + assertFeatures().doesNotContain(FEATURE_ONE) + } + + @Test + fun readOnlyAndRuntimeDisabled() { + addReadOnlyDisabledFeature(FEATURE_ONE) + addRuntimeDisabledFeature(FEATURE_ONE) + + // No issues with matching (un)availability. + assertFeatures().doesNotContain(FEATURE_ONE) + } + + @Test + fun readOnlyDisabledRuntimeEnabled() { + addReadOnlyDisabledFeature(FEATURE_ONE) + addRuntimeEnabledFeature(FEATURE_ONE) + addRuntimeEnabledFeature(FEATURE_TWO) + + // Read-only feature (un)availability should take precedence. + assertFeatures().doesNotContain(FEATURE_ONE) + assertFeatures().contains(FEATURE_TWO) + } + + fun addReadOnlyEnabledFeature(featureName: String) { + injector.readOnlyEnabledFeatures[featureName] = featureInfo(featureName) + } + + fun addReadOnlyDisabledFeature(featureName: String) { + injector.readOnlyDisabledFeatures.add(featureName) + } + + fun addRuntimeEnabledFeature(featureName: String) { + FEATURE_RUNTIME_AVAILABLE_TEMPLATE.format(featureName).write() + } + + fun addRuntimeDisabledFeature(featureName: String) { + FEATURE_RUNTIME_DISABLED_TEMPLATE.format(featureName).write() + } + + private fun String.write() = tempFolder.root.resolve("${uniqueCounter++}.xml") + .writeText(this.trimIndent()) + + private fun assertFeatures() = assertThat(availableFeatures().keys) + + private fun availableFeatures() = SystemConfig(false, injector).apply { + val parser = Xml.newPullParser() + readPermissions(parser, tempFolder.root, /*Grant all permission flags*/ 0.inv()) + }.let { it.availableFeatures } + + internal class TestInjector() : SystemConfig.Injector() { + val readOnlyEnabledFeatures = ArrayMap<String, FeatureInfo>() + val readOnlyDisabledFeatures = mutableSetOf<String>() + + override fun isReadOnlySystemEnabledFeature(featureName: String, version: Int): Boolean { + return readOnlyEnabledFeatures.containsKey(featureName) + } + + override fun isReadOnlySystemDisabledFeature(featureName: String, version: Int): Boolean { + return readOnlyDisabledFeatures.contains(featureName) + } + + override fun getReadOnlySystemEnabledFeatures(): ArrayMap<String, FeatureInfo> { + return readOnlyEnabledFeatures + } + } +} diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index cba521e639cb..196b5e7c02ab 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -22,8 +22,6 @@ import com.squareup.javapoet.JavaFile import com.squareup.javapoet.MethodSpec import com.squareup.javapoet.ParameterizedTypeName import com.squareup.javapoet.TypeSpec -import java.util.HashMap -import java.util.Map import javax.lang.model.element.Modifier /* @@ -52,7 +50,7 @@ import javax.lang.model.element.Modifier * public static boolean hasFeatureAutomotive(Context context); * public static boolean hasFeatureLeanback(Context context); * public static Boolean maybeHasFeature(String feature, int version); - * public static ArrayMap<String, FeatureInfo> getCompileTimeAvailableFeatures(); + * public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures(); * } * </pre> */ @@ -63,6 +61,7 @@ object SystemFeaturesGenerator { private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager") private val CONTEXT_CLASS = ClassName.get("android.content", "Context") private val FEATUREINFO_CLASS = ClassName.get("android.content.pm", "FeatureInfo") + private val ARRAYMAP_CLASS = ClassName.get("android.util", "ArrayMap") private val ASSUME_TRUE_CLASS = ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8") private val ASSUME_FALSE_CLASS = @@ -291,19 +290,19 @@ object SystemFeaturesGenerator { features: Collection<FeatureInfo>, ) { val methodBuilder = - MethodSpec.methodBuilder("getCompileTimeAvailableFeatures") + MethodSpec.methodBuilder("getReadOnlySystemEnabledFeatures") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .addAnnotation(ClassName.get("android.annotation", "NonNull")) .addJavadoc("Gets features marked as available at compile-time, keyed by name." + "\n\n@hide") .returns(ParameterizedTypeName.get( - ClassName.get(Map::class.java), + ARRAYMAP_CLASS, ClassName.get(String::class.java), FEATUREINFO_CLASS)) val availableFeatures = features.filter { it.readonly && it.version != null } - methodBuilder.addStatement("Map<String, FeatureInfo> features = new \$T<>(\$L)", - HashMap::class.java, availableFeatures.size) + methodBuilder.addStatement("\$T<String, FeatureInfo> features = new \$T<>(\$L)", + ARRAYMAP_CLASS, ARRAYMAP_CLASS, availableFeatures.size) if (!availableFeatures.isEmpty()) { methodBuilder.addStatement("FeatureInfo fi = new FeatureInfo()") } diff --git a/tools/systemfeatures/tests/golden/RoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoFeatures.java.gen index edbfc4237547..ee97b26159de 100644 --- a/tools/systemfeatures/tests/golden/RoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoFeatures.java.gen @@ -13,10 +13,9 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; +import android.util.ArrayMap; import com.android.aconfig.annotations.AssumeFalseForR8; import com.android.aconfig.annotations.AssumeTrueForR8; -import java.util.HashMap; -import java.util.Map; /** * @hide @@ -94,8 +93,8 @@ public final class RoFeatures { * @hide */ @NonNull - public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { - Map<String, FeatureInfo> features = new HashMap<>(2); + public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures() { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(2); FeatureInfo fi = new FeatureInfo(); fi.name = PackageManager.FEATURE_WATCH; fi.version = 1; diff --git a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen index bf7a00679fa6..40c7db7ff1df 100644 --- a/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RoNoFeatures.java.gen @@ -9,8 +9,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; -import java.util.HashMap; -import java.util.Map; +import android.util.ArrayMap; /** * @hide @@ -43,8 +42,8 @@ public final class RoNoFeatures { * @hide */ @NonNull - public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { - Map<String, FeatureInfo> features = new HashMap<>(0); + public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures() { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(0); return features; } } diff --git a/tools/systemfeatures/tests/golden/RwFeatures.java.gen b/tools/systemfeatures/tests/golden/RwFeatures.java.gen index b20b228f9814..7bf89614b92d 100644 --- a/tools/systemfeatures/tests/golden/RwFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwFeatures.java.gen @@ -12,8 +12,7 @@ import android.annotation.Nullable; import android.content.Context; import android.content.pm.FeatureInfo; import android.content.pm.PackageManager; -import java.util.HashMap; -import java.util.Map; +import android.util.ArrayMap; /** * @hide @@ -73,8 +72,8 @@ public final class RwFeatures { * @hide */ @NonNull - public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { - Map<String, FeatureInfo> features = new HashMap<>(0); + public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures() { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(0); return features; } } diff --git a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen index d91f5b62d8d4..eb7ec63f1d7d 100644 --- a/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen +++ b/tools/systemfeatures/tests/golden/RwNoFeatures.java.gen @@ -7,8 +7,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.pm.FeatureInfo; -import java.util.HashMap; -import java.util.Map; +import android.util.ArrayMap; /** * @hide @@ -32,8 +31,8 @@ public final class RwNoFeatures { * @hide */ @NonNull - public static Map<String, FeatureInfo> getCompileTimeAvailableFeatures() { - Map<String, FeatureInfo> features = new HashMap<>(0); + public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures() { + ArrayMap<String, FeatureInfo> features = new ArrayMap<>(0); return features; } } diff --git a/tools/systemfeatures/tests/src/ArrayMap.java b/tools/systemfeatures/tests/src/ArrayMap.java new file mode 100644 index 000000000000..a5ed9b088896 --- /dev/null +++ b/tools/systemfeatures/tests/src/ArrayMap.java @@ -0,0 +1,26 @@ +/* + * 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.util; + +import java.util.HashMap; + +/** Stub for testing. */ +public final class ArrayMap<K, V> extends HashMap<K, V> { + public ArrayMap(int capacity) { + super(capacity); + } +} diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java index 39f8fc44fe23..ed3f5c94ba79 100644 --- a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorTest.java @@ -60,7 +60,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); - assertThat(RwNoFeatures.getCompileTimeAvailableFeatures()).isEmpty(); + assertThat(RwNoFeatures.getReadOnlySystemEnabledFeatures()).isEmpty(); } @Test @@ -72,7 +72,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); - assertThat(RoNoFeatures.getCompileTimeAvailableFeatures()).isEmpty(); + assertThat(RoNoFeatures.getReadOnlySystemEnabledFeatures()).isEmpty(); // Also ensure we fall back to the PackageManager for feature APIs without an accompanying // versioned feature definition. @@ -106,7 +106,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); - assertThat(RwFeatures.getCompileTimeAvailableFeatures()).isEmpty(); + assertThat(RwFeatures.getReadOnlySystemEnabledFeatures()).isEmpty(); } @Test @@ -163,7 +163,7 @@ public class SystemFeaturesGeneratorTest { assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull(); assertThat(RoFeatures.maybeHasFeature("", 0)).isNull(); - Map<String, FeatureInfo> compiledFeatures = RoFeatures.getCompileTimeAvailableFeatures(); + Map<String, FeatureInfo> compiledFeatures = RoFeatures.getReadOnlySystemEnabledFeatures(); assertThat(compiledFeatures.keySet()) .containsExactly(PackageManager.FEATURE_WATCH, PackageManager.FEATURE_WIFI); assertThat(compiledFeatures.get(PackageManager.FEATURE_WATCH).version).isEqualTo(1); |