diff options
164 files changed, 4513 insertions, 2230 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 51247d18045f..542221c3fcc9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8,6 +8,9 @@ package android { public static final class Manifest.permission { ctor public Manifest.permission(); field public static final String ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER"; + field public static final String ACCESS_ADSERVICES_ATTRIBUTION = "android.permission.ACCESS_ADSERVICES_ATTRIBUTION"; + field public static final String ACCESS_ADSERVICES_CUSTOM_AUDIENCES = "android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCES"; + field public static final String ACCESS_ADSERVICES_TOPICS = "android.permission.ACCESS_ADSERVICES_TOPICS"; field public static final String ACCESS_BACKGROUND_LOCATION = "android.permission.ACCESS_BACKGROUND_LOCATION"; field public static final String ACCESS_BLOBS_ACROSS_USERS = "android.permission.ACCESS_BLOBS_ACROSS_USERS"; field public static final String ACCESS_CHECKIN_PROPERTIES = "android.permission.ACCESS_CHECKIN_PROPERTIES"; @@ -17,7 +20,6 @@ package android { field public static final String ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION"; field public static final String ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE"; field public static final String ACCESS_NOTIFICATION_POLICY = "android.permission.ACCESS_NOTIFICATION_POLICY"; - field public static final String ACCESS_SUPPLEMENTAL_APIS = "android.permission.ACCESS_SUPPLEMENTAL_APIS"; field public static final String ACCESS_WIFI_STATE = "android.permission.ACCESS_WIFI_STATE"; field public static final String ACCOUNT_MANAGER = "android.permission.ACCOUNT_MANAGER"; field public static final String ACTIVITY_RECOGNITION = "android.permission.ACTIVITY_RECOGNITION"; diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 5bc5bbc08074..27ca6a2794b9 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -20,10 +20,6 @@ package android.app { method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String); } - public class ActivityOptions { - method @NonNull public static android.app.ActivityOptions fromBundle(@NonNull android.os.Bundle); - } - public class AppOpsManager { field public static final String OPSTR_NO_ISOLATED_STORAGE = "android:no_isolated_storage"; } @@ -54,22 +50,6 @@ package android.app { method public void onCanceled(@NonNull android.app.PendingIntent); } - public class PropertyInvalidatedCache<Query, Result> { - ctor public PropertyInvalidatedCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.app.PropertyInvalidatedCache.QueryHandler<Query,Result>); - method public final void disableForCurrentProcess(); - method public final void invalidateCache(); - method public static void invalidateCache(@NonNull String, @NonNull String); - method @Nullable public final Result query(@NonNull Query); - field public static final String MODULE_BLUETOOTH = "bluetooth"; - field public static final String MODULE_TELEPHONY = "telephony"; - } - - public abstract static class PropertyInvalidatedCache.QueryHandler<Q, R> { - ctor public PropertyInvalidatedCache.QueryHandler(); - method @Nullable public abstract R apply(@NonNull Q); - method public boolean shouldBypassCache(@NonNull Q); - } - public class StatusBarManager { method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean); } @@ -358,6 +338,22 @@ package android.os { field public static final int DEVICE_INITIAL_SDK_INT; } + public class IpcDataCache<Query, Result> { + ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>); + method public void disableForCurrentProcess(); + method public static void disableForCurrentProcess(@NonNull String); + method public void invalidateCache(); + method public static void invalidateCache(@NonNull String, @NonNull String); + method @Nullable public Result query(@NonNull Query); + field public static final String MODULE_BLUETOOTH = "bluetooth"; + } + + public abstract static class IpcDataCache.QueryHandler<Q, R> { + ctor public IpcDataCache.QueryHandler(); + method @Nullable public abstract R apply(@NonNull Q); + method public boolean shouldBypassCache(@NonNull Q); + } + public interface Parcelable { method public default int getStability(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 15666017f431..5a6e8ee22128 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -234,6 +234,7 @@ package android { field public static final String POWER_SAVER = "android.permission.POWER_SAVER"; field public static final String PROVIDE_RESOLVER_RANKER_SERVICE = "android.permission.PROVIDE_RESOLVER_RANKER_SERVICE"; field public static final String PROVIDE_TRUST_AGENT = "android.permission.PROVIDE_TRUST_AGENT"; + field public static final String PROVISION_DEMO_DEVICE = "android.permission.PROVISION_DEMO_DEVICE"; field public static final String QUERY_ADMIN_POLICY = "android.permission.QUERY_ADMIN_POLICY"; field public static final String QUERY_TIME_ZONE_RULES = "android.permission.QUERY_TIME_ZONE_RULES"; field public static final String QUERY_USERS = "android.permission.QUERY_USERS"; @@ -1101,7 +1102,7 @@ package android.app.admin { method @RequiresPermission("android.permission.NOTIFY_PENDING_SYSTEM_UPDATE") public void notifyPendingSystemUpdate(long); method @RequiresPermission("android.permission.NOTIFY_PENDING_SYSTEM_UPDATE") public void notifyPendingSystemUpdate(long, boolean); method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean packageHasActiveAdmins(String); - method @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException; + method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice(@NonNull android.app.admin.FullyManagedDeviceProvisioningParams) throws android.app.admin.ProvisioningException; method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_MANAGEMENT_RESOURCES) public void resetDrawables(@NonNull String[]); method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_MANAGEMENT_RESOURCES) public void resetStrings(@NonNull String[]); method @RequiresPermission(android.Manifest.permission.SEND_LOST_MODE_LOCATION_UPDATES) public void sendLostModeLocationUpdate(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Boolean>); @@ -1254,6 +1255,7 @@ package android.app.admin { method @Nullable public java.util.Locale getLocale(); method @NonNull public String getOwnerName(); method @Nullable public String getTimeZone(); + method public boolean isDemoDevice(); method public boolean isLeaveAllSystemAppsEnabled(); method public void writeToParcel(@NonNull android.os.Parcel, @Nullable int); field @NonNull public static final android.os.Parcelable.Creator<android.app.admin.FullyManagedDeviceProvisioningParams> CREATOR; @@ -1264,6 +1266,7 @@ package android.app.admin { method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams build(); method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams.Builder setAdminExtras(@NonNull android.os.PersistableBundle); method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams.Builder setCanDeviceOwnerGrantSensorsPermissions(boolean); + method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams.Builder setDemoDevice(boolean); method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams.Builder setLeaveAllSystemAppsEnabled(boolean); method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams.Builder setLocalTime(long); method @NonNull public android.app.admin.FullyManagedDeviceProvisioningParams.Builder setLocale(@Nullable java.util.Locale); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 0dc08061a8d4..c5cce35d988e 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -154,7 +154,6 @@ package android.app { } public class ActivityOptions { - method @NonNull public static android.app.ActivityOptions fromBundle(@NonNull android.os.Bundle); method public boolean isEligibleForLegacyPermissionPrompt(); method @NonNull public static android.app.ActivityOptions makeCustomAnimation(@NonNull android.content.Context, int, int, int, @Nullable android.os.Handler, @Nullable android.app.ActivityOptions.OnAnimationStartedListener, @Nullable android.app.ActivityOptions.OnAnimationFinishedListener); method @NonNull @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS) public static android.app.ActivityOptions makeCustomTaskAnimation(@NonNull android.content.Context, int, int, @Nullable android.os.Handler, @Nullable android.app.ActivityOptions.OnAnimationStartedListener, @Nullable android.app.ActivityOptions.OnAnimationFinishedListener); @@ -374,16 +373,17 @@ package android.app { public class PropertyInvalidatedCache<Query, Result> { ctor public PropertyInvalidatedCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.app.PropertyInvalidatedCache.QueryHandler<Query,Result>); method @NonNull public static String createPropertyName(@NonNull String, @NonNull String); - method public final void disableForCurrentProcess(); + method public void disableForCurrentProcess(); + method public static void disableForCurrentProcess(@NonNull String); method public static void disableForTestMode(); method public final void disableInstance(); method public final void disableSystemWide(); method public final void forgetDisableLocal(); method public boolean getDisabledState(); - method public final void invalidateCache(); + method public void invalidateCache(); method public static void invalidateCache(@NonNull String, @NonNull String); method public final boolean isDisabled(); - method @Nullable public final Result query(@NonNull Query); + method @Nullable public Result query(@NonNull Query); method public static void setTestMode(boolean); method public void testPropertyName(); field public static final String MODULE_BLUETOOTH = "bluetooth"; @@ -1727,6 +1727,19 @@ package android.os { method @NonNull public static byte[] digest(@NonNull java.io.InputStream, @NonNull String) throws java.io.IOException, java.security.NoSuchAlgorithmException; } + public class IpcDataCache<Query, Result> extends android.app.PropertyInvalidatedCache<Query,Result> { + ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>); + method public static void disableForCurrentProcess(@NonNull String); + method public static void invalidateCache(@NonNull String, @NonNull String); + field public static final String MODULE_BLUETOOTH = "bluetooth"; + field public static final String MODULE_SYSTEM = "system_server"; + field public static final String MODULE_TEST = "test"; + } + + public abstract static class IpcDataCache.QueryHandler<Q, R> extends android.app.PropertyInvalidatedCache.QueryHandler<Q,R> { + ctor public IpcDataCache.QueryHandler(); + } + public final class MessageQueue { method public int postSyncBarrier(); method public void removeSyncBarrier(int); @@ -2878,6 +2891,7 @@ package android.view { method public static int getHoverTooltipHideTimeout(); method public static int getHoverTooltipShowTimeout(); method public static int getLongPressTooltipHideTimeout(); + method public int getPreferKeepClearForFocusDelay(); } public class ViewDebug { diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 87ac6cb1fe4c..0178fa143445 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -1421,17 +1421,9 @@ public class ActivityOptions extends ComponentOptions { return mRemoteTransition; } - /** - * Creates an ActivityOptions from the Bundle generated from {@link ActivityOptions#toBundle()}. - * Returns an instance of ActivityOptions populated with options with known keys from the - * provided Bundle, stripping out unknown entries. - * @hide - */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) - @TestApi - @NonNull - public static ActivityOptions fromBundle(@NonNull Bundle bOptions) { - return new ActivityOptions(bOptions); + /** @hide */ + public static ActivityOptions fromBundle(Bundle bOptions) { + return bOptions != null ? new ActivityOptions(bOptions) : null; } /** @hide */ diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index a82ecce2dc04..4fbe232556ed 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -116,7 +116,7 @@ interface INotificationManager ParceledListSlice getNotificationChannelGroups(String pkg); boolean onlyHasDefaultChannel(String pkg, int uid); boolean areChannelsBypassingDnd(); - ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int userId); + ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int uid); boolean isPackagePaused(String pkg); void deleteNotificationHistoryItem(String pkg, int uid, long postedTime); boolean isPermissionFixed(String pkg, int userId); diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 2f202d95e0e3..df7bf7b94700 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -18,7 +18,6 @@ package android.app; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SystemApi; import android.annotation.TestApi; import android.os.Handler; import android.os.Looper; @@ -137,6 +136,26 @@ import java.util.concurrent.atomic.AtomicLong; * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday * for the first time; on subsequent queries, we return the already-known Birthday object. * + * The second parameter to the IpcDataCache constructor is a string that identifies the "module" + * that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any + * string is permitted. The third parameters is the name of the API being cached; this, too, can + * any value. The fourth is the name of the cache. The cache is usually named after th API. + * Some things you must know about the three strings: + * <list> + * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}. + * Usually, the SELinux rules permit a process to write a system property (and therefore + * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that + * although the cache can be constructed with any module string, whatever string is chosen must be + * consistent with the SELinux configuration. + * <ul> The API name can be any string of alphanumeric characters. All caches with the same API + * are invalidated at the same time. If a server supports several caches and all are invalidated + * in common, then it is most efficient to assign the same API string to every cache. + * <ul> The cache name can be any string. In debug output, the name is used to distiguish between + * caches with the same API name. The cache name is also used when disabling caches in the + * current process. So, invalidation is based on the module+api but disabling (which is generally + * a once-per-process operation) is based on the cache name. + * </list> + * * User birthdays do occasionally change, so we have to modify the server to invalidate this * cache when necessary. That invalidation code looks like this: * @@ -192,25 +211,23 @@ import java.util.concurrent.atomic.AtomicLong; * <pre> * public class ActivityThread { * ... - * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache - * private static final String BDAY_CACHE_KEY = "cache_key.birthdayd"; - * private final PropertyInvalidatedCache<Integer, Birthday%> mBirthdayCache = new - * PropertyInvalidatedCache<Integer, Birthday%>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) { - * {@literal @}Override - * protected Birthday recompute(Integer userId) { - * return GetService("birthdayd").getUserBirthday(userId); - * } - * {@literal @}Override - * protected boolean bypass(Integer userId) { - * return userId == NEXT_BIRTHDAY; - * } - * }; + * private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery = + * new IpcDataCache.QueryHandler<Integer, Birthday>() { + * {@literal @}Override + * public Birthday apply(Integer) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * {@literal @}Override + * public boolean shouldBypassQuery(Integer userId) { + * return userId == NEXT_BIRTHDAY; + * } + * }; * ... * } * </pre> * - * If the {@code bypass()} method returns true then the cache is not used for that - * particular query. The {@code bypass()} method is not abstract and the default + * If the {@code shouldBypassQuery()} method returns true then the cache is not used for that + * particular query. The {@code shouldBypassQuery()} method is not abstract and the default * implementation returns false. * * For security, there is a allowlist of processes that are allowed to invalidate a cache. @@ -231,14 +248,12 @@ import java.util.concurrent.atomic.AtomicLong; * @param <Result> The class holding cache entries; use a boxed primitive if possible * @hide */ -@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public class PropertyInvalidatedCache<Query, Result> { /** * This is a configuration class that customizes a cache instance. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public static abstract class QueryHandler<Q,R> { /** @@ -285,7 +300,6 @@ public class PropertyInvalidatedCache<Query, Result> { * The module used for bluetooth caches. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public static final String MODULE_BLUETOOTH = "bluetooth"; @@ -533,7 +547,6 @@ public class PropertyInvalidatedCache<Query, Result> { * @param computer The code to compute values that are not in the cache. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api, @NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) { @@ -792,7 +805,7 @@ public class PropertyInvalidatedCache<Query, Result> { * TODO(216112648) Remove this in favor of disableForCurrentProcess(). * @hide */ - public final void disableLocal() { + public void disableLocal() { disableForCurrentProcess(); } @@ -802,12 +815,17 @@ public class PropertyInvalidatedCache<Query, Result> { * property. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi - public final void disableForCurrentProcess() { + public void disableForCurrentProcess() { disableLocal(mCacheName); } + /** @hide */ + @TestApi + public static void disableForCurrentProcess(@NonNull String cacheName) { + disableLocal(cacheName); + } + /** * Return whether a cache instance is disabled. * @hide @@ -821,9 +839,8 @@ public class PropertyInvalidatedCache<Query, Result> { * Get a value from the cache or recompute it. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi - public final @Nullable Result query(@NonNull Query query) { + public @Nullable Result query(@NonNull Query query) { // Let access to mDisabled race: it's atomic anyway. long currentNonce = (!isDisabled()) ? getCurrentNonce() : NONCE_DISABLED; if (bypass(query)) { @@ -964,9 +981,8 @@ public class PropertyInvalidatedCache<Query, Result> { * PropertyInvalidatedCache is keyed on a particular property value. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi - public final void invalidateCache() { + public void invalidateCache() { invalidateCache(mPropertyName); } @@ -974,7 +990,6 @@ public class PropertyInvalidatedCache<Query, Result> { * Invalidate caches in all processes that are keyed for the module and api. * @hide */ - @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) @TestApi public static void invalidateCache(@NonNull String module, @NonNull String api) { invalidateCache(createPropertyName(module, api)); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index b436f6e7374f..8d4e0d6c1f42 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -1413,6 +1413,9 @@ public class DevicePolicyManager { * admin app when performing the admin-integrated provisioning flow as a result of the * {@link #ACTION_GET_PROVISIONING_MODE} activity. * + * <p>This extra may also be provided to the admin app via an intent extra for {@link + * #ACTION_GET_PROVISIONING_MODE}. + * * @see #ACTION_GET_PROVISIONING_MODE */ public static final String EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT = @@ -3062,6 +3065,8 @@ public class DevicePolicyManager { * <li>{@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE}</li> * <li>{@link #EXTRA_PROVISIONING_IMEI}</li> * <li>{@link #EXTRA_PROVISIONING_SERIAL_NUMBER}</li> + * <li>{@link #EXTRA_PROVISIONING_ALLOWED_PROVISIONING_MODES}</li> + * <li>{@link #EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT}</li> * </ul> * * <p>The target activity should return one of the following values in @@ -3085,8 +3090,22 @@ public class DevicePolicyManager { * activity, along with the values of the {@link #EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE} extra * that are already supplied to this activity. * - * @see #EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION - * @see #EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED + * <p>Other extras the target activity may include in the intent result: + * <ul> + * <li>{@link #EXTRA_PROVISIONING_DISCLAIMERS}</li> + * <li>{@link #EXTRA_PROVISIONING_SKIP_ENCRYPTION}</li> + * <li>{@link #EXTRA_PROVISIONING_KEEP_SCREEN_ON}</li> + * <li>{@link #EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION} for work profile + * provisioning</li> + * <li>{@link #EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED} for work profile + * provisioning</li> + * <li>{@link #EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT} for fully-managed + * device provisioning</li> + * <li>{@link #EXTRA_PROVISIONING_LOCALE} for fully-managed device provisioning</li> + * <li>{@link #EXTRA_PROVISIONING_LOCAL_TIME} for fully-managed device provisioning</li> + * <li>{@link #EXTRA_PROVISIONING_TIME_ZONE} for fully-managed device provisioning</li> + * </ul> + * * @see #ACTION_ADMIN_POLICY_COMPLIANCE */ public static final String ACTION_GET_PROVISIONING_MODE = @@ -14776,17 +14795,20 @@ public class DevicePolicyManager { * <p>The method {@link #checkProvisioningPrecondition} must be returning {@link #STATUS_OK} * before calling this method. * + * <p>Holders of {@link android.Manifest.permission#PROVISION_DEMO_DEVICE} can call this API + * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}.</p> + * * @param provisioningParams Params required to provision a fully managed device, * see {@link FullyManagedDeviceProvisioningParams}. * - * @throws SecurityException if the caller does not hold - * {@link android.Manifest.permission#MANAGE_PROFILE_AND_DEVICE_OWNERS}. * @throws ProvisioningException if an error occurred during provisioning. * * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) + @RequiresPermission(anyOf = { + android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, + android.Manifest.permission.PROVISION_DEMO_DEVICE}) public void provisionFullyManagedDevice( @NonNull FullyManagedDeviceProvisioningParams provisioningParams) throws ProvisioningException { diff --git a/core/java/android/app/admin/FullyManagedDeviceProvisioningParams.java b/core/java/android/app/admin/FullyManagedDeviceProvisioningParams.java index 1f7ae4ad35de..49992452df86 100644 --- a/core/java/android/app/admin/FullyManagedDeviceProvisioningParams.java +++ b/core/java/android/app/admin/FullyManagedDeviceProvisioningParams.java @@ -26,6 +26,7 @@ import android.content.ComponentName; import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; +import android.provider.Settings; import android.stats.devicepolicy.DevicePolicyEnums; import java.util.Locale; @@ -44,6 +45,7 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { "CAN_DEVICE_OWNER_GRANT_SENSOR_PERMISSIONS"; private static final String TIME_ZONE_PROVIDED_PARAM = "TIME_ZONE_PROVIDED"; private static final String LOCALE_PROVIDED_PARAM = "LOCALE_PROVIDED"; + private static final String DEMO_DEVICE = "DEMO_DEVICE"; @NonNull private final ComponentName mDeviceAdminComponentName; @NonNull private final String mOwnerName; @@ -54,6 +56,8 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { @Nullable private final Locale mLocale; private final boolean mDeviceOwnerCanGrantSensorsPermissions; @NonNull private final PersistableBundle mAdminExtras; + private final boolean mDemoDevice; + private FullyManagedDeviceProvisioningParams( @NonNull ComponentName deviceAdminComponentName, @@ -63,7 +67,8 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { long localTime, @Nullable @SuppressLint("UseIcu") Locale locale, boolean deviceOwnerCanGrantSensorsPermissions, - @NonNull PersistableBundle adminExtras) { + @NonNull PersistableBundle adminExtras, + boolean demoDevice) { this.mDeviceAdminComponentName = requireNonNull(deviceAdminComponentName); this.mOwnerName = requireNonNull(ownerName); this.mLeaveAllSystemAppsEnabled = leaveAllSystemAppsEnabled; @@ -73,6 +78,7 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { this.mDeviceOwnerCanGrantSensorsPermissions = deviceOwnerCanGrantSensorsPermissions; this.mAdminExtras = adminExtras; + this.mDemoDevice = demoDevice; } private FullyManagedDeviceProvisioningParams( @@ -83,7 +89,8 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { long localTime, @Nullable String localeStr, boolean deviceOwnerCanGrantSensorsPermissions, - @Nullable PersistableBundle adminExtras) { + @Nullable PersistableBundle adminExtras, + boolean demoDevice) { this(deviceAdminComponentName, ownerName, leaveAllSystemAppsEnabled, @@ -91,7 +98,8 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { localTime, getLocale(localeStr), deviceOwnerCanGrantSensorsPermissions, - adminExtras); + adminExtras, + demoDevice); } @Nullable @@ -166,6 +174,14 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { } /** + * @return true if this device is being setup as a retail demo device, see + * {@link Settings.Global#DEVICE_DEMO_MODE}. + */ + public boolean isDemoDevice() { + return mDemoDevice; + } + + /** * Logs the provisioning params using {@link DevicePolicyEventLogger}. * * @hide @@ -178,6 +194,7 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { mDeviceOwnerCanGrantSensorsPermissions); logParam(callerPackage, TIME_ZONE_PROVIDED_PARAM, /* value= */ mTimeZone != null); logParam(callerPackage, LOCALE_PROVIDED_PARAM, /* value= */ mLocale != null); + logParam(callerPackage, DEMO_DEVICE, mDemoDevice); } private void logParam(String callerPackage, String param, boolean value) { @@ -204,6 +221,9 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { // Default to allowing control over sensor permission grants. boolean mDeviceOwnerCanGrantSensorsPermissions = true; @NonNull private PersistableBundle mAdminExtras; + // Default is normal user devices + boolean mDemoDevice = false; + /** * Initialize a new {@link Builder} to construct a @@ -289,6 +309,16 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { } /** + * Marks the device as a demo device, see {@link Settings.Global#DEVICE_DEMO_MODE}. The + * default value if unset is {@code false}. + */ + @NonNull + public Builder setDemoDevice(boolean demoDevice) { + this.mDemoDevice = demoDevice; + return this; + } + + /** * Combines all of the attributes that have been set on this {@code Builder} * * @return a new {@link FullyManagedDeviceProvisioningParams} object. @@ -303,7 +333,8 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { mLocalTime, mLocale, mDeviceOwnerCanGrantSensorsPermissions, - mAdminExtras); + mAdminExtras, + mDemoDevice); } } @@ -327,6 +358,7 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { + ", mDeviceOwnerCanGrantSensorsPermissions=" + mDeviceOwnerCanGrantSensorsPermissions + ", mAdminExtras=" + mAdminExtras + + ", mDemoDevice=" + mDemoDevice + '}'; } @@ -340,6 +372,7 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { dest.writeString(mLocale == null ? null : mLocale.toLanguageTag()); dest.writeBoolean(mDeviceOwnerCanGrantSensorsPermissions); dest.writePersistableBundle(mAdminExtras); + dest.writeBoolean(mDemoDevice); } @NonNull @@ -355,6 +388,7 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { String locale = in.readString(); boolean deviceOwnerCanGrantSensorsPermissions = in.readBoolean(); PersistableBundle adminExtras = in.readPersistableBundle(); + boolean demoDevice = in.readBoolean(); return new FullyManagedDeviceProvisioningParams( componentName, @@ -364,7 +398,8 @@ public final class FullyManagedDeviceProvisioningParams implements Parcelable { localtime, locale, deviceOwnerCanGrantSensorsPermissions, - adminExtras); + adminExtras, + demoDevice); } @Override diff --git a/core/java/android/app/servertransaction/ActivityTransactionItem.java b/core/java/android/app/servertransaction/ActivityTransactionItem.java index 6a6d76d20259..469a9bfe59ef 100644 --- a/core/java/android/app/servertransaction/ActivityTransactionItem.java +++ b/core/java/android/app/servertransaction/ActivityTransactionItem.java @@ -64,11 +64,11 @@ public abstract class ActivityTransactionItem extends ClientTransactionItem { final ActivityClientRecord r = client.getActivityClient(token); if (r == null) { throw new IllegalArgumentException("Activity client record must not be null to execute " - + "transaction item"); + + "transaction item: " + this); } if (client.getActivity(token) == null) { throw new IllegalArgumentException("Activity must not be null to execute " - + "transaction item"); + + "transaction item: " + this); } return r; } diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java index abf1058f45a2..d7e09519bfb7 100644 --- a/core/java/android/app/servertransaction/LaunchActivityItem.java +++ b/core/java/android/app/servertransaction/LaunchActivityItem.java @@ -179,7 +179,7 @@ public class LaunchActivityItem extends ClientTransactionItem { in.readPersistableBundle(getClass().getClassLoader()), in.createTypedArrayList(ResultInfo.CREATOR), in.createTypedArrayList(ReferrerIntent.CREATOR), - readActivityOptions(in), in.readBoolean(), + ActivityOptions.fromBundle(in.readBundle()), in.readBoolean(), in.readTypedObject(ProfilerInfo.CREATOR), in.readStrongBinder(), IActivityClientController.Stub.asInterface(in.readStrongBinder()), @@ -187,11 +187,6 @@ public class LaunchActivityItem extends ClientTransactionItem { in.readBoolean()); } - private static ActivityOptions readActivityOptions(Parcel in) { - Bundle bundle = in.readBundle(); - return bundle != null ? ActivityOptions.fromBundle(bundle) : null; - } - public static final @NonNull Creator<LaunchActivityItem> CREATOR = new Creator<LaunchActivityItem>() { public LaunchActivityItem createFromParcel(Parcel in) { diff --git a/core/java/android/app/servertransaction/StartActivityItem.java b/core/java/android/app/servertransaction/StartActivityItem.java index f267060d1be6..15f65f6d9d26 100644 --- a/core/java/android/app/servertransaction/StartActivityItem.java +++ b/core/java/android/app/servertransaction/StartActivityItem.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.app.ActivityOptions; import android.app.ActivityThread.ActivityClientRecord; import android.app.ClientTransactionHandler; -import android.os.Bundle; import android.os.Parcel; import android.os.Trace; @@ -84,8 +83,7 @@ public class StartActivityItem extends ActivityLifecycleItem { /** Read from Parcel. */ private StartActivityItem(Parcel in) { - Bundle bundle = in.readBundle(); - mActivityOptions = bundle != null ? ActivityOptions.fromBundle(bundle) : null; + mActivityOptions = ActivityOptions.fromBundle(in.readBundle()); } public static final @NonNull Creator<StartActivityItem> CREATOR = diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 673127e9f808..2961b5505794 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -2084,7 +2084,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { splitNames = source.createString8Array(); splitSourceDirs = source.createString8Array(); splitPublicSourceDirs = source.createString8Array(); - splitDependencies = source.readSparseArray(null); + splitDependencies = source.readSparseArray(null, int[].class); nativeLibraryDir = source.readString8(); secondaryNativeLibraryDir = source.readString8(); nativeLibraryRootDir = source.readString8(); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index f4bc1616da2b..2915d852cc26 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -121,6 +121,9 @@ public abstract class PackageManager { /** {@hide} */ public static final boolean APPLY_DEFAULT_TO_DEVICE_PROTECTED_STORAGE = true; + /** {@hide} */ + public static final boolean ENABLE_SHARED_UID_MIGRATION = true; + /** * This exception is thrown when a given package, application, or component * name cannot be found. diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index e914432630f7..4d4a57db84be 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1944,19 +1944,26 @@ public class PackageParser { TypedArray sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifest); - String str = sa.getNonConfigurationString( - com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0); - if (str != null && str.length() > 0) { - String nameError = validateName(str, true, true); - if (nameError != null && !"android".equals(pkg.packageName)) { - outError[0] = "<manifest> specifies bad sharedUserId name \"" - + str + "\": " + nameError; - mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID; - return null; + int maxSdkVersion = 0; + if (PackageManager.ENABLE_SHARED_UID_MIGRATION) { + maxSdkVersion = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_sharedUserMaxSdkVersion, 0); + } + if (maxSdkVersion == 0 || maxSdkVersion >= Build.VERSION.RESOURCES_SDK_INT) { + String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0); + if (str != null && str.length() > 0) { + String nameError = validateName(str, true, true); + if (nameError != null && !"android".equals(pkg.packageName)) { + outError[0] = "<manifest> specifies bad sharedUserId name \"" + + str + "\": " + nameError; + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID; + return null; + } + pkg.mSharedUserId = str.intern(); + pkg.mSharedUserLabel = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); } - pkg.mSharedUserId = str.intern(); - pkg.mSharedUserLabel = sa.getResourceId( - com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); } pkg.installLocation = sa.getInteger( diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index af57f793bf73..02302a20fe38 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -40,6 +40,7 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; +import com.android.internal.inputmethod.InputMethodNavButtonFlags; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; import com.android.internal.view.IInlineSuggestionsRequestCallback; @@ -70,7 +71,7 @@ class IInputMethodWrapper extends IInputMethod.Stub private static final int DO_SET_INPUT_CONTEXT = 20; private static final int DO_UNSET_INPUT_CONTEXT = 30; private static final int DO_START_INPUT = 32; - private static final int DO_ON_SHOULD_SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN_CHANGED = 35; + private static final int DO_ON_NAV_BUTTON_FLAGS_CHANGED = 35; private static final int DO_CREATE_SESSION = 40; private static final int DO_SET_SESSION_ENABLED = 45; private static final int DO_SHOW_SOFT_INPUT = 60; @@ -176,7 +177,7 @@ class IInputMethodWrapper extends IInputMethod.Stub try { inputMethod.initializeInternal((IBinder) args.arg1, (IInputMethodPrivilegedOperations) args.arg2, msg.arg1, - (boolean) args.arg3, msg.arg2 != 0); + (boolean) args.arg3, msg.arg2); } finally { args.recycle(); } @@ -196,22 +197,20 @@ class IInputMethodWrapper extends IInputMethod.Stub final EditorInfo info = (EditorInfo) args.arg3; final CancellationGroup cancellationGroup = (CancellationGroup) args.arg4; final boolean restarting = args.argi5 == 1; - final boolean shouldShowImeSwitcherWhenImeIsShown = args.argi6 != 0; + @InputMethodNavButtonFlags + final int navButtonFlags = args.argi6; final InputConnection ic = inputContext != null ? new RemoteInputConnection(mTarget, inputContext, cancellationGroup) : null; info.makeCompatible(mTargetSdkVersion); inputMethod.dispatchStartInputWithToken(ic, info, restarting, startInputToken, - shouldShowImeSwitcherWhenImeIsShown); + navButtonFlags); args.recycle(); return; } - case DO_ON_SHOULD_SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN_CHANGED: { - final boolean shouldShowImeSwitcherWhenImeIsShown = msg.arg1 != 0; - inputMethod.onShouldShowImeSwitcherWhenImeIsShownChanged( - shouldShowImeSwitcherWhenImeIsShown); + case DO_ON_NAV_BUTTON_FLAGS_CHANGED: + inputMethod.onNavButtonFlagsChanged(msg.arg1); return; - } case DO_CREATE_SESSION: { SomeArgs args = (SomeArgs)msg.obj; inputMethod.createSession(new InputMethodSessionCallbackWrapper( @@ -301,10 +300,9 @@ class IInputMethodWrapper extends IInputMethod.Stub @Override public void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privOps, int configChanges, boolean stylusHwSupported, - boolean shouldShowImeSwitcherWhenImeIsShown) { + @InputMethodNavButtonFlags int navButtonFlags) { mCaller.executeOrSendMessage(mCaller.obtainMessageIIOOO(DO_INITIALIZE_INTERNAL, - configChanges, shouldShowImeSwitcherWhenImeIsShown ? 1 : 0, token, privOps, - stylusHwSupported)); + configChanges, navButtonFlags, token, privOps, stylusHwSupported)); } @BinderThread @@ -344,23 +342,21 @@ class IInputMethodWrapper extends IInputMethod.Stub @BinderThread @Override public void startInput(IBinder startInputToken, IInputContext inputContext, - EditorInfo attribute, boolean restarting, boolean shouldShowImeSwitcherWhenImeIsShown) { + EditorInfo attribute, boolean restarting, + @InputMethodNavButtonFlags int navButtonFlags) { if (mCancellationGroup == null) { Log.e(TAG, "startInput must be called after bindInput."); mCancellationGroup = new CancellationGroup(); } mCaller.executeOrSendMessage(mCaller.obtainMessageOOOOII(DO_START_INPUT, startInputToken, - inputContext, attribute, mCancellationGroup, restarting ? 1 : 0, - shouldShowImeSwitcherWhenImeIsShown ? 1 : 0)); + inputContext, attribute, mCancellationGroup, restarting ? 1 : 0, navButtonFlags)); } @BinderThread @Override - public void onShouldShowImeSwitcherWhenImeIsShownChanged( - boolean shouldShowImeSwitcherWhenImeIsShown) { - mCaller.executeOrSendMessage(mCaller.obtainMessageI( - DO_ON_SHOULD_SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN_CHANGED, - shouldShowImeSwitcherWhenImeIsShown ? 1 : 0)); + public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageI(DO_ON_NAV_BUTTON_FLAGS_CHANGED, navButtonFlags)); } @BinderThread diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index fbc0732affe1..60cd4181e933 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -140,6 +140,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.ImeTracing; +import com.android.internal.inputmethod.InputMethodNavButtonFlags; import com.android.internal.inputmethod.InputMethodPrivilegedOperations; import com.android.internal.inputmethod.InputMethodPrivilegedOperationsRegistry; import com.android.internal.view.IInlineSuggestionsRequestCallback; @@ -660,7 +661,7 @@ public class InputMethodService extends AbstractInputMethodService { @Override public final void initializeInternal(@NonNull IBinder token, IInputMethodPrivilegedOperations privilegedOperations, int configChanges, - boolean stylusHwSupported, boolean shouldShowImeSwitcherWhenImeIsShown) { + boolean stylusHwSupported, @InputMethodNavButtonFlags int navButtonFlags) { if (mDestroyed) { Log.i(TAG, "The InputMethodService has already onDestroyed()." + "Ignore the initialization."); @@ -673,8 +674,7 @@ public class InputMethodService extends AbstractInputMethodService { if (stylusHwSupported) { mInkWindow = new InkWindow(mWindow.getContext()); } - mNavigationBarController.setShouldShowImeSwitcherWhenImeIsShown( - shouldShowImeSwitcherWhenImeIsShown); + mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags); attachToken(token); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -784,10 +784,9 @@ public class InputMethodService extends AbstractInputMethodService { @Override public final void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, boolean restarting, - @NonNull IBinder startInputToken, boolean shouldShowImeSwitcherWhenImeIsShown) { + @NonNull IBinder startInputToken, @InputMethodNavButtonFlags int navButtonFlags) { mPrivOps.reportStartInputAsync(startInputToken); - mNavigationBarController.setShouldShowImeSwitcherWhenImeIsShown( - shouldShowImeSwitcherWhenImeIsShown); + mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags); if (restarting) { restartInput(inputConnection, editorInfo); } else { @@ -801,10 +800,8 @@ public class InputMethodService extends AbstractInputMethodService { */ @MainThread @Override - public void onShouldShowImeSwitcherWhenImeIsShownChanged( - boolean shouldShowImeSwitcherWhenImeIsShown) { - mNavigationBarController.setShouldShowImeSwitcherWhenImeIsShown( - shouldShowImeSwitcherWhenImeIsShown); + public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { + mNavigationBarController.onNavButtonFlagsChanged(navButtonFlags); } /** diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index 19fa01de4a5f..a247db2f3310 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -49,6 +49,8 @@ import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; +import com.android.internal.inputmethod.InputMethodNavButtonFlags; + import java.util.Objects; /** @@ -77,8 +79,7 @@ final class NavigationBarController { default void onDestroy() { } - default void setShouldShowImeSwitcherWhenImeIsShown( - boolean shouldShowImeSwitcherWhenImeIsShown) { + default void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { } default String toDebugString() { @@ -117,8 +118,8 @@ final class NavigationBarController { mImpl.onDestroy(); } - void setShouldShowImeSwitcherWhenImeIsShown(boolean shouldShowImeSwitcherWhenImeIsShown) { - mImpl.setShouldShowImeSwitcherWhenImeIsShown(shouldShowImeSwitcherWhenImeIsShown); + void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { + mImpl.onNavButtonFlagsChanged(navButtonFlags); } String toDebugString() { @@ -448,11 +449,14 @@ final class NavigationBarController { } @Override - public void setShouldShowImeSwitcherWhenImeIsShown( - boolean shouldShowImeSwitcherWhenImeIsShown) { + public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { if (mDestroyed) { return; } + + final boolean shouldShowImeSwitcherWhenImeIsShown = + (navButtonFlags & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) + != 0; if (mShouldShowImeSwitcherWhenImeIsShown == shouldShowImeSwitcherWhenImeIsShown) { return; } diff --git a/core/java/android/os/ExternalVibration.java b/core/java/android/os/ExternalVibration.java index 4e0995fb8792..db5bc7024e0b 100644 --- a/core/java/android/os/ExternalVibration.java +++ b/core/java/android/os/ExternalVibration.java @@ -47,7 +47,13 @@ public class ExternalVibration implements Parcelable { this(uid, pkg, attrs, controller, new Binder()); } - private ExternalVibration(int uid, @NonNull String pkg, @NonNull AudioAttributes attrs, + /** + * Full constructor, but exposed to construct the ExternalVibration with an explicit binder + * token (for mocks). + * + * @hide + */ + public ExternalVibration(int uid, @NonNull String pkg, @NonNull AudioAttributes attrs, @NonNull IExternalVibrationController controller, @NonNull IBinder token) { mUid = uid; mPkg = Preconditions.checkNotNull(pkg); @@ -166,7 +172,7 @@ public class ExternalVibration implements Parcelable { + "pkg=" + mPkg + ", " + "attrs=" + mAttrs + ", " + "controller=" + mController - + "token=" + mController + + "token=" + mToken + "}"; } diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java new file mode 100644 index 000000000000..00734c806b7e --- /dev/null +++ b/core/java/android/os/IpcDataCache.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2022 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.os; + +import android.app.PropertyInvalidatedCache; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.FastPrintWriter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +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.atomic.AtomicLong; + +/** + * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing, + * but doesn't hold a lock across data fetches on query misses. + * + * The intended use case is caching frequently-read, seldom-changed information normally retrieved + * across interprocess communication. Imagine that you've written a user birthday information + * daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface over + * binder. That binder interface looks something like this: + * + * <pre> + * parcelable Birthday { + * int month; + * int day; + * } + * interface IUserBirthdayService { + * Birthday getUserBirthday(int userId); + * } + * </pre> + * + * Suppose the service implementation itself looks like this... + * + * <pre> + * public class UserBirthdayServiceImpl implements IUserBirthdayService { + * private final HashMap<Integer, Birthday%> mUidToBirthday; + * {@literal @}Override + * public synchronized Birthday getUserBirthday(int userId) { + * return mUidToBirthday.get(userId); + * } + * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { + * mUidToBirthday.clear(); + * mUidToBirthday.putAll(uidToBirthday); + * } + * } + * </pre> + * + * ... and we have a client in frameworks (loaded into every app process) that looks like this: + * + * <pre> + * public class ActivityThread { + * ... + * public Birthday getUserBirthday(int userId) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * ... + * } + * </pre> + * + * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call to + * the birthdayd process and consult its database of birthdays. If we query user birthdays + * frequently, we do a lot of work that we don't have to do, since user birthdays change + * infrequently. + * + * IpcDataCache is part of a pattern for optimizing this kind of information-querying code. Using + * {@code IpcDataCache}, you'd write the client this way: + * + * <pre> + * public class ActivityThread { + * ... + * private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery = + * new IpcDataCache.QueryHandler<Integer, Birthday>() { + * {@literal @}Override + * public Birthday apply(Integer) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * }; + * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache + * private static final String BDAY_API = "getUserBirthday"; + * private final IpcDataCache<Integer, Birthday%> mBirthdayCache = new + * IpcDataCache<Integer, Birthday%>( + * BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery); + * + * public void disableUserBirthdayCache() { + * mBirthdayCache.disableForCurrentProcess(); + * } + * public void invalidateUserBirthdayCache() { + * mBirthdayCache.invalidateCache(); + * } + * public Birthday getUserBirthday(int userId) { + * return mBirthdayCache.query(userId); + * } + * ... + * } + * </pre> + * + * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday + * for the first time; on subsequent queries, we return the already-known Birthday object. + * + * The second parameter to the IpcDataCache constructor is a string that identifies the "module" + * that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any + * string is permitted. The third parameters is the name of the API being cached; this, too, can + * any value. The fourth is the name of the cache. The cache is usually named after th API. + * Some things you must know about the three strings: + * <list> + * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}. + * Usually, the SELinux rules permit a process to write a system property (and therefore + * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that + * although the cache can be constructed with any module string, whatever string is chosen must be + * consistent with the SELinux configuration. + * <ul> The API name can be any string of alphanumeric characters. All caches with the same API + * are invalidated at the same time. If a server supports several caches and all are invalidated + * in common, then it is most efficient to assign the same API string to every cache. + * <ul> The cache name can be any string. In debug output, the name is used to distiguish between + * caches with the same API name. The cache name is also used when disabling caches in the + * current process. So, invalidation is based on the module+api but disabling (which is generally + * a once-per-process operation) is based on the cache name. + * </list> + * + * User birthdays do occasionally change, so we have to modify the server to invalidate this + * cache when necessary. That invalidation code looks like this: + * + * <pre> + * public class UserBirthdayServiceImpl { + * ... + * public UserBirthdayServiceImpl() { + * ... + * ActivityThread.currentActivityThread().disableUserBirthdayCache(); + * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); + * } + * + * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { + * mUidToBirthday.clear(); + * mUidToBirthday.putAll(uidToBirthday); + * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); + * } + * ... + * } + * </pre> + * + * The call to {@code IpcDataCache.invalidateCache()} guarantees that all clients will re-fetch + * birthdays from binder during consequent calls to + * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock + * held, we maintain consistency between different client views of the birthday state. The use of + * IpcDataCache in this idiomatic way introduces no new race conditions. + * + * IpcDataCache has a few other features for doing things like incremental enhancement of cached + * values and invalidation of multiple caches (that all share the same property key) at once. + * + * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each + * time we update the cache. SELinux configuration must allow everyone to read this property + * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write + * the property. (These properties conventionally begin with the "cache_key." prefix.) + * + * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so + * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In this + * local case, there's no IPC, so use of the cache is (depending on exact circumstance) + * unnecessary. + * + * There may be queries for which it is more efficient to bypass the cache than to cache the + * result. This would be true, for example, if some queries would require frequent cache + * invalidation while other queries require infrequent invalidation. To expand on the birthday + * example, suppose that there is a userId that signifies "the next birthday". When passed this + * userId, the server returns the next birthday among all users - this value changes as time + * advances. The userId value can be cached, but the cache must be invalidated whenever a + * birthday occurs, and this invalidates all birthdays. If there is a large number of users, + * invalidation will happen so often that the cache provides no value. + * + * The class provides a bypass mechanism to handle this situation. + * <pre> + * public class ActivityThread { + * ... + * private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery = + * new IpcDataCache.QueryHandler<Integer, Birthday>() { + * {@literal @}Override + * public Birthday apply(Integer) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * {@literal @}Override + * public boolean shouldBypassQuery(Integer userId) { + * return userId == NEXT_BIRTHDAY; + * } + * }; + * ... + * } + * </pre> + * + * If the {@code shouldBypassQuery()} method returns true then the cache is not used for that + * particular query. The {@code shouldBypassQuery()} method is not abstract and the default + * implementation returns false. + * + * For security, there is a allowlist of processes that are allowed to invalidate a cache. The + * allowlist includes normal runtime processes but does not include test processes. Test + * processes must call {@code IpcDataCache.disableForTestMode()} to disable all cache activity in + * that process. + * + * Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding. + * + * To test a binder cache, create one or more tests that exercise the binder method. This should + * be done twice: once with production code and once with a special image that sets {@code DEBUG} + * and {@code VERIFY} true. In the latter case, verify that no cache inconsistencies are + * reported. If a cache inconsistency is reported, however, it might be a false positive. This + * happens if the server side data can be read and written non-atomically with respect to cache + * invalidation. + * + * @param <Query> The class used to index cache entries: must be hashable and comparable + * @param <Result> The class holding cache entries; use a boxed primitive if possible + * @hide + */ +@TestApi +@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) +public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, Result> { + /** + * {@inheritDoc} + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + public static abstract class QueryHandler<Q,R> + extends PropertyInvalidatedCache.QueryHandler<Q,R> { + /** + * Compute a result given a query. The semantics are those of Functor. + */ + public abstract @Nullable R apply(@NonNull Q query); + + /** + * Return true if a query should not use the cache. The default implementation + * always uses the cache. + */ + public boolean shouldBypassCache(@NonNull Q query) { + return false; + } + }; + + /** + * The module used for unit tests and cts tests. It is expected that no process in + * the system has permissions to write properties with this module. + * @hide + */ + @TestApi + public static final String MODULE_TEST = PropertyInvalidatedCache.MODULE_TEST; + + /** + * The module used for system server/framework caches. This is not visible outside + * the system processes. + * @hide + */ + @TestApi + public static final String MODULE_SYSTEM = PropertyInvalidatedCache.MODULE_SYSTEM; + + /** + * The module used for bluetooth caches. + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + public static final String MODULE_BLUETOOTH = PropertyInvalidatedCache.MODULE_BLUETOOTH; + + /** + * Make a new property invalidated cache. The key is computed from the module and api + * parameters. + * + * @param maxEntries Maximum number of entries to cache; LRU discard + * @param module The module under which the cache key should be placed. + * @param api The api this cache front-ends. The api must be a Java identifier but + * need not be an actual api. + * @param cacheName Name of this cache in debug and dumpsys + * @param computer The code to compute values that are not in the cache. + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + public IpcDataCache(int maxEntries, @NonNull String module, @NonNull String api, + @NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) { + super(maxEntries, module, api, cacheName, computer); + } + + /** + * {@inheritDoc} + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + @Override + public void disableForCurrentProcess() { + super.disableForCurrentProcess(); + } + + /** + * {@inheritDoc} + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + public static void disableForCurrentProcess(@NonNull String cacheName) { + PropertyInvalidatedCache.disableForCurrentProcess(cacheName); + } + + /** + * {@inheritDoc} + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + @Override + public @Nullable Result query(@NonNull Query query) { + return super.query(query); + } + + /** + * {@inheritDoc} + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + @Override + public void invalidateCache() { + super.invalidateCache(); + } + + /** + * Invalidate caches in all processes that are keyed for the module and api. + * @hide + */ + @SystemApi(client=SystemApi.Client.MODULE_LIBRARIES) + @TestApi + public static void invalidateCache(@NonNull String module, @NonNull String api) { + PropertyInvalidatedCache.invalidateCache(module, api); + } +} diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 0ef585478346..cc93adc32541 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -1877,7 +1877,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { float x, float y, float pressure, float size, int metaState, float xPrecision, float yPrecision, int deviceId, int edgeFlags) { return obtain(downTime, eventTime, action, x, y, pressure, size, metaState, - xPrecision, yPrecision, deviceId, edgeFlags, InputDevice.SOURCE_UNKNOWN, + xPrecision, yPrecision, deviceId, edgeFlags, InputDevice.SOURCE_CLASS_POINTER, DEFAULT_DISPLAY); } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 5775944b4e97..ebc409e470e9 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -1104,6 +1104,7 @@ public class ViewConfiguration { * clear, or -1 if Views should not set themselves as preferred to keep clear. * @hide */ + @TestApi public int getPreferKeepClearForFocusDelay() { return mPreferKeepClearForFocusDelay; } diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index 69ad739608b2..fd336a27bb67 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -31,6 +31,7 @@ import android.view.MotionEvent; import android.view.View; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; +import com.android.internal.inputmethod.InputMethodNavButtonFlags; import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.InlineSuggestionsRequestInfo; @@ -105,14 +106,13 @@ public interface InputMethod { * current IME. * @param configChanges {@link InputMethodInfo#getConfigChanges()} declared by IME. * @param stylusHwSupported {@link InputMethodInfo#supportsStylusHandwriting()} declared by IME. - * @param shouldShowImeSwitcherWhenImeIsShown {@code true} If the IME switcher is expected to be - * shown while the IME is shown. + * @param navButtonFlags The initial state of {@link InputMethodNavButtonFlags}. * @hide */ @MainThread default void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privilegedOperations, int configChanges, - boolean stylusHwSupported, boolean shouldShowImeSwitcherWhenImeIsShown) { + boolean stylusHwSupported, @InputMethodNavButtonFlags int navButtonFlags) { attachToken(token); } @@ -231,8 +231,7 @@ public interface InputMethod { * the next {@link #startInput(InputConnection, EditorInfo, IBinder)} as * long as your implementation of {@link InputMethod} relies on such * IPCs - * @param shouldShowImeSwitcherWhenImeIsShown {@code true} If the IME switcher is expected to be - * shown while the IME is shown. + * @param navButtonFlags {@link InputMethodNavButtonFlags} in the initial state of this session. * @see #startInput(InputConnection, EditorInfo) * @see #restartInput(InputConnection, EditorInfo) * @see EditorInfo @@ -241,7 +240,7 @@ public interface InputMethod { @MainThread default void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, boolean restarting, - @NonNull IBinder startInputToken, boolean shouldShowImeSwitcherWhenImeIsShown) { + @NonNull IBinder startInputToken, @InputMethodNavButtonFlags int navButtonFlags) { if (restarting) { restartInput(inputConnection, editorInfo); } else { @@ -250,15 +249,13 @@ public interface InputMethod { } /** - * Notifies that whether the IME should show the IME switcher or not is being changed. + * Notifies that {@link InputMethodNavButtonFlags} have been updated. * - * @param shouldShowImeSwitcherWhenImeIsShown {@code true} If the IME switcher is expected to be - * shown while the IME is shown. + * @param navButtonFlags The new {@link InputMethodNavButtonFlags}. * @hide */ @MainThread - default void onShouldShowImeSwitcherWhenImeIsShownChanged( - boolean shouldShowImeSwitcherWhenImeIsShown) { + default void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { } /** diff --git a/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java b/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java new file mode 100644 index 000000000000..1b37c6cbe5ae --- /dev/null +++ b/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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.internal.inputmethod; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; + +/** + * A set of flags notified from {@link com.android.server.inputmethod.InputMethodManagerService} to + * {@link android.inputmethodservice.InputMethodService} regarding how + * {@link android.inputmethodservice.NavigationBarController} should behave. + * + * <p>These flags will take effect when and only when + * {@link android.inputmethodservice.InputMethodService#canImeRenderGesturalNavButtons} returns + * {@code true}.</p> + */ +@Retention(SOURCE) +@IntDef(flag = true, value = { + InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN, +}) +public @interface InputMethodNavButtonFlags { + /** + * When set, the IME switcher icon needs to be shown on the navigation bar. + */ + int SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN = 1; +} diff --git a/core/java/com/android/internal/net/VpnConfig.java b/core/java/com/android/internal/net/VpnConfig.java index ec95baae8c19..2784da00a02e 100644 --- a/core/java/com/android/internal/net/VpnConfig.java +++ b/core/java/com/android/internal/net/VpnConfig.java @@ -106,6 +106,7 @@ public class VpnConfig implements Parcelable { public boolean allowIPv6; public boolean isMetered = true; public boolean requiresInternetValidation = false; + public boolean excludeLocalRoutes = false; public Network[] underlyingNetworks; public ProxyInfo proxyInfo; @@ -133,6 +134,7 @@ public class VpnConfig implements Parcelable { allowIPv6 = other.allowIPv6; isMetered = other.isMetered; requiresInternetValidation = other.requiresInternetValidation; + excludeLocalRoutes = other.excludeLocalRoutes; underlyingNetworks = other.underlyingNetworks != null ? Arrays.copyOf( other.underlyingNetworks, other.underlyingNetworks.length) : null; proxyInfo = other.proxyInfo; @@ -192,6 +194,7 @@ public class VpnConfig implements Parcelable { out.writeInt(allowIPv6 ? 1 : 0); out.writeInt(isMetered ? 1 : 0); out.writeInt(requiresInternetValidation ? 1 : 0); + out.writeInt(excludeLocalRoutes ? 1 : 0); out.writeTypedArray(underlyingNetworks, flags); out.writeParcelable(proxyInfo, flags); } @@ -220,6 +223,7 @@ public class VpnConfig implements Parcelable { config.allowIPv6 = in.readInt() != 0; config.isMetered = in.readInt() != 0; config.requiresInternetValidation = in.readInt() != 0; + config.excludeLocalRoutes = in.readInt() != 0; config.underlyingNetworks = in.createTypedArray(Network.CREATOR); config.proxyInfo = in.readParcelable(null, android.net.ProxyInfo.class); return config; @@ -253,7 +257,8 @@ public class VpnConfig implements Parcelable { .append(", allowIPv4=").append(allowIPv4) .append(", allowIPv6=").append(allowIPv6) .append(", isMetered=").append(isMetered) - .append(", requiresInternetValidation").append(requiresInternetValidation) + .append(", requiresInternetValidation=").append(requiresInternetValidation) + .append(", excludeLocalRoutes=").append(excludeLocalRoutes) .append(", underlyingNetworks=").append(Arrays.toString(underlyingNetworks)) .append(", proxyInfo=").append(proxyInfo) .append("}") diff --git a/core/java/com/android/internal/policy/TransitionAnimation.java b/core/java/com/android/internal/policy/TransitionAnimation.java index 37c96e71a0a8..7ee1f17b34df 100644 --- a/core/java/com/android/internal/policy/TransitionAnimation.java +++ b/core/java/com/android/internal/policy/TransitionAnimation.java @@ -312,9 +312,9 @@ public class TransitionAnimation { /** Load animation by attribute Id from android package. */ @Nullable - public Animation loadDefaultAnimationAttr(int animAttr) { + public Animation loadDefaultAnimationAttr(int animAttr, boolean translucent) { return loadAnimationAttr(DEFAULT_PACKAGE, mDefaultWindowAnimationStyleResId, animAttr, - false /* translucent */); + translucent); } @Nullable diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 9163b6d6215e..1d60c501a88b 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -70,7 +70,7 @@ interface IStatusBarService void onPanelRevealed(boolean clearNotificationEffects, int numItems); void onPanelHidden(); // Mark current notifications as "seen" and stop ringing, vibrating, blinking. - void clearNotificationEffects(); + oneway void clearNotificationEffects(); void onNotificationClick(String key, in NotificationVisibility nv); void onNotificationActionClick(String key, int actionIndex, in Notification.Action action, in NotificationVisibility nv, boolean generatedByAssistant); void onNotificationError(String pkg, String tag, int id, diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl index d2bc3442ed36..273c5f170812 100644 --- a/core/java/com/android/internal/view/IInputMethod.aidl +++ b/core/java/com/android/internal/view/IInputMethod.aidl @@ -37,8 +37,7 @@ import com.android.internal.view.InlineSuggestionsRequestInfo; */ oneway interface IInputMethod { void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privOps, - int configChanges, boolean stylusHwSupported, - boolean shouldShowImeSwitcherWhenImeIsShown); + int configChanges, boolean stylusHwSupported, int navigationBarFlags); void onCreateInlineSuggestionsRequest(in InlineSuggestionsRequestInfo requestInfo, in IInlineSuggestionsRequestCallback cb); @@ -48,10 +47,9 @@ oneway interface IInputMethod { void unbindInput(); void startInput(in IBinder startInputToken, in IInputContext inputContext, - in EditorInfo attribute, boolean restarting, - boolean shouldShowImeSwitcherWhenImeIsShown); + in EditorInfo attribute, boolean restarting, int navigationBarFlags); - void onShouldShowImeSwitcherWhenImeIsShownChanged(boolean shouldShowImeSwitcherWhenImeIsShown); + void onNavButtonFlagsChanged(int navButtonFlags); void createSession(in InputChannel channel, IInputSessionCallback callback); diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index 248db76da71d..0c05da551c8f 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -45,7 +45,7 @@ using android::zygote::ZygoteFailure; // WARNING: Knows a little about the wire protocol used to communicate with Zygote. // TODO: Fix error handling. -constexpr size_t MAX_COMMAND_BYTES = 12200; +constexpr size_t MAX_COMMAND_BYTES = 32768; constexpr size_t NICE_NAME_BYTES = 50; // A buffer optionally bundled with a file descriptor from which we can fill it. @@ -273,8 +273,6 @@ class NativeCommandBuffer { char mBuffer[MAX_COMMAND_BYTES]; }; -static_assert(sizeof(NativeCommandBuffer) < 3 * 4096); - static int buffersAllocd(0); // Get a new NativeCommandBuffer. Can only be called once between freeNativeBuffer calls, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 434222ae8016..fa9eba34d30b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2937,7 +2937,7 @@ <!-- @SystemApi @hide Allows an application to set the profile owners and the device owner. This permission is not available to third party applications.--> <permission android:name="android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS" - android:protectionLevel="signature|role|setup" + android:protectionLevel="signature|role" android:label="@string/permlab_manageProfileAndDeviceOwners" android:description="@string/permdesc_manageProfileAndDeviceOwners" /> @@ -2946,6 +2946,10 @@ <permission android:name="android.permission.QUERY_ADMIN_POLICY" android:protectionLevel="signature|role" /> + <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.--> + <permission android:name="android.permission.PROVISION_DEMO_DEVICE" + android:protectionLevel="signature|setup" /> + <!-- @TestApi @hide Allows an application to reset the record of previous system update freeze periods. --> <permission android:name="android.permission.CLEAR_FREEZE_PERIOD" @@ -3714,15 +3718,26 @@ android:protectionLevel="signature|privileged" /> <!-- ========================================= --> - <!-- Permissions for SupplementalApi --> + <!-- Permissions for AdServices --> <!-- ========================================= --> <eat-comment /> - <!-- TODO(b/213488783): Update with correct names. --> - <!-- Allows an application to access SupplementalApis. --> - <permission android:name="android.permission.ACCESS_SUPPLEMENTAL_APIS" - android:label="@string/permlab_accessSupplementalApi" - android:description="@string/permdesc_accessSupplementalApi" + <!-- Allows an application to access AdServices Topics API. --> + <permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" + android:label="@string/permlab_accessAdServicesTopics" + android:description="@string/permdesc_accessAdServicesTopics" + android:protectionLevel="normal" /> + + <!-- Allows an application to access AdServices Attribution APIs. --> + <permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" + android:label="@string/permlab_accessAdServicesAttribution" + android:description="@string/permdesc_accessAdServicesAttribution" + android:protectionLevel="normal" /> + + <!-- Allows an application to access AdServices Custom Audiences APIs. --> + <permission android:name="android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCES" + android:label="@string/permlab_accessAdServicesCustomAudiences" + android:description="@string/permdesc_accessAdServicesCustomAudiences" android:protectionLevel="normal" /> <!-- ==================================== --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index e187f6e07856..879fc9e6ceb7 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5267,9 +5267,9 @@ <bool name="config_cecPowerStateChangeOnActiveSourceLost_userConfigurable">true</bool> <bool name="config_cecPowerStateChangeOnActiveSourceLostNone_allowed">true</bool> - <bool name="config_cecPowerStateChangeOnActiveSourceLostNone_default">true</bool> + <bool name="config_cecPowerStateChangeOnActiveSourceLostNone_default">false</bool> <bool name="config_cecPowerStateChangeOnActiveSourceLostStandbyNow_allowed">true</bool> - <bool name="config_cecPowerStateChangeOnActiveSourceLostStandbyNow_default">false</bool> + <bool name="config_cecPowerStateChangeOnActiveSourceLostStandbyNow_default">true</bool> <bool name="config_cecSystemAudioControl_userConfigurable">true</bool> <bool name="config_cecSystemAudioControlEnabled_allowed">true</bool> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index c8202c5989b5..05ebef9bf56f 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -4095,10 +4095,20 @@ <!-- Description of an application permission that lets it query all other packages. [CHAR LIMIT=NONE] --> <string name="permdesc_queryAllPackages">Allows an app to see all installed packages.</string> - <!-- Title of an application permission that lets it access SupplementalApis. [CHAR LIMIT=NONE] --> - <string name="permlab_accessSupplementalApi">access SupplementalApis</string> - <!-- Description of an application permission that lets it access SupplementalApis. [CHAR LIMIT=NONE]--> - <string name="permdesc_accessSupplementalApi">Allows an application to access SupplementalApis.</string> + <!-- Title of an application permission that lets it access AdServices Topics API. [CHAR LIMIT=NONE] --> + <string name="permlab_accessAdServicesTopics">access AdServices Topics API</string> + <!-- Description of an application permission that lets it access AdServices Topics API. [CHAR LIMIT=NONE]--> + <string name="permdesc_accessAdServicesTopics">Allows an application to access AdServices Topics API.</string> + + <!-- Title of an application permission that lets it access AdServices Attribution APIs. [CHAR LIMIT=NONE] --> + <string name="permlab_accessAdServicesAttribution">access AdServices Attribution APIs</string> + <!-- Description of an application permission that lets it access AdServices Attribution APIs. [CHAR LIMIT=NONE]--> + <string name="permdesc_accessAdServicesAttribution">Allows an application to access AdServices Attribution APIs.</string> + + <!-- Title of an application permission that lets it access AdServices Custom Audiences API. [CHAR LIMIT=NONE] --> + <string name="permlab_accessAdServicesCustomAudiences">access AdServices Custom Audiences API</string> + <!-- Description of an application permission that lets it access AdServices Custom Audiences API. [CHAR LIMIT=NONE]--> + <string name="permdesc_accessAdServicesCustomAudiences">Allows an application to access AdServices Custom Audiences API.</string> <!-- Shown in the tutorial for tap twice for zoom control. --> <string name="tutorial_double_tap_to_zoom_message_short">Tap twice for zoom control</string> diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java new file mode 100644 index 000000000000..fa7d7214d289 --- /dev/null +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2022 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.os; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Test; + +/** + * Test for verifying the behavior of {@link IpcDataCache}. This test does + * not use any actual binder calls - it is entirely self-contained. This test also relies + * on the test mode of {@link IpcDataCache} because Android SELinux rules do + * not grant test processes the permission to set system properties. + * <p> + * Build/Install/Run: + * atest FrameworksCoreTests:IpcDataCacheTest + */ +@SmallTest +public class IpcDataCacheTest { + + // Configuration for creating caches + private static final String MODULE = IpcDataCache.MODULE_TEST; + private static final String API = "testApi"; + + // This class is a proxy for binder calls. It contains a counter that increments + // every time the class is queried. + private static class ServerProxy { + // The number of times this class was queried. + private int mCount = 0; + + // A single query. The key behavior is that the query count is incremented. + boolean query(int x) { + mCount++; + return value(x); + } + + // Return the expected value of an input, without incrementing the query count. + boolean value(int x) { + return x % 3 == 0; + } + + // Verify the count. + void verify(int x) { + assertEquals(x, mCount); + } + } + + // The functions for querying the server. + private static class ServerQuery + extends IpcDataCache.QueryHandler<Integer, Boolean> { + private final ServerProxy mServer; + + ServerQuery(ServerProxy server) { + mServer = server; + } + + @Override + 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. + @After + public void tearDown() throws Exception { + IpcDataCache.setTestMode(false); + } + + // This test is disabled pending an sepolicy change that allows any app to set the + // test property. + @Test + public void testBasicCache() { + + // A stand-in for the binder. The test verifies that calls are passed through to + // this class properly. + ServerProxy tester = new ServerProxy(); + + // Create a cache that uses simple arithmetic to computer its values. + IpcDataCache<Integer, Boolean> testCache = + 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); + assertEquals(tester.value(3), testCache.query(3)); + tester.verify(2); + testCache.invalidateCache(); + assertEquals(tester.value(3), testCache.query(3)); + tester.verify(3); + assertEquals(tester.value(5), testCache.query(5)); + tester.verify(4); + assertEquals(tester.value(5), testCache.query(5)); + tester.verify(4); + assertEquals(tester.value(3), testCache.query(3)); + tester.verify(4); + + // Invalidate the cache, and verify that the next read on 3 goes to the server. + testCache.invalidateCache(); + assertEquals(tester.value(3), testCache.query(3)); + tester.verify(5); + + // Test bypass. The query for 13 always bypasses the cache. + assertEquals(tester.value(12), testCache.query(12)); + assertEquals(tester.value(13), testCache.query(13)); + assertEquals(tester.value(14), testCache.query(14)); + tester.verify(8); + assertEquals(tester.value(12), testCache.query(12)); + assertEquals(tester.value(13), testCache.query(13)); + assertEquals(tester.value(14), testCache.query(14)); + tester.verify(9); + } + + @Test + public void testDisableCache() { + + // A stand-in for the binder. The test verifies that calls are passed through to + // this class properly. + ServerProxy tester = new ServerProxy(); + + // Three caches, all using the same system property but one uses a different name. + IpcDataCache<Integer, Boolean> cache1 = + new IpcDataCache<>(4, MODULE, API, "cacheA", + new ServerQuery(tester)); + IpcDataCache<Integer, Boolean> cache2 = + new IpcDataCache<>(4, MODULE, API, "cacheA", + new ServerQuery(tester)); + IpcDataCache<Integer, Boolean> cache3 = + new IpcDataCache<>(4, MODULE, API, "cacheB", + new ServerQuery(tester)); + + // Caches are enabled upon creation. + assertEquals(false, cache1.getDisabledState()); + assertEquals(false, cache2.getDisabledState()); + assertEquals(false, cache3.getDisabledState()); + + // Disable the cache1 instance. Only cache1 is disabled + cache1.disableInstance(); + assertEquals(true, cache1.getDisabledState()); + assertEquals(false, cache2.getDisabledState()); + assertEquals(false, cache3.getDisabledState()); + + // Disable cache1. This will disable cache1 and cache2 because they share the + // same name. cache3 has a different name and will not be disabled. + cache1.disableForCurrentProcess(); + assertEquals(true, cache1.getDisabledState()); + assertEquals(true, cache2.getDisabledState()); + assertEquals(false, cache3.getDisabledState()); + + // Create a new cache1. Verify that the new instance is disabled. + cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA", + new ServerQuery(tester)); + assertEquals(true, cache1.getDisabledState()); + + // Remove the record of caches being locally disabled. This is a clean-up step. + cache1.forgetDisableLocal(); + assertEquals(true, cache1.getDisabledState()); + assertEquals(true, cache2.getDisabledState()); + assertEquals(false, cache3.getDisabledState()); + + // Create a new cache1. Verify that the new instance is not disabled. + cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA", + new ServerQuery(tester)); + assertEquals(false, cache1.getDisabledState()); + } + + private static class TestQuery + extends IpcDataCache.QueryHandler<Integer, String> { + + private int mRecomputeCount = 0; + + @Override + public String apply(Integer qv) { + mRecomputeCount += 1; + return "foo" + qv.toString(); + } + + int getRecomputeCount() { + return mRecomputeCount; + } + } + + private static class TestCache extends IpcDataCache<Integer, String> { + private final TestQuery mQuery; + + TestCache() { + this(MODULE, API); + } + + TestCache(String module, String api) { + this(module, api, new TestQuery()); + } + + TestCache(String module, String api, TestQuery query) { + super(4, module, api, "testCache7", query); + mQuery = query; + setTestMode(true); + testPropertyName(); + } + + int getRecomputeCount() { + return mQuery.getRecomputeCount(); + } + } + + @Test + public void testCacheRecompute() { + TestCache cache = new TestCache(); + cache.invalidateCache(); + assertEquals(cache.isDisabled(), false); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo6", cache.query(6)); + assertEquals(2, cache.getRecomputeCount()); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + // Invalidate the cache with a direct call to the property. + IpcDataCache.invalidateCache(MODULE, API); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(4, cache.getRecomputeCount()); + } + + @Test + public void testCacheInitialState() { + TestCache cache = new TestCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + } + + @Test + public void testCachePropertyUnset() { + final String UNSET_API = "otherApi"; + TestCache cache = new TestCache(MODULE, UNSET_API); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + } + + @Test + public void testCacheDisableState() { + TestCache cache = new TestCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(2, cache.getRecomputeCount()); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + cache.disableSystemWide(); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(5, cache.getRecomputeCount()); + cache.invalidateCache(); // Should not reenable + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(7, cache.getRecomputeCount()); + } + + @Test + public void testLocalProcessDisable() { + TestCache cache = new TestCache(); + assertEquals(cache.isDisabled(), false); + cache.invalidateCache(); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals("foo5", cache.query(5)); + assertEquals(1, cache.getRecomputeCount()); + assertEquals(cache.isDisabled(), false); + cache.disableForCurrentProcess(); + assertEquals(cache.isDisabled(), true); + assertEquals("foo5", cache.query(5)); + assertEquals("foo5", cache.query(5)); + assertEquals(3, cache.getRecomputeCount()); + } +} diff --git a/core/tests/coretests/src/android/view/MotionEventTest.java b/core/tests/coretests/src/android/view/MotionEventTest.java index 78a8f7b3f32e..c4c983d24af9 100644 --- a/core/tests/coretests/src/android/view/MotionEventTest.java +++ b/core/tests/coretests/src/android/view/MotionEventTest.java @@ -16,6 +16,7 @@ package android.view; +import static android.view.InputDevice.SOURCE_CLASS_POINTER; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_POINTER_DOWN; import static android.view.MotionEvent.TOOL_TYPE_FINGER; @@ -214,4 +215,27 @@ public class MotionEventTest { rotInvalid.transform(mat); assertEquals(-1, rotInvalid.getSurfaceRotation()); } + + @Test + public void testUsesPointerSourceByDefault() { + final MotionEvent event = MotionEvent.obtain(0 /* downTime */, 0 /* eventTime */, + ACTION_DOWN, 0 /* x */, 0 /* y */, 0 /* metaState */); + assertTrue(event.isFromSource(SOURCE_CLASS_POINTER)); + } + + @Test + public void testLocationOffsetOnlyAppliedToNonPointerSources() { + final MotionEvent event = MotionEvent.obtain(0 /* downTime */, 0 /* eventTime */, + ACTION_DOWN, 10 /* x */, 20 /* y */, 0 /* metaState */); + event.offsetLocation(40, 50); + + // The offset should be applied since a pointer source is used by default. + assertEquals(50, (int) event.getX()); + assertEquals(70, (int) event.getY()); + + // The offset should not be applied if the source is changed to a non-pointer source. + event.setSource(InputDevice.SOURCE_JOYSTICK); + assertEquals(10, (int) event.getX()); + assertEquals(20, (int) event.getY()); + } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 3c64cf55eb50..deffa3a6fea6 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -237,27 +237,6 @@ applications that come with the platform <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/> </privapp-permissions> - <privapp-permissions package="com.android.providers.media.module"> - <permission name="android.permission.INTERACT_ACROSS_USERS"/> - <permission name="android.permission.MANAGE_USERS"/> - <permission name="android.permission.USE_RESERVED_DISK"/> - <permission name="android.permission.WRITE_MEDIA_STORAGE"/> - <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/> - <permission name="android.permission.WATCH_APPOPS"/> - <permission name="android.permission.UPDATE_APP_OPS_STATS"/> - <permission name="android.permission.UPDATE_DEVICE_STATS"/> - <!-- Permissions required for reading and logging compat changes --> - <permission name="android.permission.LOG_COMPAT_CHANGE" /> - <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> - <permission name="android.permission.REGISTER_STATS_PULL_ATOM" /> - <!-- Permissions required for reading DeviceConfig --> - <permission name="android.permission.READ_DEVICE_CONFIG" /> - <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/> - <permission name="android.permission.MODIFY_QUIET_MODE"/> - <!-- Permissions required to check if an app is in the foreground or not during IO --> - <permission name="android.permission.PACKAGE_USAGE_STATS"/> - </privapp-permissions> - <privapp-permissions package="com.android.providers.telephony"> <permission name="android.permission.INTERACT_ACROSS_USERS"/> <permission name="android.permission.MODIFY_PHONE_STATE"/> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonDisplayFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java index eb9429747b66..1c49881904e4 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/CommonDisplayFeature.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/CommonFoldingFeature.java @@ -18,17 +18,73 @@ package androidx.window.common; import static androidx.window.util.ExtensionHelper.isZero; +import android.annotation.IntDef; import android.annotation.Nullable; import android.graphics.Rect; +import android.util.Log; import androidx.annotation.NonNull; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** Wrapper for both Extension and Sidecar versions of DisplayFeature. */ -final class CommonDisplayFeature implements DisplayFeature { +/** A representation of a folding feature for both Extension and Sidecar. + * For Sidecar this is the same as combining {@link androidx.window.sidecar.SidecarDeviceState} and + * {@link androidx.window.sidecar.SidecarDisplayFeature}. For Extensions this is the mirror of + * {@link androidx.window.extensions.layout.FoldingFeature}. + */ +public final class CommonFoldingFeature { + + private static final boolean DEBUG = false; + + public static final String TAG = CommonFoldingFeature.class.getSimpleName(); + + /** + * A common type to represent a hinge where the screen is continuous. + */ + public static final int COMMON_TYPE_FOLD = 1; + + /** + * A common type to represent a hinge where there is a physical gap separating multiple + * displays. + */ + public static final int COMMON_TYPE_HINGE = 2; + + @IntDef({COMMON_TYPE_FOLD, COMMON_TYPE_HINGE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Type { + } + + /** + * A common state to represent when the state is not known. One example is if the device is + * closed. We do not emit this value for developers but is useful for implementation reasons. + */ + public static final int COMMON_STATE_UNKNOWN = -1; + + /** + * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar + * and Extensions do not match exactly. + */ + public static final int COMMON_STATE_FLAT = 3; + /** + * A common state to represent a HALF_OPENED hinge. This is needed because the definitions in + * Sidecar and Extensions do not match exactly. + */ + public static final int COMMON_STATE_HALF_OPENED = 2; + + /** + * The possible states for a folding hinge. + */ + @IntDef({COMMON_STATE_UNKNOWN, COMMON_STATE_FLAT, COMMON_STATE_HALF_OPENED}) + @Retention(RetentionPolicy.SOURCE) + public @interface State { + } + private static final Pattern FEATURE_PATTERN = Pattern.compile("([a-z]+)-\\[(\\d+),(\\d+),(\\d+),(\\d+)]-?(flat|half-opened)?"); @@ -38,17 +94,49 @@ final class CommonDisplayFeature implements DisplayFeature { private static final String PATTERN_STATE_FLAT = "flat"; private static final String PATTERN_STATE_HALF_OPENED = "half-opened"; - // TODO(b/183049815): Support feature strings that include the state of the feature. + /** + * Parse a {@link List} of {@link CommonFoldingFeature} from a {@link String}. + * @param value a {@link String} representation of multiple {@link CommonFoldingFeature} + * separated by a ":". + * @param hingeState a global fallback value for a {@link CommonFoldingFeature} if one is not + * specified in the input. + * @throws IllegalArgumentException if the provided string is improperly formatted or could not + * otherwise be parsed. + * @see #FEATURE_PATTERN + * @return {@link List} of {@link CommonFoldingFeature}. + */ + static List<CommonFoldingFeature> parseListFromString(@NonNull String value, + @State int hingeState) { + List<CommonFoldingFeature> features = new ArrayList<>(); + String[] featureStrings = value.split(";"); + for (String featureString : featureStrings) { + CommonFoldingFeature feature; + try { + feature = CommonFoldingFeature.parseFromString(featureString, hingeState); + } catch (IllegalArgumentException e) { + if (DEBUG) { + Log.w(TAG, "Failed to parse display feature: " + featureString, e); + } + continue; + } + features.add(feature); + } + return features; + } /** * Parses a display feature from a string. * + * @param string A {@link String} representation of a {@link CommonFoldingFeature}. + * @param hingeState A fallback value for the {@link State} if it is not specified in the input. * @throws IllegalArgumentException if the provided string is improperly formatted or could not * otherwise be parsed. + * @return {@link CommonFoldingFeature} represented by the {@link String} value. * @see #FEATURE_PATTERN */ @NonNull - static CommonDisplayFeature parseFromString(@NonNull String string) { + private static CommonFoldingFeature parseFromString(@NonNull String string, + @State int hingeState) { Matcher featureMatcher = FEATURE_PATTERN.matcher(string); if (!featureMatcher.matches()) { throw new IllegalArgumentException("Malformed feature description format: " + string); @@ -59,10 +147,10 @@ final class CommonDisplayFeature implements DisplayFeature { int type; switch (featureType) { case FEATURE_TYPE_FOLD: - type = 1 /* TYPE_FOLD */; + type = COMMON_TYPE_FOLD; break; case FEATURE_TYPE_HINGE: - type = 2 /* TYPE_HINGE */; + type = COMMON_TYPE_HINGE; break; default: { throw new IllegalArgumentException("Malformed feature type: " + featureType); @@ -79,7 +167,7 @@ final class CommonDisplayFeature implements DisplayFeature { } String stateString = featureMatcher.group(6); stateString = stateString == null ? "" : stateString; - Integer state; + final int state; switch (stateString) { case PATTERN_STATE_FLAT: state = COMMON_STATE_FLAT; @@ -88,10 +176,10 @@ final class CommonDisplayFeature implements DisplayFeature { state = COMMON_STATE_HALF_OPENED; break; default: - state = null; + state = hingeState; break; } - return new CommonDisplayFeature(type, state, featureRect); + return new CommonFoldingFeature(type, state, featureRect); } catch (NumberFormatException e) { throw new IllegalArgumentException("Malformed feature description: " + string, e); } @@ -99,11 +187,11 @@ final class CommonDisplayFeature implements DisplayFeature { private final int mType; @Nullable - private final Integer mState; + private final int mState; @NonNull private final Rect mRect; - CommonDisplayFeature(int type, @Nullable Integer state, @NonNull Rect rect) { + CommonFoldingFeature(int type, int state, @NonNull Rect rect) { assertValidState(state); this.mType = type; this.mState = state; @@ -114,16 +202,19 @@ final class CommonDisplayFeature implements DisplayFeature { this.mRect = rect; } + /** Returns the type of the feature. */ + @Type public int getType() { return mType; } - /** Returns the state of the feature, or {@code null} if the feature has no state. */ - @Nullable - public Integer getState() { + /** Returns the state of the feature.*/ + @State + public int getState() { return mState; } + /** Returns the bounds of the feature. */ @NonNull public Rect getRect() { return mRect; @@ -133,7 +224,7 @@ final class CommonDisplayFeature implements DisplayFeature { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - CommonDisplayFeature that = (CommonDisplayFeature) o; + CommonFoldingFeature that = (CommonFoldingFeature) o; return mType == that.mType && Objects.equals(mState, that.mState) && mRect.equals(that.mRect); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerPostureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java index fa9a5a8b7a1b..6987401525b4 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerPostureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java @@ -18,11 +18,15 @@ package androidx.window.common; import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE; +import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; +import static androidx.window.common.CommonFoldingFeature.parseListFromString; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; +import android.text.TextUtils; import android.util.Log; import android.util.SparseIntArray; @@ -30,6 +34,7 @@ import androidx.window.util.BaseDataProducer; import com.android.internal.R; +import java.util.List; import java.util.Optional; /** @@ -37,10 +42,13 @@ import java.util.Optional; * by mapping the state returned from {@link DeviceStateManager} to values provided in the resources * config at {@link R.array#config_device_state_postures}. */ -public final class DeviceStateManagerPostureProducer extends BaseDataProducer<Integer> { - private static final String TAG = "ConfigDevicePostureProducer"; +public final class DeviceStateManagerFoldingFeatureProducer extends + BaseDataProducer<List<CommonFoldingFeature>> { + private static final String TAG = + DeviceStateManagerFoldingFeatureProducer.class.getSimpleName(); private static final boolean DEBUG = false; + private final Context mContext; private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray(); private int mCurrentDeviceState = INVALID_DEVICE_STATE; @@ -50,7 +58,8 @@ public final class DeviceStateManagerPostureProducer extends BaseDataProducer<In notifyDataChanged(); }; - public DeviceStateManagerPostureProducer(@NonNull Context context) { + public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context) { + mContext = context; String[] deviceStatePosturePairs = context.getResources() .getStringArray(R.array.config_device_state_postures); for (String deviceStatePosturePair : deviceStatePosturePairs) { @@ -86,8 +95,17 @@ public final class DeviceStateManagerPostureProducer extends BaseDataProducer<In @Override @Nullable - public Optional<Integer> getData() { - final int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, -1); - return posture != -1 ? Optional.of(posture) : Optional.empty(); + public Optional<List<CommonFoldingFeature>> getData() { + final int globalHingeState = globalHingeState(); + String displayFeaturesString = mContext.getResources().getString( + R.string.config_display_features); + if (TextUtils.isEmpty(displayFeaturesString)) { + return Optional.empty(); + } + return Optional.of(parseListFromString(displayFeaturesString, globalHingeState)); + } + + private int globalHingeState() { + return mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN); } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DisplayFeature.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DisplayFeature.java deleted file mode 100644 index 573641857b99..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/DisplayFeature.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2021 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 androidx.window.common; - -import android.annotation.IntDef; -import android.annotation.Nullable; -import android.graphics.Rect; - -import androidx.annotation.NonNull; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** Wrapper for both Extension and Sidecar versions of DisplayFeature. */ -public interface DisplayFeature { - /** Returns the type of the feature. */ - int getType(); - - /** Returns the state of the feature, or {@code null} if the feature has no state. */ - @Nullable - @State - Integer getState(); - - /** Returns the bounds of the feature. */ - @NonNull - Rect getRect(); - - /** - * A common state to represent a FLAT hinge. This is needed because the definitions in Sidecar - * and Extensions do not match exactly. - */ - int COMMON_STATE_FLAT = 3; - /** - * A common state to represent a HALF_OPENED hinge. This is needed because the definitions in - * Sidecar and Extensions do not match exactly. - */ - int COMMON_STATE_HALF_OPENED = 2; - - /** - * The possible states for a folding hinge. - */ - @IntDef({COMMON_STATE_FLAT, COMMON_STATE_HALF_OPENED}) - @Retention(RetentionPolicy.SOURCE) - @interface State {} - -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java new file mode 100644 index 000000000000..d923a46c3b5d --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 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 androidx.window.common; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + +/** + * An empty implementation of {@link Application.ActivityLifecycleCallbacks} derived classes can + * implement the methods necessary. + */ +public class EmptyLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityResumed(Activity activity) { + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/ResourceConfigDisplayFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/ResourceConfigDisplayFeatureProducer.java deleted file mode 100644 index cd2cadc082e1..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/ResourceConfigDisplayFeatureProducer.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2021 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 androidx.window.common; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; - -import androidx.window.util.BaseDataProducer; - -import com.android.internal.R; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * Implementation of {@link androidx.window.util.DataProducer} that produces - * {@link CommonDisplayFeature} parsed from a string stored in the resources config at - * {@link R.string#config_display_features}. - */ -public final class ResourceConfigDisplayFeatureProducer extends - BaseDataProducer<List<DisplayFeature>> { - private static final boolean DEBUG = false; - private static final String TAG = "ResourceConfigDisplayFeatureProducer"; - - private final Context mContext; - - public ResourceConfigDisplayFeatureProducer(@NonNull Context context) { - mContext = context; - } - - @Override - @Nullable - public Optional<List<DisplayFeature>> getData() { - String displayFeaturesString = mContext.getResources().getString( - R.string.config_display_features); - if (TextUtils.isEmpty(displayFeaturesString)) { - return Optional.empty(); - } - - List<DisplayFeature> features = new ArrayList<>(); - String[] featureStrings = displayFeaturesString.split(";"); - for (String featureString : featureStrings) { - CommonDisplayFeature feature; - try { - feature = CommonDisplayFeature.parseFromString(featureString); - } catch (IllegalArgumentException e) { - if (DEBUG) { - Log.w(TAG, "Failed to parse display feature: " + featureString, e); - } - continue; - } - features.add(feature); - } - return Optional.of(features); - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDevicePostureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDevicePostureProducer.java deleted file mode 100644 index 2026df3fa979..000000000000 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDevicePostureProducer.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2021 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 androidx.window.common; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ContentResolver; -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; - -import androidx.window.util.BaseDataProducer; - -import java.util.Optional; - -/** - * Implementation of {@link androidx.window.util.DataProducer} that provides the device posture - * as an {@link Integer} from a value stored in {@link Settings}. - */ -public final class SettingsDevicePostureProducer extends BaseDataProducer<Integer> { - private static final String DEVICE_POSTURE = "device_posture"; - - private final Uri mDevicePostureUri = - Settings.Global.getUriFor(DEVICE_POSTURE); - - private final ContentResolver mResolver; - private final ContentObserver mObserver; - private boolean mRegisteredObservers; - - public SettingsDevicePostureProducer(@NonNull Context context) { - mResolver = context.getContentResolver(); - mObserver = new SettingsObserver(); - } - - @Override - @Nullable - public Optional<Integer> getData() { - int posture = Settings.Global.getInt(mResolver, DEVICE_POSTURE, -1); - return posture == -1 ? Optional.empty() : Optional.of(posture); - } - - /** - * Registers settings observers, if needed. When settings observers are registered for this - * producer callbacks for changes in data will be triggered. - */ - public void registerObserversIfNeeded() { - if (mRegisteredObservers) { - return; - } - mRegisteredObservers = true; - mResolver.registerContentObserver(mDevicePostureUri, false /* notifyForDescendants */, - mObserver /* ContentObserver */); - } - - /** - * Unregisters settings observers, if needed. When settings observers are unregistered for this - * producer callbacks for changes in data will not be triggered. - */ - public void unregisterObserversIfNeeded() { - if (!mRegisteredObservers) { - return; - } - mRegisteredObservers = false; - mResolver.unregisterContentObserver(mObserver); - } - - private final class SettingsObserver extends ContentObserver { - SettingsObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - if (mDevicePostureUri.equals(uri)) { - notifyDataChanged(); - } - } - } -} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDisplayFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDisplayFeatureProducer.java index 040662657a74..e9d213e06fa9 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDisplayFeatureProducer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/SettingsDisplayFeatureProducer.java @@ -16,8 +16,10 @@ package androidx.window.common; +import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN; +import static androidx.window.common.CommonFoldingFeature.parseListFromString; + import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -26,22 +28,19 @@ import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.text.TextUtils; -import android.util.Log; import androidx.window.util.BaseDataProducer; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; /** * Implementation of {@link androidx.window.util.DataProducer} that produces - * {@link CommonDisplayFeature} parsed from a string stored in {@link Settings}. + * {@link CommonFoldingFeature} parsed from a string stored in {@link Settings}. */ public final class SettingsDisplayFeatureProducer - extends BaseDataProducer<List<DisplayFeature>> { - private static final boolean DEBUG = false; - private static final String TAG = "SettingsDisplayFeatureProducer"; + extends BaseDataProducer<List<CommonFoldingFeature>> { private static final String DISPLAY_FEATURES = "display_features"; private final Uri mDisplayFeaturesUri = @@ -57,32 +56,17 @@ public final class SettingsDisplayFeatureProducer } @Override - @Nullable - public Optional<List<DisplayFeature>> getData() { + @NonNull + public Optional<List<CommonFoldingFeature>> getData() { String displayFeaturesString = Settings.Global.getString(mResolver, DISPLAY_FEATURES); if (displayFeaturesString == null) { return Optional.empty(); } - List<DisplayFeature> features = new ArrayList<>(); if (TextUtils.isEmpty(displayFeaturesString)) { - return Optional.of(features); - } - String[] featureStrings = displayFeaturesString.split(";"); - - for (String featureString : featureStrings) { - CommonDisplayFeature feature; - try { - feature = CommonDisplayFeature.parseFromString(featureString); - } catch (IllegalArgumentException e) { - if (DEBUG) { - Log.w(TAG, "Failed to parse display feature: " + featureString, e); - } - continue; - } - features.add(feature); + return Optional.of(Collections.emptyList()); } - return Optional.of(features); + return Optional.of(parseListFromString(displayFeaturesString, COMMON_STATE_UNKNOWN)); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 6d8383372461..1d2b9384f47d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -28,7 +28,6 @@ import android.app.Activity; import android.app.ActivityClient; import android.app.ActivityOptions; import android.app.ActivityThread; -import android.app.Application.ActivityLifecycleCallbacks; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; @@ -41,6 +40,8 @@ import android.os.Looper; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; +import androidx.window.common.EmptyLifecycleCallbacksAdapter; + import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -763,11 +764,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return shouldRetainAssociatedContainer(finishingContainer, associatedContainer); } - private final class LifecycleCallbacks implements ActivityLifecycleCallbacks { - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - } + private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { @Override public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { @@ -779,30 +776,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override - public void onActivityStarted(Activity activity) { - } - - @Override - public void onActivityResumed(Activity activity) { - } - - @Override - public void onActivityPaused(Activity activity) { - } - - @Override - public void onActivityStopped(Activity activity) { - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - } - - @Override - public void onActivityDestroyed(Activity activity) { - } - - @Override public void onActivityConfigurationChanged(Activity activity) { SplitController.this.onActivityConfigurationChanged(activity); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index fe9ce971d4d9..a4fbdbc493f5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -18,28 +18,30 @@ package androidx.window.extensions.layout; import static android.view.Display.DEFAULT_DISPLAY; -import static androidx.window.common.DisplayFeature.COMMON_STATE_FLAT; -import static androidx.window.common.DisplayFeature.COMMON_STATE_HALF_OPENED; +import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT; +import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED; import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation; import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; import android.annotation.Nullable; import android.app.Activity; +import android.app.Application; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; -import androidx.window.common.DeviceStateManagerPostureProducer; -import androidx.window.common.DisplayFeature; -import androidx.window.common.ResourceConfigDisplayFeatureProducer; -import androidx.window.common.SettingsDevicePostureProducer; +import androidx.window.common.CommonFoldingFeature; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.common.SettingsDisplayFeatureProducer; import androidx.window.util.DataProducer; import androidx.window.util.PriorityDataProducer; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -56,36 +58,27 @@ import java.util.function.Consumer; */ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private static final String TAG = "SampleExtension"; - private static WindowLayoutComponent sInstance; private final Map<Activity, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners = - new HashMap<>(); - - private final SettingsDevicePostureProducer mSettingsDevicePostureProducer; - private final DataProducer<Integer> mDevicePostureProducer; + new ArrayMap<>(); private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer; - private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer; + private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; public WindowLayoutComponentImpl(Context context) { - mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context); - mDevicePostureProducer = new PriorityDataProducer<>(List.of( - mSettingsDevicePostureProducer, - new DeviceStateManagerPostureProducer(context) - )); - + ((Application) context.getApplicationContext()) + .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); mSettingsDisplayFeatureProducer = new SettingsDisplayFeatureProducer(context); - mDisplayFeatureProducer = new PriorityDataProducer<>(List.of( + mFoldingFeatureProducer = new PriorityDataProducer<>(List.of( mSettingsDisplayFeatureProducer, - new ResourceConfigDisplayFeatureProducer(context) + new DeviceStateManagerFoldingFeatureProducer(context) )); - - mDevicePostureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); - mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); + mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } /** * Adds a listener interested in receiving updates to {@link WindowLayoutInfo} + * * @param activity hosting a {@link android.view.Window} * @param consumer interested in receiving updates to {@link WindowLayoutInfo} */ @@ -97,6 +90,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { /** * Removes a listener no longer interested in receiving updates. + * * @param consumer no longer interested in receiving updates to {@link WindowLayoutInfo} */ public void removeWindowLayoutInfoListener( @@ -118,43 +112,34 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return mWindowLayoutChangeListeners.keySet(); } - protected boolean hasListeners() { - return !mWindowLayoutChangeListeners.isEmpty(); + @NonNull + private boolean isListeningForLayoutChanges(IBinder token) { + for (Activity activity: getActivitiesListeningForLayoutChanges()) { + if (token.equals(activity.getWindow().getAttributes().token)) { + return true; + } + } + return false; } - /** - * Calculate the {@link DisplayFeature.State} from the feature or the device posture producer. - * If the given {@link DisplayFeature.State} is not valid then {@code null} will be returned. - * The {@link FoldingFeature} should be ignored in the case of an invalid - * {@link DisplayFeature.State}. - * - * @param feature a {@link DisplayFeature} to provide the feature state if present. - * @return {@link DisplayFeature.State} of the hinge if present or the state from the posture - * produce if present. - */ - @Nullable - private Integer getFeatureState(DisplayFeature feature) { - Integer featureState = feature.getState(); - Optional<Integer> posture = mDevicePostureProducer.getData(); - Integer state = featureState == null ? posture.orElse(null) : featureState; - return convertToExtensionState(state); + protected boolean hasListeners() { + return !mWindowLayoutChangeListeners.isEmpty(); } /** * A convenience method to translate from the common feature state to the extensions feature - * state. More specifically, translates from {@link DisplayFeature.State} to + * state. More specifically, translates from {@link CommonFoldingFeature.State} to * {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED}. If it is not * possible to translate, then we will return a {@code null} value. * - * @param state if it matches a value in {@link DisplayFeature.State}, {@code null} otherwise. - * @return a {@link FoldingFeature.STATE_FLAT} or {@link FoldingFeature.STATE_HALF_OPENED} if - * the given state matches a value in {@link DisplayFeature.State} and {@code null} otherwise. + * @param state if it matches a value in {@link CommonFoldingFeature.State}, {@code null} + * otherwise. @return a {@link FoldingFeature.STATE_FLAT} or + * {@link FoldingFeature.STATE_HALF_OPENED} if the given state matches a value in + * {@link CommonFoldingFeature.State} and {@code null} otherwise. */ @Nullable - private Integer convertToExtensionState(@Nullable Integer state) { - if (state == null) { // The null check avoids a NullPointerException. - return null; - } else if (state == COMMON_STATE_FLAT) { + private Integer convertToExtensionState(int state) { + if (state == COMMON_STATE_FLAT) { return FoldingFeature.STATE_FLAT; } else if (state == COMMON_STATE_HALF_OPENED) { return FoldingFeature.STATE_HALF_OPENED; @@ -172,33 +157,30 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { @NonNull private WindowLayoutInfo getWindowLayoutInfo(@NonNull Activity activity) { - List<androidx.window.extensions.layout.DisplayFeature> displayFeatures = - getDisplayFeatures(activity); + List<DisplayFeature> displayFeatures = getDisplayFeatures(activity); return new WindowLayoutInfo(displayFeatures); } /** - * Translate from the {@link DisplayFeature} to - * {@link androidx.window.extensions.layout.DisplayFeature} for a given {@link Activity}. If a - * {@link DisplayFeature} is not valid then it will be omitted. + * Translate from the {@link CommonFoldingFeature} to + * {@link DisplayFeature} for a given {@link Activity}. If a + * {@link CommonFoldingFeature} is not valid then it will be omitted. * * For a {@link FoldingFeature} the bounds are localized into the {@link Activity} window - * coordinate space and the state is calculated either from {@link DisplayFeature#getState()} or - * {@link #mDisplayFeatureProducer}. The state from {@link #mDisplayFeatureProducer} may not be - * valid since {@link #mDisplayFeatureProducer} is a general state controller. If the state is - * not valid, the {@link FoldingFeature} is omitted from the {@link List} of - * {@link androidx.window.extensions.layout.DisplayFeature}. If the bounds are not valid, - * constructing a {@link FoldingFeature} will throw an {@link IllegalArgumentException} since - * this can cause negative UI effects down stream. + * coordinate space and the state is calculated from {@link CommonFoldingFeature#getState()}. + * The state from {@link #mFoldingFeatureProducer} may not be valid since + * {@link #mFoldingFeatureProducer} is a general state controller. If the state is not valid, + * the {@link FoldingFeature} is omitted from the {@link List} of {@link DisplayFeature}. If the + * bounds are not valid, constructing a {@link FoldingFeature} will throw an + * {@link IllegalArgumentException} since this can cause negative UI effects down stream. * * @param activity a proxy for the {@link android.view.Window} that contains the - * {@link androidx.window.extensions.layout.DisplayFeature}. - * @return a {@link List} of valid {@link androidx.window.extensions.layout.DisplayFeature} that + * {@link DisplayFeature}. + * @return a {@link List} of valid {@link DisplayFeature} that * are within the {@link android.view.Window} of the {@link Activity} */ - private List<androidx.window.extensions.layout.DisplayFeature> getDisplayFeatures( - @NonNull Activity activity) { - List<androidx.window.extensions.layout.DisplayFeature> features = new ArrayList<>(); + private List<DisplayFeature> getDisplayFeatures(@NonNull Activity activity) { + List<DisplayFeature> features = new ArrayList<>(); int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { Log.w(TAG, "This sample doesn't support display features on secondary displays"); @@ -211,11 +193,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return features; } - Optional<List<DisplayFeature>> storedFeatures = mDisplayFeatureProducer.getData(); + Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData(); if (storedFeatures.isPresent()) { - - for (DisplayFeature baseFeature : storedFeatures.get()) { - Integer state = getFeatureState(baseFeature); + for (CommonFoldingFeature baseFeature : storedFeatures.get()) { + Integer state = convertToExtensionState(baseFeature.getState()); if (state == null) { continue; } @@ -223,8 +204,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { rotateRectToDisplayRotation(displayId, featureRect); transformToWindowSpaceRect(activity, featureRect); - features.add(new FoldingFeature(featureRect, baseFeature.getType(), - getFeatureState(baseFeature))); + features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); } } return features; @@ -232,13 +212,31 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private void updateRegistrations() { if (hasListeners()) { - mSettingsDevicePostureProducer.registerObserversIfNeeded(); mSettingsDisplayFeatureProducer.registerObserversIfNeeded(); } else { - mSettingsDevicePostureProducer.unregisterObserversIfNeeded(); mSettingsDisplayFeatureProducer.unregisterObserversIfNeeded(); } - onDisplayFeaturesChanged(); } + + private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + super.onActivityCreated(activity, savedInstanceState); + onDisplayFeaturesChangedIfListening(activity); + } + + @Override + public void onActivityConfigurationChanged(Activity activity) { + super.onActivityConfigurationChanged(activity); + onDisplayFeaturesChangedIfListening(activity); + } + + private void onDisplayFeaturesChangedIfListening(Activity activity) { + IBinder token = activity.getWindow().getAttributes().token; + if (token == null || isListeningForLayoutChanges(token)) { + onDisplayFeaturesChanged(); + } + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java index aa949f126154..c7b709347060 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java @@ -23,16 +23,17 @@ import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect; import android.app.Activity; import android.app.ActivityThread; +import android.app.Application; import android.content.Context; import android.graphics.Rect; +import android.os.Bundle; import android.os.IBinder; import android.util.Log; import androidx.annotation.NonNull; -import androidx.window.common.DeviceStateManagerPostureProducer; -import androidx.window.common.DisplayFeature; -import androidx.window.common.ResourceConfigDisplayFeatureProducer; -import androidx.window.common.SettingsDevicePostureProducer; +import androidx.window.common.CommonFoldingFeature; +import androidx.window.common.DeviceStateManagerFoldingFeatureProducer; +import androidx.window.common.EmptyLifecycleCallbacksAdapter; import androidx.window.common.SettingsDisplayFeatureProducer; import androidx.window.util.DataProducer; import androidx.window.util.PriorityDataProducer; @@ -48,36 +49,25 @@ import java.util.Optional; */ class SampleSidecarImpl extends StubSidecar { private static final String TAG = "SampleSidecar"; - private static final boolean DEBUG = false; - private final SettingsDevicePostureProducer mSettingsDevicePostureProducer; - private final DataProducer<Integer> mDevicePostureProducer; + private final DataProducer<List<CommonFoldingFeature>> mFoldingFeatureProducer; - private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer; - private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer; + private final SettingsDisplayFeatureProducer mSettingsFoldingFeatureProducer; SampleSidecarImpl(Context context) { - mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context); - mDevicePostureProducer = new PriorityDataProducer<>(List.of( - mSettingsDevicePostureProducer, - new DeviceStateManagerPostureProducer(context) + ((Application) context.getApplicationContext()) + .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); + mSettingsFoldingFeatureProducer = new SettingsDisplayFeatureProducer(context); + mFoldingFeatureProducer = new PriorityDataProducer<>(List.of( + mSettingsFoldingFeatureProducer, + new DeviceStateManagerFoldingFeatureProducer(context) )); - mSettingsDisplayFeatureProducer = new SettingsDisplayFeatureProducer(context); - mDisplayFeatureProducer = new PriorityDataProducer<>(List.of( - mSettingsDisplayFeatureProducer, - new ResourceConfigDisplayFeatureProducer(context) - )); - - mDevicePostureProducer.addDataChangedCallback(this::onDevicePostureChanged); - mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); - } - - private void onDevicePostureChanged() { - updateDeviceState(getDeviceState()); + mFoldingFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged); } private void onDisplayFeaturesChanged() { + updateDeviceState(getDeviceState()); for (IBinder windowToken : getWindowsListeningForLayoutChanges()) { SidecarWindowLayoutInfo newLayout = getWindowLayoutInfo(windowToken); updateWindowLayout(windowToken, newLayout); @@ -87,27 +77,21 @@ class SampleSidecarImpl extends StubSidecar { @NonNull @Override public SidecarDeviceState getDeviceState() { - Optional<Integer> posture = mDevicePostureProducer.getData(); - SidecarDeviceState deviceState = new SidecarDeviceState(); - deviceState.posture = posture.orElse(deviceStateFromFeature()); + deviceState.posture = deviceStateFromFeature(); return deviceState; } private int deviceStateFromFeature() { - List<DisplayFeature> storedFeatures = mDisplayFeatureProducer.getData() + List<CommonFoldingFeature> storedFeatures = mFoldingFeatureProducer.getData() .orElse(Collections.emptyList()); for (int i = 0; i < storedFeatures.size(); i++) { - DisplayFeature feature = storedFeatures.get(i); - final int state = feature.getState() == null ? -1 : feature.getState(); - if (DEBUG && feature.getState() == null) { - Log.d(TAG, "feature#getState was null for DisplayFeature: " + feature); - } - + CommonFoldingFeature feature = storedFeatures.get(i); + final int state = feature.getState(); switch (state) { - case DisplayFeature.COMMON_STATE_FLAT: + case CommonFoldingFeature.COMMON_STATE_FLAT: return SidecarDeviceState.POSTURE_OPENED; - case DisplayFeature.COMMON_STATE_HALF_OPENED: + case CommonFoldingFeature.COMMON_STATE_HALF_OPENED: return SidecarDeviceState.POSTURE_HALF_OPENED; } } @@ -127,22 +111,22 @@ class SampleSidecarImpl extends StubSidecar { } private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) { - List<SidecarDisplayFeature> features = new ArrayList<SidecarDisplayFeature>(); int displayId = activity.getDisplay().getDisplayId(); if (displayId != DEFAULT_DISPLAY) { Log.w(TAG, "This sample doesn't support display features on secondary displays"); - return features; + return Collections.emptyList(); } if (activity.isInMultiWindowMode()) { // It is recommended not to report any display features in multi-window mode, since it // won't be possible to synchronize the display feature positions with window movement. - return features; + return Collections.emptyList(); } - Optional<List<DisplayFeature>> storedFeatures = mDisplayFeatureProducer.getData(); + Optional<List<CommonFoldingFeature>> storedFeatures = mFoldingFeatureProducer.getData(); + List<SidecarDisplayFeature> features = new ArrayList<>(); if (storedFeatures.isPresent()) { - for (DisplayFeature baseFeature : storedFeatures.get()) { + for (CommonFoldingFeature baseFeature : storedFeatures.get()) { SidecarDisplayFeature feature = new SidecarDisplayFeature(); Rect featureRect = baseFeature.getRect(); rotateRectToDisplayRotation(displayId, featureRect); @@ -152,17 +136,37 @@ class SampleSidecarImpl extends StubSidecar { features.add(feature); } } - return features; + return Collections.unmodifiableList(features); } @Override protected void onListenersChanged() { if (hasListeners()) { - mSettingsDevicePostureProducer.registerObserversIfNeeded(); - mSettingsDisplayFeatureProducer.registerObserversIfNeeded(); + mSettingsFoldingFeatureProducer.registerObserversIfNeeded(); + onDisplayFeaturesChanged(); } else { - mSettingsDevicePostureProducer.unregisterObserversIfNeeded(); - mSettingsDisplayFeatureProducer.unregisterObserversIfNeeded(); + mSettingsFoldingFeatureProducer.unregisterObserversIfNeeded(); + } + } + + private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + super.onActivityCreated(activity, savedInstanceState); + onDisplayFeaturesChangedForActivity(activity); + } + + @Override + public void onActivityConfigurationChanged(Activity activity) { + super.onActivityConfigurationChanged(activity); + onDisplayFeaturesChangedForActivity(activity); + } + + private void onDisplayFeaturesChangedForActivity(@NonNull Activity activity) { + IBinder token = activity.getWindow().getAttributes().token; + if (token == null || mWindowLayoutChangeListenerTokens.contains(token)) { + onDisplayFeaturesChanged(); + } } } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java index 199c37315c07..b9c808a6569b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java @@ -30,7 +30,7 @@ import java.util.Set; abstract class StubSidecar implements SidecarInterface { private SidecarCallback mSidecarCallback; - private final Set<IBinder> mWindowLayoutChangeListenerTokens = new HashSet<>(); + final Set<IBinder> mWindowLayoutChangeListenerTokens = new HashSet<>(); private boolean mDeviceStateChangeListenerRegistered; StubSidecar() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java index 99dbfe01964c..b87cf47dd93f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java @@ -24,9 +24,12 @@ import com.android.wm.shell.common.annotations.ExternalThread; @ExternalThread public interface CompatUI { /** - * Called when the keyguard occluded state changes. Removes all compat UIs if the - * keyguard is now occluded. - * @param occluded indicates if the keyguard is now occluded. + * Called when the keyguard showing state changes. Removes all compat UIs if the + * keyguard is now showing. + * + * <p>Note that if the keyguard is occluded it will also be considered showing. + * + * @param showing indicates if the keyguard is now showing. */ - void onKeyguardOccludedChanged(boolean occluded); + void onKeyguardShowingChanged(boolean showing); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index ee4d5ed018bf..b2bbafeb7bf5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -109,9 +109,9 @@ public class CompatUIController implements OnDisplaysChangedListener, // Only show each hint once automatically in the process life. private final CompatUIHintsState mCompatUIHintsState; - // Indicates if the keyguard is currently occluded, in which case compat UIs shouldn't + // Indicates if the keyguard is currently showing, in which case compat UIs shouldn't // be shown. - private boolean mKeyguardOccluded; + private boolean mKeyguardShowing; public CompatUIController(Context context, DisplayController displayController, @@ -218,14 +218,14 @@ public class CompatUIController implements OnDisplaysChangedListener, } @VisibleForTesting - void onKeyguardOccludedChanged(boolean occluded) { - mKeyguardOccluded = occluded; - // Hide the compat UIs when keyguard is occluded. + void onKeyguardShowingChanged(boolean showing) { + mKeyguardShowing = showing; + // Hide the compat UIs when keyguard is showing. forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); } private boolean showOnDisplay(int displayId) { - return !mKeyguardOccluded && !isImeShowingOnDisplay(displayId); + return !mKeyguardShowing && !isImeShowingOnDisplay(displayId); } private boolean isImeShowingOnDisplay(int displayId) { @@ -372,9 +372,9 @@ public class CompatUIController implements OnDisplaysChangedListener, @ExternalThread private class CompatUIImpl implements CompatUI { @Override - public void onKeyguardOccludedChanged(boolean occluded) { + public void onKeyguardShowingChanged(boolean showing) { mMainExecutor.execute(() -> { - CompatUIController.this.onKeyguardOccludedChanged(occluded); + CompatUIController.this.onKeyguardShowingChanged(showing); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java index 2da6a6bc70c0..8aa4d0ee99ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java @@ -21,6 +21,7 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; +import android.widget.TextView; import androidx.constraintlayout.widget.ConstraintLayout; @@ -38,6 +39,7 @@ class LetterboxEduDialogLayout extends ConstraintLayout { // 204 is simply 255 * 0.8. static final int BACKGROUND_DIM_ALPHA = 204; private View mDialogContainer; + private TextView mDialogTitle; private Drawable mBackgroundDim; public LetterboxEduDialogLayout(Context context) { @@ -61,6 +63,10 @@ class LetterboxEduDialogLayout extends ConstraintLayout { return mDialogContainer; } + TextView getDialogTitle() { + return mDialogTitle; + } + Drawable getBackgroundDim() { return mBackgroundDim; } @@ -84,6 +90,7 @@ class LetterboxEduDialogLayout extends ConstraintLayout { protected void onFinishInflate() { super.onFinishInflate(); mDialogContainer = findViewById(R.id.letterbox_education_dialog_container); + mDialogTitle = findViewById(R.id.letterbox_education_dialog_title); mBackgroundDim = getBackground().mutate(); // Set the alpha of the background dim to 0 for enter animation. mBackgroundDim.setAlpha(0); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java index 30b9f0838e10..dda72ffb432f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java @@ -28,6 +28,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup.MarginLayoutParams; import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; @@ -132,7 +133,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { updateDialogMargins(); mAnimationController.startEnterAnimation(mLayout, /* endCallback= */ - this::setDismissOnClickListener); + this::onDialogEnterAnimationEnded); return mLayout; } @@ -157,11 +158,13 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { R.layout.letterbox_education_dialog_layout, null); } - private void setDismissOnClickListener() { + private void onDialogEnterAnimationEnded() { if (mLayout == null) { return; } mLayout.setDismissOnClickListener(this::onDismiss); + // Focus on the dialog title for accessibility. + mLayout.getDialogTitle().sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); } private void onDismiss() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index 491dff08187f..1e934c5c6e22 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -27,7 +27,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipMediaController; @@ -163,15 +162,14 @@ public abstract class TvPipModule { PipAnimationController pipAnimationController, PipTransitionController pipTransitionController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, - Optional<LegacySplitScreenController> splitScreenOptional, - Optional<SplitScreenController> newSplitScreenOptional, + Optional<SplitScreenController> splitScreenControllerOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new PipTaskOrganizer(context, syncTransactionQueue, pipTransitionState, tvPipBoundsState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, splitScreenOptional, newSplitScreenOptional, + pipTransitionController, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 7879e7a5bb00..73f393140cbc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -285,15 +285,14 @@ public class WMShellModule { PipAnimationController pipAnimationController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, PipTransitionController pipTransitionController, - Optional<LegacySplitScreenController> splitScreenOptional, - Optional<SplitScreenController> newSplitScreenOptional, + Optional<SplitScreenController> splitScreenControllerOptional, DisplayController displayController, PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new PipTaskOrganizer(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, - pipTransitionController, splitScreenOptional, newSplitScreenOptional, + pipTransitionController, splitScreenControllerOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index bfa14f3a705d..b266189ec262 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -81,7 +81,6 @@ import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ShellMainThread; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PipMotionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -134,7 +133,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final int mExitAnimationDuration; private final int mCrossFadeAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; private final Optional<SplitScreenController> mSplitScreenOptional; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; @@ -262,7 +260,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, - Optional<LegacySplitScreenController> legacySplitScreenOptional, Optional<SplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @@ -285,7 +282,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; - mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; @@ -505,11 +501,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, wct.setWindowingMode(mToken, getOutPipWindowingMode()); // Simply reset the activity mode set prior to the animation running. wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - mLegacySplitScreenOptional.ifPresent(splitScreen -> { - if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { - wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */); - } - }); } /** @@ -1512,36 +1503,21 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * Sync with {@link LegacySplitScreenController} or {@link SplitScreenController} on destination - * bounds if PiP is going to split screen. + * Sync with {@link SplitScreenController} on destination bounds if PiP is going to + * split screen. * * @param destinationBoundsOut contain the updated destination bounds if applicable * @return {@code true} if destinationBounds is altered for split screen */ private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit) { - if (enterSplit && mSplitScreenOptional.isPresent()) { - final Rect topLeft = new Rect(); - final Rect bottomRight = new Rect(); - mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); - final boolean isPipTopLeft = isPipTopLeft(); - destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); - return true; - } - - if (!mLegacySplitScreenOptional.isPresent()) { - return false; - } - - LegacySplitScreenController legacySplitScreen = mLegacySplitScreenOptional.get(); - if (!legacySplitScreen.isDividerVisible()) { - // fail early if system is not in split screen mode + if (!enterSplit || !mSplitScreenOptional.isPresent()) { return false; } - - // PiP window will go to split-secondary mode instead of fullscreen, populates the - // split screen bounds here. - destinationBoundsOut.set(legacySplitScreen.getDividerView() - .getNonMinimizedSplitScreenSecondaryBounds()); + final Rect topLeft = new Rect(); + final Rect bottomRight = new Rect(); + mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); + final boolean isPipTopLeft = isPipTopLeft(); + destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 56d51687603a..a225f4ec8349 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -408,7 +408,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { || type == TRANSIT_CLOSE || type == TRANSIT_TO_FRONT || type == TRANSIT_TO_BACK; - if (isOpenOrCloseTransition) { + final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0; + if (isOpenOrCloseTransition && !isTranslucent) { // Use the overview background as the background for the animation final Context uiContext = ActivityThread.currentActivityThread() .getSystemUiContext(); @@ -729,14 +730,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation : R.styleable.WindowAnimation_wallpaperCloseExitAnimation; } else if (type == TRANSIT_OPEN) { - if (isTask) { + // We will translucent open animation for translucent activities and tasks. Choose + // WindowAnimation_activityOpenEnterAnimation and set translucent here, then + // TransitionAnimation loads appropriate animation later. + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { + translucent = true; + } + if (isTask && !translucent) { animAttr = enter ? R.styleable.WindowAnimation_taskOpenEnterAnimation : R.styleable.WindowAnimation_taskOpenExitAnimation; } else { - if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { - translucent = true; - } animAttr = enter ? R.styleable.WindowAnimation_activityOpenEnterAnimation : R.styleable.WindowAnimation_activityOpenExitAnimation; @@ -770,7 +774,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { .loadAnimationAttr(options.getPackageName(), options.getAnimations(), animAttr, translucent); } else { - a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr); + a = mTransitionAnimation.loadDefaultAnimationAttr(animAttr, translucent); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index efb52a5b4644..610d2cc39445 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -363,7 +363,9 @@ public class Transitions implements RemoteCallable<Transitions> { return; } - // apply transfer starting window directly if there is no other task change. + // apply transfer starting window directly if there is no other task change. Since this + // is an activity->activity situation, we can detect it by selecting transitions with only + // 2 changes where neither are tasks and one is a starting-window recipient. final int changeSize = info.getChanges().size(); if (changeSize == 2) { boolean nonTaskChange = true; @@ -380,7 +382,9 @@ public class Transitions implements RemoteCallable<Transitions> { } if (nonTaskChange && transferStartingWindow) { t.apply(); - onFinish(transitionToken, null /* wct */, null /* wctCB */); + // Treat this as an abort since we are bypassing any merge logic and effectively + // finishing immediately. + onAbort(transitionToken); return; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 29e40be457d1..a31b28737552 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -325,17 +325,17 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test - public void testChangeLayoutsVisibilityOnKeyguardOccludedChanged() { + public void testChangeLayoutsVisibilityOnKeyguardShowingChanged() { mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); - // Verify that the restart button is hidden after keyguard becomes occluded. - mController.onKeyguardOccludedChanged(true); + // Verify that the restart button is hidden after keyguard becomes showing. + mController.onKeyguardShowingChanged(true); verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); - // Verify button remains hidden while keyguard is occluded. + // Verify button remains hidden while keyguard is showing. TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); mController.onCompatInfoChanged(taskInfo, mMockTaskListener); @@ -345,20 +345,20 @@ public class CompatUIControllerTest extends ShellTestCase { verify(mMockLetterboxEduLayout).updateCompatInfo(taskInfo, mMockTaskListener, /* canShow= */ false); - // Verify button is shown after keyguard becomes not occluded. - mController.onKeyguardOccludedChanged(false); + // Verify button is shown after keyguard becomes not showing. + mController.onKeyguardShowingChanged(false); verify(mMockCompatLayout).updateVisibility(true); verify(mMockLetterboxEduLayout).updateVisibility(true); } @Test - public void testLayoutsRemainHiddenOnKeyguardOccludedFalseWhenImeIsShowing() { + public void testLayoutsRemainHiddenOnKeyguardShowingFalseWhenImeIsShowing() { mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); - mController.onKeyguardOccludedChanged(true); + mController.onKeyguardShowingChanged(true); verify(mMockCompatLayout, times(2)).updateVisibility(false); verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false); @@ -366,8 +366,8 @@ public class CompatUIControllerTest extends ShellTestCase { clearInvocations(mMockCompatLayout); clearInvocations(mMockLetterboxEduLayout); - // Verify button remains hidden after keyguard becomes not occluded since IME is showing. - mController.onKeyguardOccludedChanged(false); + // Verify button remains hidden after keyguard becomes not showing since IME is showing. + mController.onKeyguardShowingChanged(false); verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); @@ -380,12 +380,12 @@ public class CompatUIControllerTest extends ShellTestCase { } @Test - public void testLayoutsRemainHiddenOnImeHideWhenKeyguardIsOccluded() { + public void testLayoutsRemainHiddenOnImeHideWhenKeyguardIsShowing() { mController.onCompatInfoChanged(createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN), mMockTaskListener); mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ true); - mController.onKeyguardOccludedChanged(true); + mController.onKeyguardShowingChanged(true); verify(mMockCompatLayout, times(2)).updateVisibility(false); verify(mMockLetterboxEduLayout, times(2)).updateVisibility(false); @@ -393,14 +393,14 @@ public class CompatUIControllerTest extends ShellTestCase { clearInvocations(mMockCompatLayout); clearInvocations(mMockLetterboxEduLayout); - // Verify button remains hidden after IME is hidden since keyguard is occluded. + // Verify button remains hidden after IME is hidden since keyguard is showing. mController.onImeVisibilityChanged(DISPLAY_ID, /* isShowing= */ false); verify(mMockCompatLayout).updateVisibility(false); verify(mMockLetterboxEduLayout).updateVisibility(false); - // Verify button is shown after keyguard becomes not occluded. - mController.onKeyguardOccludedChanged(false); + // Verify button is shown after keyguard becomes not showing. + mController.onKeyguardShowingChanged(false); verify(mMockCompatLayout).updateVisibility(true); verify(mMockLetterboxEduLayout).updateVisibility(true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java index 00e4938d866c..1dee88c43806 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java @@ -70,6 +70,8 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { public void testOnFinishInflate() { assertEquals(mLayout.getDialogContainer(), mLayout.findViewById(R.id.letterbox_education_dialog_container)); + assertEquals(mLayout.getDialogTitle(), + mLayout.findViewById(R.id.letterbox_education_dialog_title)); assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground()); assertEquals(mLayout.getBackground().getAlpha(), 0); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java index 0509dd38abe0..337b7385faec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java @@ -41,9 +41,11 @@ import android.testing.AndroidTestingRunner; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.SurfaceControlViewHost; +import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; import androidx.test.filters.SmallTest; @@ -173,13 +175,19 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { verifyLayout(layout, mWindowAttrsCaptor.getValue(), /* expectedWidth= */ TASK_WIDTH, /* expectedHeight= */ TASK_HEIGHT, /* expectedExtraTopMargin= */ DISPLAY_CUTOUT_TOP, /* expectedExtraBottomMargin= */ DISPLAY_CUTOUT_BOTTOM); + View dialogTitle = layout.getDialogTitle(); + assertNotNull(dialogTitle); + spyOn(dialogTitle); // Clicking the layout does nothing until enter animation is done. layout.performClick(); verify(mAnimationController, never()).startExitAnimation(any(), any()); + // The dialog title shouldn't be focused for Accessibility until enter animation is done. + verify(dialogTitle, never()).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); verifyAndFinishEnterAnimation(layout); + verify(dialogTitle).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); // Exit animation should start following a click on the layout. layout.performClick(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 0172cf324eea..14d9fb9babc4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -48,7 +48,6 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -76,7 +75,6 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; - @Mock private Optional<LegacySplitScreenController> mMockOptionalLegacySplitScreen; @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; private TestShellExecutor mMainExecutor; @@ -101,8 +99,8 @@ public class PipTaskOrganizerTest extends ShellTestCase { mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, - mMockPipTransitionController, mMockOptionalLegacySplitScreen, - mMockOptionalSplitScreen, mMockDisplayController, mMockPipUiEventLogger, + mMockPipTransitionController, mMockOptionalSplitScreen, + mMockDisplayController, mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); mMainExecutor.flushAll(); preparePipTaskOrg(); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 311476cbd489..47e402f73f40 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -76,6 +76,7 @@ public final class MediaRouter2 { @GuardedBy("sSystemRouterLock") private static Map<String, MediaRouter2> sSystemMediaRouter2Map = new ArrayMap<>(); + private static MediaRouter2Manager sManager; @GuardedBy("sRouterLock") @@ -119,14 +120,12 @@ public final class MediaRouter2 { private final AtomicInteger mNextRequestId = new AtomicInteger(1); final Handler mHandler; - @GuardedBy("mLock") - private boolean mShouldUpdateRoutes = true; + + private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>(); private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList(); private volatile OnGetControllerHintsListener mOnGetControllerHintsListener; - /** - * Gets an instance of the media router associated with the context. - */ + /** Gets an instance of the media router associated with the context. */ @NonNull public static MediaRouter2 getInstance(@NonNull Context context) { Objects.requireNonNull(context, "context must not be null"); @@ -139,29 +138,31 @@ public final class MediaRouter2 { } /** - * Gets an instance of the system media router which controls the app's media routing. - * Returns {@code null} if the given package name is invalid. - * There are several things to note when using the media routers created with this method. - * <p> - * First of all, the discovery preference passed to {@link #registerRouteCallback} - * will have no effect. The callback will be called accordingly with the client app's - * discovery preference. Therefore, it is recommended to pass - * {@link RouteDiscoveryPreference#EMPTY} there. - * <p> - * Also, do not keep/compare the instances of the {@link RoutingController}, since they are + * Gets an instance of the system media router which controls the app's media routing. Returns + * {@code null} if the given package name is invalid. There are several things to note when + * using the media routers created with this method. + * + * <p>First of all, the discovery preference passed to {@link #registerRouteCallback} will have + * no effect. The callback will be called accordingly with the client app's discovery + * preference. Therefore, it is recommended to pass {@link RouteDiscoveryPreference#EMPTY} + * there. + * + * <p>Also, do not keep/compare the instances of the {@link RoutingController}, since they are * always newly created with the latest session information whenever below methods are called: + * * <ul> - * <li> {@link #getControllers()} </li> - * <li> {@link #getController(String)}} </li> - * <li> {@link TransferCallback#onTransfer(RoutingController, RoutingController)} </li> - * <li> {@link TransferCallback#onStop(RoutingController)} </li> - * <li> {@link ControllerCallback#onControllerUpdated(RoutingController)} </li> + * <li>{@link #getControllers()} + * <li>{@link #getController(String)}} + * <li>{@link TransferCallback#onTransfer(RoutingController, RoutingController)} + * <li>{@link TransferCallback#onStop(RoutingController)} + * <li>{@link ControllerCallback#onControllerUpdated(RoutingController)} * </ul> + * * Therefore, in order to track the current routing status, keep the controller's ID instead, - * and use {@link #getController(String)} and {@link #getSystemController()} for - * getting controllers. - * <p> - * Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}. + * and use {@link #getController(String)} and {@link #getSystemController()} for getting + * controllers. + * + * <p>Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}. * * @param clientPackageName the package name of the app to control * @throws SecurityException if the caller doesn't have MODIFY_AUDIO_ROUTING permission. @@ -170,15 +171,16 @@ public final class MediaRouter2 { @SystemApi @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) @Nullable - public static MediaRouter2 getInstance(@NonNull Context context, - @NonNull String clientPackageName) { + public static MediaRouter2 getInstance( + @NonNull Context context, @NonNull String clientPackageName) { Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(clientPackageName, "clientPackageName must not be null"); // Note: Even though this check could be somehow bypassed, the other permission checks // in system server will not allow MediaRouter2Manager to be registered. - IMediaRouterService serviceBinder = IMediaRouterService.Stub.asInterface( - ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); + IMediaRouterService serviceBinder = + IMediaRouterService.Stub.asInterface( + ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); try { // SecurityException will be thrown if there's no permission. serviceBinder.enforceMediaContentControlPermission(); @@ -212,17 +214,17 @@ public final class MediaRouter2 { /** * Starts scanning remote routes. - * <p> - * Route discovery can happen even when the {@link #startScan()} is not called. - * This is because the scanning could be started before by other apps. - * Therefore, calling this method after calling {@link #stopScan()} does not necessarily mean - * that the routes found before are removed and added again. - * <p> - * Use {@link RouteCallback} to get the route related events. - * <p> - * Note that calling start/stopScan is applied to all system routers in the same process. - * <p> - * This will be no-op for non-system media routers. + * + * <p>Route discovery can happen even when the {@link #startScan()} is not called. This is + * because the scanning could be started before by other apps. Therefore, calling this method + * after calling {@link #stopScan()} does not necessarily mean that the routes found before are + * removed and added again. + * + * <p>Use {@link RouteCallback} to get the route related events. + * + * <p>Note that calling start/stopScan is applied to all system routers in the same process. + * + * <p>This will be no-op for non-system media routers. * * @see #stopScan() * @see #getInstance(Context, String) @@ -238,18 +240,17 @@ public final class MediaRouter2 { /** * Stops scanning remote routes to reduce resource consumption. - * <p> - * Route discovery can be continued even after this method is called. - * This is because the scanning is only turned off when all the apps stop scanning. - * Therefore, calling this method does not necessarily mean the routes are removed. - * Also, for the same reason it does not mean that {@link RouteCallback#onRoutesAdded(List)} - * is not called afterwards. - * <p> - * Use {@link RouteCallback} to get the route related events. - * <p> - * Note that calling start/stopScan is applied to all system routers in the same process. - * <p> - * This will be no-op for non-system media routers. + * + * <p>Route discovery can be continued even after this method is called. This is because the + * scanning is only turned off when all the apps stop scanning. Therefore, calling this method + * does not necessarily mean the routes are removed. Also, for the same reason it does not mean + * that {@link RouteCallback#onRoutesAdded(List)} is not called afterwards. + * + * <p>Use {@link RouteCallback} to get the route related events. + * + * <p>Note that calling start/stopScan is applied to all system routers in the same process. + * + * <p>This will be no-op for non-system media routers. * * @see #startScan() * @see #getInstance(Context, String) @@ -265,8 +266,9 @@ public final class MediaRouter2 { private MediaRouter2(Context appContext) { mContext = appContext; - mMediaRouterService = IMediaRouterService.Stub.asInterface( - ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); + mMediaRouterService = + IMediaRouterService.Stub.asInterface( + ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); mPackageName = mContext.getPackageName(); mHandler = new Handler(Looper.getMainLooper()); @@ -302,9 +304,10 @@ public final class MediaRouter2 { mClientPackageName = clientPackageName; mManagerCallback = new ManagerCallback(); mHandler = new Handler(Looper.getMainLooper()); - mSystemController = new SystemRoutingController( - ensureClientPackageNameForSystemSession( - sManager.getSystemRoutingSession(clientPackageName))); + mSystemController = + new SystemRoutingController( + ensureClientPackageNameForSystemSession( + sManager.getSystemRoutingSession(clientPackageName))); mDiscoveryPreference = sManager.getDiscoveryPreference(clientPackageName); updateAllRoutesFromManager(); @@ -318,8 +321,8 @@ public final class MediaRouter2 { * * @hide */ - static boolean checkRouteListContainsRouteId(@NonNull List<MediaRoute2Info> routeList, - @NonNull String routeId) { + static boolean checkRouteListContainsRouteId( + @NonNull List<MediaRoute2Info> routeList, @NonNull String routeId) { for (MediaRoute2Info info : routeList) { if (TextUtils.equals(routeId, info.getId())) { return true; @@ -330,8 +333,8 @@ public final class MediaRouter2 { /** * Gets the client package name of the app which this media router controls. - * <p> - * This will return null for non-system media routers. + * + * <p>This will return null for non-system media routers. * * @see #getInstance(Context, String) * @hide @@ -344,12 +347,12 @@ public final class MediaRouter2 { /** * Registers a callback to discover routes and to receive events when they change. - * <p> - * If the specified callback is already registered, its registration will be updated for the + * + * <p>If the specified callback is already registered, its registration will be updated for the * given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}. - * </p> */ - public void registerRouteCallback(@NonNull @CallbackExecutor Executor executor, + public void registerRouteCallback( + @NonNull @CallbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference) { Objects.requireNonNull(executor, "executor must not be null"); @@ -391,8 +394,8 @@ public final class MediaRouter2 { } /** - * Unregisters the given callback. The callback will no longer receive events. - * If the callback has not been added or been removed already, it is ignored. + * Unregisters the given callback. The callback will no longer receive events. If the callback + * has not been added or been removed already, it is ignored. * * @param routeCallback the callback to unregister * @see #registerRouteCallback @@ -400,8 +403,7 @@ public final class MediaRouter2 { public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) { Objects.requireNonNull(routeCallback, "callback must not be null"); - if (!mRouteCallbackRecords.remove( - new RouteCallbackRecord(null, routeCallback, null))) { + if (!mRouteCallbackRecords.remove(new RouteCallbackRecord(null, routeCallback, null))) { Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback"); return; } @@ -416,8 +418,7 @@ public final class MediaRouter2 { } if (updateDiscoveryPreferenceIfNeededLocked()) { try { - mMediaRouterService.setDiscoveryRequestWithRouter2( - mStub, mDiscoveryPreference); + mMediaRouterService.setDiscoveryRequestWithRouter2(mStub, mDiscoveryPreference); } catch (RemoteException ex) { Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex); } @@ -430,27 +431,28 @@ public final class MediaRouter2 { } mStub = null; } - mShouldUpdateRoutes = true; } } + @GuardedBy("mLock") private boolean updateDiscoveryPreferenceIfNeededLocked() { RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder( mRouteCallbackRecords.stream().map(record -> record.mPreference).collect( Collectors.toList())).build(); + if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) { return false; } mDiscoveryPreference = newDiscoveryPreference; - mShouldUpdateRoutes = true; + updateFilteredRoutesLocked(); return true; } /** - * Gets the list of all discovered routes. - * This list includes the routes that are not related to the client app. - * <p> - * This will return an empty list for non-system media routers. + * Gets the list of all discovered routes. This list includes the routes that are not related to + * the client app. + * + * <p>This will return an empty list for non-system media routers. * * @hide */ @@ -464,25 +466,19 @@ public final class MediaRouter2 { } /** - * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently - * known to the media router. - * <p> - * Please note that the list can be changed before callbacks are invoked. - * </p> + * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently known to the media + * router. + * + * <p>Please note that the list can be changed before callbacks are invoked. + * * @return the list of routes that contains at least one of the route features in discovery - * preferences registered by the application + * preferences registered by the application */ @NonNull public List<MediaRoute2Info> getRoutes() { synchronized (mLock) { - if (mShouldUpdateRoutes) { - mShouldUpdateRoutes = false; - - mFilteredRoutes = Collections.unmodifiableList( - filterRoutes(List.copyOf(mRoutes.values()), mDiscoveryPreference)); - } + return mFilteredRoutes; } - return mFilteredRoutes; } /** @@ -493,8 +489,8 @@ public final class MediaRouter2 { * @param callback the callback to register * @see #unregisterTransferCallback */ - public void registerTransferCallback(@NonNull @CallbackExecutor Executor executor, - @NonNull TransferCallback callback) { + public void registerTransferCallback( + @NonNull @CallbackExecutor Executor executor, @NonNull TransferCallback callback) { Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(callback, "callback must not be null"); @@ -522,12 +518,13 @@ public final class MediaRouter2 { } /** - * Registers a {@link ControllerCallback}. - * If you register the same callback twice or more, it will be ignored. + * Registers a {@link ControllerCallback}. If you register the same callback twice or more, it + * will be ignored. + * * @see #unregisterControllerCallback(ControllerCallback) */ - public void registerControllerCallback(@NonNull @CallbackExecutor Executor executor, - @NonNull ControllerCallback callback) { + public void registerControllerCallback( + @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) { Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(callback, "callback must not be null"); @@ -539,12 +536,12 @@ public final class MediaRouter2 { } /** - * Unregisters a {@link ControllerCallback}. The callback will no longer receive - * events. If the callback has not been added or been removed already, it is ignored. + * Unregisters a {@link ControllerCallback}. The callback will no longer receive events. + * If the callback has not been added or been removed already, it is ignored. + * * @see #registerControllerCallback(Executor, ControllerCallback) */ - public void unregisterControllerCallback( - @NonNull ControllerCallback callback) { + public void unregisterControllerCallback(@NonNull ControllerCallback callback) { Objects.requireNonNull(callback, "callback must not be null"); if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) { @@ -559,7 +556,7 @@ public final class MediaRouter2 { * {@link #transferTo(MediaRoute2Info)}. * * @param listener A listener to send optional app-specific hints when creating a controller. - * {@code null} for unset. + * {@code null} for unset. */ public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) { if (isSystemRouter()) { @@ -569,13 +566,11 @@ public final class MediaRouter2 { } /** - * Transfers the current media to the given route. - * If it's necessary a new {@link RoutingController} is created or it is handled within - * the current routing controller. + * Transfers the current media to the given route. If it's necessary a new + * {@link RoutingController} is created or it is handled within the current routing controller. * * @param route the route you want to transfer the current media to. Pass {@code null} to * stop routing of the current media. - * * @see TransferCallback#onTransfer * @see TransferCallback#onTransferFailure */ @@ -622,8 +617,8 @@ public final class MediaRouter2 { /** * Transfers the media of a routing controller to the given route. - * <p> - * This will be no-op for non-system media routers. + * + * <p>This will be no-op for non-system media routers. * * @param controller a routing controller controlling media routing. * @param route the route you want to transfer the media to. @@ -638,13 +633,15 @@ public final class MediaRouter2 { } } - void requestCreateController(@NonNull RoutingController controller, - @NonNull MediaRoute2Info route, long managerRequestId) { + void requestCreateController( + @NonNull RoutingController controller, + @NonNull MediaRoute2Info route, + long managerRequestId) { final int requestId = mNextRequestId.getAndIncrement(); - ControllerCreationRequest request = new ControllerCreationRequest(requestId, - managerRequestId, route, controller); + ControllerCreationRequest request = + new ControllerCreationRequest(requestId, managerRequestId, route, controller); mControllerCreationRequests.add(request); OnGetControllerHintsListener listener = mOnGetControllerHintsListener; @@ -663,11 +660,15 @@ public final class MediaRouter2 { if (stub != null) { try { mMediaRouterService.requestCreateSessionWithRouter2( - stub, requestId, managerRequestId, - controller.getRoutingSessionInfo(), route, controllerHints); + stub, + requestId, + managerRequestId, + controller.getRoutingSessionInfo(), + route, + controllerHints); } catch (RemoteException ex) { Log.e(TAG, "createControllerForTransfer: " - + "Failed to request for creating a controller.", ex); + + "Failed to request for creating a controller.", ex); mControllerCreationRequests.remove(request); if (managerRequestId == MANAGER_REQUEST_ID_NONE) { notifyTransferFailure(route); @@ -685,11 +686,11 @@ public final class MediaRouter2 { /** * Gets a {@link RoutingController} which can control the routes provided by system. * e.g. Phone speaker, wired headset, Bluetooth, etc. - * <p> - * Note: The system controller can't be released. Calling {@link RoutingController#release()} + * + * <p>Note: The system controller can't be released. Calling {@link RoutingController#release()} * will be ignored. - * <p> - * This method always returns the same instance. + * + * <p>This method always returns the same instance. */ @NonNull public RoutingController getSystemController() { @@ -714,8 +715,8 @@ public final class MediaRouter2 { /** * Gets the list of currently active {@link RoutingController routing controllers} on which * media can be played. - * <p> - * Note: The list returned here will never be empty. The first element in the list is + * + * <p>Note: The list returned here will never be empty. The first element in the list is * always the {@link #getSystemController() system controller}. */ @NonNull @@ -750,8 +751,8 @@ public final class MediaRouter2 { /** * Requests a volume change for the route asynchronously. * It may have no effect if the route is currently not selected. - * <p> - * This will be no-op for non-system media routers. + * + * <p>This will be no-op for non-system media routers. * * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}. * @see #getInstance(Context, String) @@ -769,55 +770,61 @@ public final class MediaRouter2 { // If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2() } - void syncRoutesOnHandler(List<MediaRoute2Info> currentRoutes, - RoutingSessionInfo currentSystemSessionInfo) { + void syncRoutesOnHandler( + List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) { if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) { Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes + ", currentSystemSessionInfo=" + currentSystemSessionInfo); return; } + synchronized (mLock) { + mRoutes.clear(); + for (MediaRoute2Info route : currentRoutes) { + mRoutes.put(route.getId(), route); + } + updateFilteredRoutesLocked(); + } + + RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo(); + mSystemController.setRoutingSessionInfo(currentSystemSessionInfo); + if (!oldInfo.equals(currentSystemSessionInfo)) { + notifyControllerUpdated(mSystemController); + } + } + + void dispatchFilteredRoutesChangedLocked(List<MediaRoute2Info> newRoutes) { List<MediaRoute2Info> addedRoutes = new ArrayList<>(); List<MediaRoute2Info> removedRoutes = new ArrayList<>(); List<MediaRoute2Info> changedRoutes = new ArrayList<>(); - synchronized (mLock) { - List<String> currentRoutesIds = currentRoutes.stream().map(MediaRoute2Info::getId) - .collect(Collectors.toList()); - - for (String routeId : mRoutes.keySet()) { - if (!currentRoutesIds.contains(routeId)) { - // This route is removed while the callback is unregistered. - MediaRoute2Info route = mRoutes.get(routeId); - if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { - removedRoutes.add(mRoutes.get(routeId)); - } - } - } + Set<String> newRouteIds = + newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet()); - for (MediaRoute2Info route : currentRoutes) { - if (mRoutes.containsKey(route.getId())) { - if (!route.equals(mRoutes.get(route.getId()))) { - // This route is changed while the callback is unregistered. - if (route.hasAnyFeatures( - mDiscoveryPreference.getPreferredFeatures())) { - changedRoutes.add(route); - } - } - } else { - // This route is added while the callback is unregistered. - if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { - addedRoutes.add(route); - } - } + for (MediaRoute2Info route : newRoutes) { + MediaRoute2Info prevRoute = mPreviousRoutes.get(route.getId()); + if (prevRoute == null) { + addedRoutes.add(route); + } else if (!prevRoute.equals(route)) { + changedRoutes.add(route); } + } - mRoutes.clear(); - for (MediaRoute2Info route : currentRoutes) { - mRoutes.put(route.getId(), route); + for (int i = 0; i < mPreviousRoutes.size(); i++) { + if (!newRouteIds.contains(mPreviousRoutes.keyAt(i))) { + removedRoutes.add(mPreviousRoutes.valueAt(i)); } + } - mShouldUpdateRoutes = true; + // update previous routes + for (MediaRoute2Info route : removedRoutes) { + mPreviousRoutes.remove(route.getId()); + } + for (MediaRoute2Info route : addedRoutes) { + mPreviousRoutes.put(route.getId(), route); + } + for (MediaRoute2Info route : changedRoutes) { + mPreviousRoutes.put(route.getId(), route); } if (!addedRoutes.isEmpty()) { @@ -829,43 +836,23 @@ public final class MediaRouter2 { if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } - - RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo(); - mSystemController.setRoutingSessionInfo(currentSystemSessionInfo); - if (!oldInfo.equals(currentSystemSessionInfo)) { - notifyControllerUpdated(mSystemController); - } } void addRoutesOnHandler(List<MediaRoute2Info> routes) { - List<MediaRoute2Info> addedRoutes = new ArrayList<>(); synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); - if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { - addedRoutes.add(route); - } } - mShouldUpdateRoutes = true; - } - if (!addedRoutes.isEmpty()) { - notifyRoutesAdded(addedRoutes); + updateFilteredRoutesLocked(); } } void removeRoutesOnHandler(List<MediaRoute2Info> routes) { - List<MediaRoute2Info> removedRoutes = new ArrayList<>(); synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.remove(route.getId()); - if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { - removedRoutes.add(route); - } } - mShouldUpdateRoutes = true; - } - if (!removedRoutes.isEmpty()) { - notifyRoutesRemoved(removedRoutes); + updateFilteredRoutesLocked(); } } @@ -874,23 +861,27 @@ public final class MediaRouter2 { synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); - if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { - changedRoutes.add(route); - } } - mShouldUpdateRoutes = true; - } - if (!changedRoutes.isEmpty()) { - notifyRoutesChanged(changedRoutes); + updateFilteredRoutesLocked(); } } + /** Updates filtered routes and dispatch callbacks */ + @GuardedBy("mLock") + void updateFilteredRoutesLocked() { + mFilteredRoutes = + Collections.unmodifiableList( + filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values()))); + mHandler.sendMessage( + obtainMessage(MediaRouter2::dispatchFilteredRoutesChangedLocked, + this, mFilteredRoutes)); + } + /** - * Creates a controller and calls the {@link TransferCallback#onTransfer}. - * If the controller creation has failed, then it calls - * {@link TransferCallback#onTransferFailure}. - * <p> - * Pass {@code null} to sessionInfo for the failure case. + * Creates a controller and calls the {@link TransferCallback#onTransfer}. If the controller + * creation has failed, then it calls {@link TransferCallback#onTransferFailure}. + * + * <p>Pass {@code null} to sessionInfo for the failure case. */ void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) { ControllerCreationRequest matchingRequest = null; @@ -913,12 +904,15 @@ public final class MediaRouter2 { if (sessionInfo == null) { notifyTransferFailure(requestedRoute); return; - } else if (!TextUtils.equals(requestedRoute.getProviderId(), - sessionInfo.getProviderId())) { - Log.w(TAG, "The session's provider ID does not match the requested route's. " - + "(requested route's providerId=" + requestedRoute.getProviderId() - + ", actual providerId=" + sessionInfo.getProviderId() - + ")"); + } else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) { + Log.w( + TAG, + "The session's provider ID does not match the requested route's. " + + "(requested route's providerId=" + + requestedRoute.getProviderId() + + ", actual providerId=" + + sessionInfo.getProviderId() + + ")"); notifyTransferFailure(requestedRoute); return; } @@ -927,9 +921,12 @@ public final class MediaRouter2 { // When the old controller is released before transferred, treat it as a failure. // This could also happen when transfer is requested twice or more. if (!oldController.scheduleRelease()) { - Log.w(TAG, "createControllerOnHandler: " - + "Ignoring controller creation for released old controller. " - + "oldController=" + oldController); + Log.w( + TAG, + "createControllerOnHandler: " + + "Ignoring controller creation for released old controller. " + + "oldController=" + + oldController); if (!sessionInfo.isSystemSession()) { new RoutingController(sessionInfo).release(); } @@ -971,15 +968,21 @@ public final class MediaRouter2 { } if (matchingController == null) { - Log.w(TAG, "updateControllerOnHandler: Matching controller not found. uniqueSessionId=" - + sessionInfo.getId()); + Log.w( + TAG, + "updateControllerOnHandler: Matching controller not found. uniqueSessionId=" + + sessionInfo.getId()); return; } RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo(); if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { - Log.w(TAG, "updateControllerOnHandler: Provider IDs are not matched. old=" - + oldInfo.getProviderId() + ", new=" + sessionInfo.getProviderId()); + Log.w( + TAG, + "updateControllerOnHandler: Provider IDs are not matched. old=" + + oldInfo.getProviderId() + + ", new=" + + sessionInfo.getProviderId()); return; } @@ -1000,24 +1003,31 @@ public final class MediaRouter2 { if (matchingController == null) { if (DEBUG) { - Log.d(TAG, "releaseControllerOnHandler: Matching controller not found. " - + "uniqueSessionId=" + sessionInfo.getId()); + Log.d( + TAG, + "releaseControllerOnHandler: Matching controller not found. " + + "uniqueSessionId=" + + sessionInfo.getId()); } return; } RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo(); if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) { - Log.w(TAG, "releaseControllerOnHandler: Provider IDs are not matched. old=" - + oldInfo.getProviderId() + ", new=" + sessionInfo.getProviderId()); + Log.w( + TAG, + "releaseControllerOnHandler: Provider IDs are not matched. old=" + + oldInfo.getProviderId() + + ", new=" + + sessionInfo.getProviderId()); return; } matchingController.releaseInternal(/* shouldReleaseSession= */ false); } - void onRequestCreateControllerByManagerOnHandler(RoutingSessionInfo oldSession, - MediaRoute2Info route, long managerRequestId) { + void onRequestCreateControllerByManagerOnHandler( + RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) { RoutingController controller; if (oldSession.isSystemSession()) { controller = getSystemController(); @@ -1033,17 +1043,17 @@ public final class MediaRouter2 { } /** - * Returns whether this router is created with {@link #getInstance(Context, String)}. - * This kind of router can control the target app's media routing. + * Returns whether this router is created with {@link #getInstance(Context, String)}. This kind + * of router can control the target app's media routing. */ private boolean isSystemRouter() { return mClientPackageName != null; } /** - * Returns a {@link RoutingSessionInfo} which has the client package name. - * The client package name is set only when the given sessionInfo doesn't have it. - * Should only used for system media routers. + * Returns a {@link RoutingSessionInfo} which has the client package name. The client package + * name is set only when the given sessionInfo doesn't have it. Should only used for system + * media routers. */ private RoutingSessionInfo ensureClientPackageNameForSystemSession( @NonNull RoutingSessionInfo sessionInfo) { @@ -1057,41 +1067,44 @@ public final class MediaRouter2 { .build(); } - private List<MediaRoute2Info> getSortedRoutes(List<MediaRoute2Info> routes, - RouteDiscoveryPreference preference) { - if (!preference.shouldRemoveDuplicates()) { + private List<MediaRoute2Info> getSortedRoutes( + List<MediaRoute2Info> routes, List<String> packageOrder) { + if (packageOrder.isEmpty()) { return routes; } Map<String, Integer> packagePriority = new ArrayMap<>(); - int count = preference.getDeduplicationPackageOrder().size(); + int count = packageOrder.size(); for (int i = 0; i < count; i++) { // the last package will have 1 as the priority - packagePriority.put(preference.getDeduplicationPackageOrder().get(i), count - i); + packagePriority.put(packageOrder.get(i), count - i); } ArrayList<MediaRoute2Info> sortedRoutes = new ArrayList<>(routes); // take the negative for descending order - sortedRoutes.sort(Comparator.comparingInt( - r -> -packagePriority.getOrDefault(r.getPackageName(), 0))); + sortedRoutes.sort( + Comparator.comparingInt(r -> -packagePriority.getOrDefault(r.getPackageName(), 0))); return sortedRoutes; } - private List<MediaRoute2Info> filterRoutes(List<MediaRoute2Info> routes, - RouteDiscoveryPreference discoveryPreference) { + @GuardedBy("mLock") + private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked( + List<MediaRoute2Info> routes) { Set<String> deduplicationIdSet = new ArraySet<>(); List<MediaRoute2Info> filteredRoutes = new ArrayList<>(); - for (MediaRoute2Info route : getSortedRoutes(routes, discoveryPreference)) { - if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) { + for (MediaRoute2Info route : + getSortedRoutes(routes, mDiscoveryPreference.getDeduplicationPackageOrder())) { + if (!route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { continue; } - if (!discoveryPreference.getAllowedPackages().isEmpty() + if (!mDiscoveryPreference.getAllowedPackages().isEmpty() && (route.getPackageName() == null - || !discoveryPreference.getAllowedPackages() - .contains(route.getPackageName()))) { + || !mDiscoveryPreference + .getAllowedPackages() + .contains(route.getPackageName()))) { continue; } - if (discoveryPreference.shouldRemoveDuplicates()) { + if (mDiscoveryPreference.shouldRemoveDuplicates()) { if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) { continue; } @@ -1102,6 +1115,25 @@ public final class MediaRouter2 { return filteredRoutes; } + private List<MediaRoute2Info> filterRoutesWithIndividualPreference( + List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) { + List<MediaRoute2Info> filteredRoutes = new ArrayList<>(); + for (MediaRoute2Info route : routes) { + if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) { + continue; + } + if (!discoveryPreference.getAllowedPackages().isEmpty() + && (route.getPackageName() == null + || !discoveryPreference + .getAllowedPackages() + .contains(route.getPackageName()))) { + continue; + } + filteredRoutes.add(route); + } + return filteredRoutes; + } + private void updateAllRoutesFromManager() { if (!isSystemRouter()) { return; @@ -1111,23 +1143,24 @@ public final class MediaRouter2 { for (MediaRoute2Info route : sManager.getAllRoutes()) { mRoutes.put(route.getId(), route); } - mShouldUpdateRoutes = true; + updateFilteredRoutesLocked(); } } private void notifyRoutesAdded(List<MediaRoute2Info> routes) { - for (RouteCallbackRecord record: mRouteCallbackRecords) { - List<MediaRoute2Info> filteredRoutes = filterRoutes(routes, record.mPreference); + for (RouteCallbackRecord record : mRouteCallbackRecords) { + List<MediaRoute2Info> filteredRoutes = + filterRoutesWithIndividualPreference(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { - record.mExecutor.execute( - () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); + record.mExecutor.execute(() -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); } } } private void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - for (RouteCallbackRecord record: mRouteCallbackRecords) { - List<MediaRoute2Info> filteredRoutes = filterRoutes(routes, record.mPreference); + for (RouteCallbackRecord record : mRouteCallbackRecords) { + List<MediaRoute2Info> filteredRoutes = + filterRoutesWithIndividualPreference(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); @@ -1136,8 +1169,9 @@ public final class MediaRouter2 { } private void notifyRoutesChanged(List<MediaRoute2Info> routes) { - for (RouteCallbackRecord record: mRouteCallbackRecords) { - List<MediaRoute2Info> filteredRoutes = filterRoutes(routes, record.mPreference); + for (RouteCallbackRecord record : mRouteCallbackRecords) { + List<MediaRoute2Info> filteredRoutes = + filterRoutesWithIndividualPreference(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); @@ -1146,46 +1180,42 @@ public final class MediaRouter2 { } private void notifyPreferredFeaturesChanged(List<String> features) { - for (RouteCallbackRecord record: mRouteCallbackRecords) { + for (RouteCallbackRecord record : mRouteCallbackRecords) { record.mExecutor.execute( () -> record.mRouteCallback.onPreferredFeaturesChanged(features)); } } private void notifyTransfer(RoutingController oldController, RoutingController newController) { - for (TransferCallbackRecord record: mTransferCallbackRecords) { + for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute( () -> record.mTransferCallback.onTransfer(oldController, newController)); } } private void notifyTransferFailure(MediaRoute2Info route) { - for (TransferCallbackRecord record: mTransferCallbackRecords) { - record.mExecutor.execute( - () -> record.mTransferCallback.onTransferFailure(route)); + for (TransferCallbackRecord record : mTransferCallbackRecords) { + record.mExecutor.execute(() -> record.mTransferCallback.onTransferFailure(route)); } } private void notifyStop(RoutingController controller) { - for (TransferCallbackRecord record: mTransferCallbackRecords) { - record.mExecutor.execute( - () -> record.mTransferCallback.onStop(controller)); + for (TransferCallbackRecord record : mTransferCallbackRecords) { + record.mExecutor.execute(() -> record.mTransferCallback.onStop(controller)); } } private void notifyControllerUpdated(RoutingController controller) { - for (ControllerCallbackRecord record: mControllerCallbackRecords) { + for (ControllerCallbackRecord record : mControllerCallbackRecords) { record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller)); } } - /** - * Callback for receiving events about media route discovery. - */ + /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** - * Called when routes are added. Whenever you registers a callback, this will - * be invoked with known routes. + * Called when routes are added. Whenever you registers a callback, this will be invoked + * with known routes. * * @param routes the list of routes that have been added. It's never empty. */ @@ -1199,17 +1229,17 @@ public final class MediaRouter2 { public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {} /** - * Called when routes are changed. For example, it is called when the route's name - * or volume have been changed. + * Called when routes are changed. For example, it is called when the route's name or volume + * have been changed. * * @param routes the list of routes that have been changed. It's never empty. */ public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {} /** - * Called when the client app's preferred features are changed. - * When this is called, it is recommended to {@link #getRoutes()} to get the routes - * that are currently available to the app. + * Called when the client app's preferred features are changed. When this is called, it is + * recommended to {@link #getRoutes()} to get the routes that are currently available to the + * app. * * @param preferredFeatures the new preferred features set by the application * @hide @@ -1218,26 +1248,25 @@ public final class MediaRouter2 { public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {} } - /** - * Callback for receiving events on media transfer. - */ + /** Callback for receiving events on media transfer. */ public abstract static class TransferCallback { /** - * Called when a media is transferred between two different routing controllers. - * This can happen by calling {@link #transferTo(MediaRoute2Info)}. - * <p> Override this to start playback with {@code newController}. You may want to get - * the status of the media that is being played with {@code oldController} and resume it - * continuously with {@code newController}. - * After this is called, any callbacks with {@code oldController} will not be invoked - * unless {@code oldController} is the {@link #getSystemController() system controller}. - * You need to {@link RoutingController#release() release} {@code oldController} before - * playing the media with {@code newController}. + * Called when a media is transferred between two different routing controllers. This can + * happen by calling {@link #transferTo(MediaRoute2Info)}. + * + * <p>Override this to start playback with {@code newController}. You may want to get the + * status of the media that is being played with {@code oldController} and resume it + * continuously with {@code newController}. After this is called, any callbacks with {@code + * oldController} will not be invoked unless {@code oldController} is the {@link + * #getSystemController() system controller}. You need to {@link RoutingController#release() + * release} {@code oldController} before playing the media with {@code newController}. * * @param oldController the previous controller that controlled routing * @param newController the new controller to control routing * @see #transferTo(MediaRoute2Info) */ - public void onTransfer(@NonNull RoutingController oldController, + public void onTransfer( + @NonNull RoutingController oldController, @NonNull RoutingController newController) {} /** @@ -1248,61 +1277,58 @@ public final class MediaRouter2 { public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {} /** - * Called when a media routing stops. It can be stopped by a user or a provider. - * App should not continue playing media locally when this method is called. - * The {@code controller} is released before this method is called. + * Called when a media routing stops. It can be stopped by a user or a provider. App should + * not continue playing media locally when this method is called. The {@code controller} is + * released before this method is called. * * @param controller the controller that controlled the stopped media routing */ - public void onStop(@NonNull RoutingController controller) { } + public void onStop(@NonNull RoutingController controller) {} } /** - * A listener interface to send optional app-specific hints when creating a - * {@link RoutingController}. + * A listener interface to send optional app-specific hints when creating a {@link + * RoutingController}. */ public interface OnGetControllerHintsListener { /** - * Called when the {@link MediaRouter2} or the system is about to request - * a media route provider service to create a controller with the given route. - * The {@link Bundle} returned here will be sent to media route provider service as a hint. - * <p> - * Since controller creation can be requested by the {@link MediaRouter2} and the system, - * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. - * The method will be called on the same thread that calls - * {@link #transferTo(MediaRoute2Info)} or the main thread if it is requested by the system. + * Called when the {@link MediaRouter2} or the system is about to request a media route + * provider service to create a controller with the given route. The {@link Bundle} returned + * here will be sent to media route provider service as a hint. + * + * <p>Since controller creation can be requested by the {@link MediaRouter2} and the system, + * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. The + * method will be called on the same thread that calls {@link #transferTo(MediaRoute2Info)} + * or the main thread if it is requested by the system. * * @param route the route to create a controller with - * @return An optional bundle of app-specific arguments to send to the provider, - * or {@code null} if none. The contents of this bundle may affect the result of - * controller creation. + * @return An optional bundle of app-specific arguments to send to the provider, or {@code + * null} if none. The contents of this bundle may affect the result of controller + * creation. * @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle) */ @Nullable Bundle onGetControllerHints(@NonNull MediaRoute2Info route); } - /** - * Callback for receiving {@link RoutingController} updates. - */ + /** Callback for receiving {@link RoutingController} updates. */ public abstract static class ControllerCallback { /** - * Called when a controller is updated. (e.g., when the selected routes of the - * controller is changed or when the volume of the controller is changed.) + * Called when a controller is updated. (e.g., when the selected routes of the controller is + * changed or when the volume of the controller is changed.) * - * @param controller the updated controller. It may be the - * {@link #getSystemController() system controller}. + * @param controller the updated controller. It may be the {@link #getSystemController() + * system controller}. * @see #getSystemController() */ - public void onControllerUpdated(@NonNull RoutingController controller) { } + public void onControllerUpdated(@NonNull RoutingController controller) {} } /** - * A class to control media routing session in media route provider. - * For example, selecting/deselecting/transferring to routes of a session can be done through - * this. Instances are created when - * {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is called, - * which is invoked after {@link #transferTo(MediaRoute2Info)} is called. + * A class to control media routing session in media route provider. For example, + * selecting/deselecting/transferring to routes of a session can be done through this. Instances + * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is + * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called. */ public class RoutingController { private final Object mControllerLock = new Object(); @@ -1339,8 +1365,8 @@ public final class MediaRouter2 { } /** - * Gets the original session ID set by - * {@link RoutingSessionInfo.Builder#Builder(String, String)}. + * Gets the original session ID set by {@link RoutingSessionInfo.Builder#Builder(String, + * String)}. * * @hide */ @@ -1353,8 +1379,8 @@ public final class MediaRouter2 { } /** - * Gets the control hints used to control routing session if available. - * It is set by the media route provider. + * Gets the control hints used to control routing session if available. It is set by the + * media route provider. */ @Nullable public Bundle getControlHints() { @@ -1401,11 +1427,12 @@ public final class MediaRouter2 { /** * Gets the information about how volume is handled on the session. - * <p>Please note that you may not control the volume of the session even when - * you can control the volume of each selected route in the session. * - * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or - * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE} + * <p>Please note that you may not control the volume of the session even when you can + * control the volume of each selected route in the session. + * + * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or {@link + * MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE} */ @MediaRoute2Info.PlaybackVolume public int getVolumeHandling() { @@ -1414,9 +1441,7 @@ public final class MediaRouter2 { } } - /** - * Gets the maximum volume of the session. - */ + /** Gets the maximum volume of the session. */ public int getVolumeMax() { synchronized (mControllerLock) { return mSessionInfo.getVolumeMax(); @@ -1425,11 +1450,10 @@ public final class MediaRouter2 { /** * Gets the current volume of the session. - * <p> - * When it's available, it represents the volume of routing session, which is a group - * of selected routes. Use {@link MediaRoute2Info#getVolume()} - * to get the volume of a route, - * </p> + * + * <p>When it's available, it represents the volume of routing session, which is a group of + * selected routes. Use {@link MediaRoute2Info#getVolume()} to get the volume of a route, + * * @see MediaRoute2Info#getVolume() */ public int getVolume() { @@ -1439,9 +1463,9 @@ public final class MediaRouter2 { } /** - * Returns true if this controller is released, false otherwise. - * If it is released, then all other getters from this instance may return invalid values. - * Also, any operations to this instance will be ignored once released. + * Returns true if this controller is released, false otherwise. If it is released, then all + * other getters from this instance may return invalid values. Also, any operations to this + * instance will be ignored once released. * * @see #release */ @@ -1454,14 +1478,16 @@ public final class MediaRouter2 { /** * Selects a route for the remote session. After a route is selected, the media is expected * to be played to the all the selected routes. This is different from {@link - * MediaRouter2#transferTo(MediaRoute2Info)} transferring to a route}, - * where the media is expected to 'move' from one route to another. - * <p> - * The given route must satisfy all of the following conditions: + * MediaRouter2#transferTo(MediaRoute2Info)} transferring to a route}, where the media is + * expected to 'move' from one route to another. + * + * <p>The given route must satisfy all of the following conditions: + * * <ul> - * <li>It should not be included in {@link #getSelectedRoutes()}</li> - * <li>It should be included in {@link #getSelectableRoutes()}</li> + * <li>It should not be included in {@link #getSelectedRoutes()} + * <li>It should be included in {@link #getSelectableRoutes()} * </ul> + * * If the route doesn't meet any of above conditions, it will be ignored. * * @see #deselectRoute(MediaRoute2Info) @@ -1509,12 +1535,14 @@ public final class MediaRouter2 { /** * Deselects a route from the remote session. After a route is deselected, the media is * expected to be stopped on the deselected route. - * <p> - * The given route must satisfy all of the following conditions: + * + * <p>The given route must satisfy all of the following conditions: + * * <ul> - * <li>It should be included in {@link #getSelectedRoutes()}</li> - * <li>It should be included in {@link #getDeselectableRoutes()}</li> + * <li>It should be included in {@link #getSelectedRoutes()} + * <li>It should be included in {@link #getDeselectableRoutes()} * </ul> + * * If the route doesn't meet any of above conditions, it will be ignored. * * @see #getSelectedRoutes() @@ -1559,8 +1587,8 @@ public final class MediaRouter2 { } /** - * Transfers to a given route for the remote session. The given route must be included - * in {@link RoutingSessionInfo#getTransferableRoutes()}. + * Transfers to a given route for the remote session. The given route must be included in + * {@link RoutingSessionInfo#getTransferableRoutes()}. * * @see RoutingSessionInfo#getSelectedRoutes() * @see RoutingSessionInfo#getTransferableRoutes() @@ -1597,7 +1625,7 @@ public final class MediaRouter2 { * Requests a volume change for the remote session asynchronously. * * @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax} - * (inclusive). + * (inclusive). * @see #getVolume() */ public void setVolume(int volume) { @@ -1634,9 +1662,9 @@ public final class MediaRouter2 { } /** - * Releases this controller and the corresponding session. - * Any operations on this controller after calling this method will be ignored. - * The devices that are playing media will stop playing it. + * Releases this controller and the corresponding session. Any operations on this controller + * after calling this method will be ignored. The devices that are playing media will stop + * playing it. */ public void release() { releaseInternal(/* shouldReleaseSession= */ true); @@ -1644,8 +1672,9 @@ public final class MediaRouter2 { /** * Schedules release of the controller. + * * @return {@code true} if it's successfully scheduled, {@code false} if it's already - * scheduled to be released or released. + * scheduled to be released or released. */ boolean scheduleRelease() { synchronized (mControllerLock) { @@ -1701,11 +1730,15 @@ public final class MediaRouter2 { } if (shouldNotifyStop) { - mHandler.sendMessage(obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, - RoutingController.this)); + mHandler.sendMessage( + obtainMessage( + MediaRouter2::notifyStop, + MediaRouter2.this, + RoutingController.this)); } - if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty() + if (mRouteCallbackRecords.isEmpty() + && mNonSystemRoutingControllers.isEmpty() && mStub != null) { try { mMediaRouterService.unregisterRouter2(mStub); @@ -1720,26 +1753,34 @@ public final class MediaRouter2 { @Override public String toString() { // To prevent logging spam, we only print the ID of each route. - List<String> selectedRoutes = getSelectedRoutes().stream() - .map(MediaRoute2Info::getId).collect(Collectors.toList()); - List<String> selectableRoutes = getSelectableRoutes().stream() - .map(MediaRoute2Info::getId).collect(Collectors.toList()); - List<String> deselectableRoutes = getDeselectableRoutes().stream() - .map(MediaRoute2Info::getId).collect(Collectors.toList()); - - StringBuilder result = new StringBuilder() - .append("RoutingController{ ") - .append("id=").append(getId()) - .append(", selectedRoutes={") - .append(selectedRoutes) - .append("}") - .append(", selectableRoutes={") - .append(selectableRoutes) - .append("}") - .append(", deselectableRoutes={") - .append(deselectableRoutes) - .append("}") - .append(" }"); + List<String> selectedRoutes = + getSelectedRoutes().stream() + .map(MediaRoute2Info::getId) + .collect(Collectors.toList()); + List<String> selectableRoutes = + getSelectableRoutes().stream() + .map(MediaRoute2Info::getId) + .collect(Collectors.toList()); + List<String> deselectableRoutes = + getDeselectableRoutes().stream() + .map(MediaRoute2Info::getId) + .collect(Collectors.toList()); + + StringBuilder result = + new StringBuilder() + .append("RoutingController{ ") + .append("id=") + .append(getId()) + .append(", selectedRoutes={") + .append(selectedRoutes) + .append("}") + .append(", selectableRoutes={") + .append(selectableRoutes) + .append("}") + .append(", deselectableRoutes={") + .append(deselectableRoutes) + .append("}") + .append(" }"); return result.toString(); } @@ -1764,7 +1805,8 @@ public final class MediaRouter2 { } synchronized (mLock) { - return routeIds.stream().map(mRoutes::get) + return routeIds.stream() + .map(mRoutes::get) .filter(Objects::nonNull) .collect(Collectors.toList()); } @@ -1799,7 +1841,9 @@ public final class MediaRouter2 { public final RouteCallback mRouteCallback; public final RouteDiscoveryPreference mPreference; - RouteCallbackRecord(@Nullable Executor executor, @NonNull RouteCallback routeCallback, + RouteCallbackRecord( + @Nullable Executor executor, + @NonNull RouteCallback routeCallback, @Nullable RouteDiscoveryPreference preference) { mRouteCallback = routeCallback; mExecutor = executor; @@ -1827,8 +1871,8 @@ public final class MediaRouter2 { public final Executor mExecutor; public final TransferCallback mTransferCallback; - TransferCallbackRecord(@NonNull Executor executor, - @NonNull TransferCallback transferCallback) { + TransferCallbackRecord( + @NonNull Executor executor, @NonNull TransferCallback transferCallback) { mTransferCallback = transferCallback; mExecutor = executor; } @@ -1854,8 +1898,8 @@ public final class MediaRouter2 { public final Executor mExecutor; public final ControllerCallback mCallback; - ControllerCallbackRecord(@Nullable Executor executor, - @NonNull ControllerCallback callback) { + ControllerCallbackRecord( + @Nullable Executor executor, @NonNull ControllerCallback callback) { mCallback = callback; mExecutor = executor; } @@ -1883,66 +1927,87 @@ public final class MediaRouter2 { public final MediaRoute2Info mRoute; public final RoutingController mOldController; - ControllerCreationRequest(int requestId, long managerRequestId, - @NonNull MediaRoute2Info route, @NonNull RoutingController oldController) { + ControllerCreationRequest( + int requestId, + long managerRequestId, + @NonNull MediaRoute2Info route, + @NonNull RoutingController oldController) { mRequestId = requestId; mManagerRequestId = managerRequestId; mRoute = Objects.requireNonNull(route, "route must not be null"); - mOldController = Objects.requireNonNull(oldController, - "oldController must not be null"); + mOldController = + Objects.requireNonNull(oldController, "oldController must not be null"); } } class MediaRouter2Stub extends IMediaRouter2.Stub { @Override - public void notifyRouterRegistered(List<MediaRoute2Info> currentRoutes, - RoutingSessionInfo currentSystemSessionInfo) { - mHandler.sendMessage(obtainMessage(MediaRouter2::syncRoutesOnHandler, - MediaRouter2.this, currentRoutes, currentSystemSessionInfo)); + public void notifyRouterRegistered( + List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2::syncRoutesOnHandler, + MediaRouter2.this, + currentRoutes, + currentSystemSessionInfo)); } @Override public void notifyRoutesAdded(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2::addRoutesOnHandler, - MediaRouter2.this, routes)); + mHandler.sendMessage( + obtainMessage(MediaRouter2::addRoutesOnHandler, MediaRouter2.this, routes)); } @Override public void notifyRoutesRemoved(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2::removeRoutesOnHandler, - MediaRouter2.this, routes)); + mHandler.sendMessage( + obtainMessage(MediaRouter2::removeRoutesOnHandler, MediaRouter2.this, routes)); } @Override public void notifyRoutesChanged(List<MediaRoute2Info> routes) { - mHandler.sendMessage(obtainMessage(MediaRouter2::changeRoutesOnHandler, - MediaRouter2.this, routes)); + mHandler.sendMessage( + obtainMessage(MediaRouter2::changeRoutesOnHandler, MediaRouter2.this, routes)); } @Override public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) { - mHandler.sendMessage(obtainMessage(MediaRouter2::createControllerOnHandler, - MediaRouter2.this, requestId, sessionInfo)); + mHandler.sendMessage( + obtainMessage( + MediaRouter2::createControllerOnHandler, + MediaRouter2.this, + requestId, + sessionInfo)); } @Override public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) { - mHandler.sendMessage(obtainMessage(MediaRouter2::updateControllerOnHandler, - MediaRouter2.this, sessionInfo)); + mHandler.sendMessage( + obtainMessage( + MediaRouter2::updateControllerOnHandler, + MediaRouter2.this, + sessionInfo)); } @Override public void notifySessionReleased(RoutingSessionInfo sessionInfo) { - mHandler.sendMessage(obtainMessage(MediaRouter2::releaseControllerOnHandler, - MediaRouter2.this, sessionInfo)); + mHandler.sendMessage( + obtainMessage( + MediaRouter2::releaseControllerOnHandler, + MediaRouter2.this, + sessionInfo)); } @Override - public void requestCreateSessionByManager(long managerRequestId, - RoutingSessionInfo oldSession, MediaRoute2Info route) { - mHandler.sendMessage(obtainMessage( - MediaRouter2::onRequestCreateControllerByManagerOnHandler, - MediaRouter2.this, oldSession, route, managerRequestId)); + public void requestCreateSessionByManager( + long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { + mHandler.sendMessage( + obtainMessage( + MediaRouter2::onRequestCreateControllerByManagerOnHandler, + MediaRouter2.this, + oldSession, + route, + managerRequestId)); } } @@ -1952,57 +2017,21 @@ public final class MediaRouter2 { @Override public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) { updateAllRoutesFromManager(); - - List<MediaRoute2Info> filteredRoutes; - synchronized (mLock) { - filteredRoutes = filterRoutes(routes, mDiscoveryPreference); - } - if (filteredRoutes.isEmpty()) { - return; - } - for (RouteCallbackRecord record: mRouteCallbackRecords) { - record.mExecutor.execute( - () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); - } } @Override public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) { updateAllRoutesFromManager(); - - List<MediaRoute2Info> filteredRoutes; - synchronized (mLock) { - filteredRoutes = filterRoutes(routes, mDiscoveryPreference); - } - if (filteredRoutes.isEmpty()) { - return; - } - for (RouteCallbackRecord record: mRouteCallbackRecords) { - record.mExecutor.execute( - () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); - } } @Override public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) { updateAllRoutesFromManager(); - - List<MediaRoute2Info> filteredRoutes; - synchronized (mLock) { - filteredRoutes = filterRoutes(routes, mDiscoveryPreference); - } - if (filteredRoutes.isEmpty()) { - return; - } - for (RouteCallbackRecord record: mRouteCallbackRecords) { - record.mExecutor.execute( - () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); - } } @Override - public void onTransferred(@NonNull RoutingSessionInfo oldSession, - @NonNull RoutingSessionInfo newSession) { + public void onTransferred( + @NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) { if (!oldSession.isSystemSession() && !TextUtils.equals(mClientPackageName, oldSession.getClientPackageName())) { return; @@ -2018,7 +2047,6 @@ public final class MediaRouter2 { return; } - RoutingController oldController; if (oldSession.isSystemSession()) { mSystemController.setRoutingSessionInfo( @@ -2041,8 +2069,8 @@ public final class MediaRouter2 { } @Override - public void onTransferFailed(@NonNull RoutingSessionInfo session, - @NonNull MediaRoute2Info route) { + public void onTransferFailed( + @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) { if (!session.isSystemSession() && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) { return; @@ -2083,8 +2111,8 @@ public final class MediaRouter2 { } @Override - public void onDiscoveryPreferenceChanged(@NonNull String packageName, - @NonNull RouteDiscoveryPreference preference) { + public void onDiscoveryPreferenceChanged( + @NonNull String packageName, @NonNull RouteDiscoveryPreference preference) { if (!TextUtils.equals(mClientPackageName, packageName)) { return; } diff --git a/media/java/android/media/NearbyDevice.java b/media/java/android/media/NearbyDevice.java index c203e7b2b88b..dbcc6b716c8f 100644 --- a/media/java/android/media/NearbyDevice.java +++ b/media/java/android/media/NearbyDevice.java @@ -22,9 +22,10 @@ import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; /** * A parcelable representing a nearby device that can be used for media transfer. @@ -35,6 +36,7 @@ import java.lang.annotation.RetentionPolicy; * <li>a range zone specifying how far away this device is from the device with the media route. * </li> * </ul> + * * @hide */ @SystemApi @@ -69,7 +71,7 @@ public final class NearbyDevice implements Parcelable { * * @hide */ - @IntDef(prefix = { "RANGE_" }, value = { + @IntDef(prefix = {"RANGE_"}, value = { RANGE_UNKNOWN, RANGE_FAR, RANGE_LONG, @@ -77,7 +79,8 @@ public final class NearbyDevice implements Parcelable { RANGE_WITHIN_REACH }) @Retention(RetentionPolicy.SOURCE) - public @interface RangeZone {} + public @interface RangeZone { + } /** * Gets a human-readable string of the range zone. @@ -102,8 +105,17 @@ public final class NearbyDevice implements Parcelable { } } - @NonNull private final String mMediaRoute2Id; - @RangeZone private final int mRangeZone; + /** + * A list stores all the range and list from far to close, used for range comparison. + */ + private static final List<Integer> RANGE_WEIGHT_LIST = + Arrays.asList(RANGE_UNKNOWN, + RANGE_FAR, RANGE_LONG, RANGE_CLOSE, RANGE_WITHIN_REACH); + + @NonNull + private final String mMediaRoute2Id; + @RangeZone + private final int mRangeZone; /** Creates a device object with the given ID and range zone. */ public NearbyDevice(@NonNull String mediaRoute2Id, @RangeZone int rangeZone) { @@ -129,6 +141,22 @@ public final class NearbyDevice implements Parcelable { } }; + /** + * Compares two ranges and return result. + * + * @return 0 means two ranges are the same, -1 means first range is closer, 1 means farther + * + * @hide + */ + public static int compareRangeZones(@RangeZone int rangeZone, @RangeZone int anotherRangeZone) { + if (rangeZone == anotherRangeZone) { + return 0; + } else { + return RANGE_WEIGHT_LIST.indexOf(rangeZone) > RANGE_WEIGHT_LIST.indexOf( + anotherRangeZone) ? -1 : 1; + } + } + @Override public int describeContents() { return 0; diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index c7599623c465..3984ee9aa007 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -35,6 +35,7 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; +import android.media.NearbyDevice; import android.text.TextUtils; import android.util.Log; @@ -77,6 +78,8 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { private int mConnectedRecord; private int mState; + @NearbyDevice.RangeZone + private int mRangeZone = NearbyDevice.RANGE_UNKNOWN; protected final Context mContext; protected final MediaRoute2Info mRouteInfo; @@ -136,6 +139,14 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { getId()); } + public @NearbyDevice.RangeZone int getRangeZone() { + return mRangeZone; + } + + public void setRangeZone(@NearbyDevice.RangeZone int rangeZone) { + mRangeZone = rangeZone; + } + /** * Get name from MediaDevice. * @@ -319,6 +330,11 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { } } + // Both devices have same connection status, compare the range zone + if (NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()) != 0) { + return NearbyDevice.compareRangeZones(getRangeZone(), another.getRangeZone()); + } + if (mType == another.mType) { // Check device is muting expected device if (isMutingExpectedDevice()) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index c122a37d0f79..179a498b7397 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -31,6 +31,7 @@ import android.bluetooth.BluetoothDevice; import android.content.Context; import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; +import android.media.NearbyDevice; import android.os.Parcel; import com.android.settingslib.bluetooth.A2dpProfile; @@ -200,6 +201,18 @@ public class MediaDeviceTest { } @Test + public void compareTo_differentRange_sortWithRange() { + mBluetoothMediaDevice1.setRangeZone(NearbyDevice.RANGE_FAR); + mBluetoothMediaDevice2.setRangeZone(NearbyDevice.RANGE_CLOSE); + mMediaDevices.add(mBluetoothMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice2); + + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice2); + } + + @Test public void compareTo_carKit_info_carKitFirst() { when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); mMediaDevices.add(mInfoMediaDevice1); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index c0e2b2ef361f..dae63a8b0e3c 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -210,6 +210,7 @@ <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" /> <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" /> <uses-permission android:name="android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS" /> + <uses-permission android:name="android.permission.PROVISION_DEMO_DEVICE" /> <uses-permission android:name="android.permission.QUERY_ADMIN_POLICY" /> <uses-permission android:name="android.permission.UPDATE_DEVICE_MANAGEMENT_RESOURCES" /> <uses-permission android:name="android.permission.FORCE_DEVICE_POLICY_MANAGER_LOGS" /> diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt index 183584227087..3f7e0f0fb527 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewLaunchAnimatorController.kt @@ -33,6 +33,7 @@ import android.view.ViewGroup import android.view.ViewGroupOverlay import android.widget.FrameLayout import com.android.internal.jank.InteractionJankMonitor +import java.util.LinkedList import kotlin.math.min private const val TAG = "GhostedViewLaunchAnimatorController" @@ -81,21 +82,51 @@ open class GhostedViewLaunchAnimatorController( * [backgroundView]. */ private var backgroundDrawable: WrappedDrawable? = null - private val backgroundInsets by lazy { getBackground()?.opticalInsets ?: Insets.NONE } + private val backgroundInsets by lazy { background?.opticalInsets ?: Insets.NONE } private var startBackgroundAlpha: Int = 0xFF private val ghostedViewLocation = IntArray(2) private val ghostedViewState = LaunchAnimator.State() /** - * Return the background of the [ghostedView]. This background will be used to draw the - * background of the background view that is expanding up to the final animation position. This - * is called at the start of the animation. + * The background of the [ghostedView]. This background will be used to draw the background of + * the background view that is expanding up to the final animation position. * * Note that during the animation, the alpha value value of this background will be set to 0, * then set back to its initial value at the end of the animation. */ - protected open fun getBackground(): Drawable? = ghostedView.background + private val background: Drawable? + + init { + /** Find the first view with a background in [view] and its children. */ + fun findBackground(view: View): Drawable? { + if (view.background != null) { + return view.background + } + + // Perform a BFS to find the largest View with background. + val views = LinkedList<View>().apply { + add(view) + } + + while (views.isNotEmpty()) { + val v = views.removeFirst() + if (v.background != null) { + return v.background + } + + if (v is ViewGroup) { + for (i in 0 until v.childCount) { + views.add(v.getChildAt(i)) + } + } + } + + return null + } + + background = findBackground(ghostedView) + } /** * Set the corner radius of [background]. The background is the one that was returned by @@ -113,7 +144,7 @@ open class GhostedViewLaunchAnimatorController( /** Return the current top corner radius of the background. */ protected open fun getCurrentTopCornerRadius(): Float { - val drawable = getBackground() ?: return 0f + val drawable = background ?: return 0f val gradient = findGradientDrawable(drawable) ?: return 0f // TODO(b/184121838): Support more than symmetric top & bottom radius. @@ -122,7 +153,7 @@ open class GhostedViewLaunchAnimatorController( /** Return the current bottom corner radius of the background. */ protected open fun getCurrentBottomCornerRadius(): Float { - val drawable = getBackground() ?: return 0f + val drawable = background ?: return 0f val gradient = findGradientDrawable(drawable) ?: return 0f // TODO(b/184121838): Support more than symmetric top & bottom radius. @@ -162,9 +193,8 @@ open class GhostedViewLaunchAnimatorController( // We wrap the ghosted view background and use it to draw the expandable background. Its // alpha will be set to 0 as soon as we start drawing the expanding background. - val drawable = getBackground() - startBackgroundAlpha = drawable?.alpha ?: 0xFF - backgroundDrawable = WrappedDrawable(drawable) + startBackgroundAlpha = background?.alpha ?: 0xFF + backgroundDrawable = WrappedDrawable(background) backgroundView?.background = backgroundDrawable // Create a ghost of the view that will be moving and fading out. This allows to fade out diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml index 4f95811b21b3..117404bad46e 100644 --- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml @@ -34,17 +34,45 @@ <item name="controls_task_view_width_percentage" translatable="false" format="float" type="dimen">0.45</item> <dimen name="controls_task_view_right_margin">8dp</dimen> - <!-- Distance that the full shade transition takes in order for qs to fully transition to the - shade --> - <dimen name="lockscreen_shade_qs_transition_distance">200dp</dimen> + <!-- Distance that the full shade transition takes in order to complete by tapping on a button + like "expand". --> + <dimen name="lockscreen_shade_transition_by_tap_distance">200dp</dimen> - <!-- Distance that the full shade transition takes in order for scrim to fully transition to - the shade (in alpha) --> - <dimen name="lockscreen_shade_scrim_transition_distance">200dp</dimen> + <!-- Distance that the full shade transition takes in order to complete. --> + <dimen name="lockscreen_shade_full_transition_distance">200dp</dimen> <!-- Distance that the full shade transition takes in order for media to fully transition to - the shade --> + the shade --> <dimen name="lockscreen_shade_media_transition_distance">200dp</dimen> + <!-- Distance that the full shade transition takes in order for scrim to fully transition to + the shade (in alpha) --> + <dimen name="lockscreen_shade_scrim_transition_distance">80dp</dimen> + + <!-- Distance that the full shade transition takes in order for the keyguard content on + NotificationPanelViewController to fully fade (e.g. Clock & Smartspace) --> + <dimen name="lockscreen_shade_npvc_keyguard_content_alpha_transition_distance">80dp</dimen> + + <!-- Distance that the full shade transition takes in order for the notification shell to fully + expand. --> + <dimen name="lockscreen_shade_notif_shelf_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for the Quick Settings to fully + fade and expand. --> + <dimen name="lockscreen_shade_qs_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for depth of the wallpaper to fully + change. + On split-shade, there should be no depth effect, so setting the value to 0. --> + <dimen name="lockscreen_shade_depth_controller_transition_distance">0dp</dimen> + + <!-- Distance that the full shade transition takes in order for the UDFPS Keyguard View to fully + fade. --> + <dimen name="lockscreen_shade_udfps_keyguard_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Used for StatusBar to know that a transition is in progress. At the moment it only checks + whether the progress is > 0, therefore this value is not very important. --> + <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + <dimen name="notification_panel_margin_horizontal">12dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml index 71c195896051..139146181a47 100644 --- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml @@ -24,4 +24,6 @@ <dimen name="keyguard_split_shade_top_margin">72dp</dimen> <dimen name="notification_panel_margin_horizontal">24dp</dimen> + + <dimen name="split_shade_header_height">56dp</dimen> </resources> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 5a7efca3dece..5ca7285d3f73 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1122,13 +1122,40 @@ <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen> <dimen name="media_output_dialog_app_tier_icon_size">20dp</dimen> - <!-- Distance that the full shade transition takes in order for qs to fully transition to the - shade --> - <dimen name="lockscreen_shade_qs_transition_distance">200dp</dimen> + <!-- Distance that the full shade transition takes in order to complete by tapping on a button + like "expand". --> + <dimen name="lockscreen_shade_transition_by_tap_distance">200dp</dimen> + + <!-- Distance that the full shade transition takes in order to complete. --> + <dimen name="lockscreen_shade_full_transition_distance">80dp</dimen> <!-- Distance that the full shade transition takes in order for scrim to fully transition to the shade (in alpha) --> - <dimen name="lockscreen_shade_scrim_transition_distance">80dp</dimen> + <dimen name="lockscreen_shade_scrim_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for the keyguard content on + NotificationPanelViewController to fully fade (e.g. Clock & Smartspace) --> + <dimen name="lockscreen_shade_npvc_keyguard_content_alpha_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for the notification shelf to fully + expand. --> + <dimen name="lockscreen_shade_notif_shelf_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for the Quick Settings to fully + fade and expand. --> + <dimen name="lockscreen_shade_qs_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for depth of the wallpaper to fully + change. --> + <dimen name="lockscreen_shade_depth_controller_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Distance that the full shade transition takes in order for the UDFPS Keyguard View to fully + fade. --> + <dimen name="lockscreen_shade_udfps_keyguard_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> + + <!-- Used for StatusBar to know that a transition is in progress. At the moment it only checks + whether the progress is > 0, therefore this value is not very important. --> + <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen> <!-- Distance that the full shade transition takes in order for media to fully transition to the shade --> diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java index c4b02f62f291..ffd15460ce91 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButton.java @@ -107,10 +107,10 @@ public class EmergencyButton extends Button { return super.performLongClick(); } - void updateEmergencyCallButton(boolean isInCall, boolean isVoiceCapable, boolean simLocked) { + void updateEmergencyCallButton(boolean isInCall, boolean hasTelephonyRadio, boolean simLocked) { boolean visible = false; - if (isVoiceCapable) { - // Emergency calling requires voice capability. + if (hasTelephonyRadio) { + // Emergency calling requires a telephony radio. if (isInCall) { visible = true; // always show "return to call" if phone is off-hook } else { diff --git a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java index e7215b8ebe49..f28910576073 100644 --- a/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java +++ b/packages/SystemUI/src/com/android/keyguard/EmergencyButtonController.java @@ -21,6 +21,7 @@ import static com.android.systemui.DejankUtils.whitelistIpcs; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.PowerManager; import android.os.SystemClock; @@ -116,7 +117,8 @@ public class EmergencyButtonController extends ViewController<EmergencyButton> { if (mView != null) { mView.updateEmergencyCallButton( mTelecomManager != null && mTelecomManager.isInCall(), - mTelephonyManager.isVoiceCapable(), + getContext().getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY), mKeyguardUpdateMonitor.isSimPinVoiceSecure()); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index 50ca447090b5..7e1a02626dc9 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -1072,6 +1072,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold pw.println("WindowMagnificationController (displayId=" + mDisplayId + "):"); pw.println(" mOverlapWithGestureInsets:" + mOverlapWithGestureInsets); pw.println(" mScale:" + mScale); + pw.println(" mWindowBounds:" + mWindowBounds); pw.println(" mMirrorViewBounds:" + (isWindowVisible() ? mMirrorViewBounds : "empty")); pw.println(" mMagnificationFrameBoundary:" + (isWindowVisible() ? mMagnificationFrameBoundary : "empty")); diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index f16ca7dd7361..0c202e09b62e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -30,12 +30,16 @@ import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.media.INearbyMediaDevicesUpdateCallback; import android.media.MediaMetadata; import android.media.MediaRoute2Info; +import android.media.NearbyDevice; import android.media.RoutingSessionInfo; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; +import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -61,6 +65,7 @@ import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.R; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.monet.ColorScheme; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -70,6 +75,9 @@ import com.android.systemui.statusbar.phone.ShadeController; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import javax.inject.Inject; @@ -77,7 +85,8 @@ import javax.inject.Inject; /** * Controller for media output dialog */ -public class MediaOutputController implements LocalMediaManager.DeviceCallback { +public class MediaOutputController implements LocalMediaManager.DeviceCallback, + INearbyMediaDevicesUpdateCallback { private static final String TAG = "MediaOutputController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -95,6 +104,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { private final CommonNotifCollection mNotifCollection; @VisibleForTesting final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); + private final NearbyMediaDevicesManager mNearbyMediaDevicesManager; + private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>(); private MediaController mMediaController; @VisibleForTesting @@ -116,7 +127,8 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { boolean aboveStatusbar, MediaSessionManager mediaSessionManager, LocalBluetoothManager lbm, ShadeController shadeController, ActivityStarter starter, CommonNotifCollection notifCollection, UiEventLogger uiEventLogger, - DialogLaunchAnimator dialogLaunchAnimator) { + DialogLaunchAnimator dialogLaunchAnimator, + Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional) { mContext = context; mPackageName = packageName; mMediaSessionManager = mediaSessionManager; @@ -130,6 +142,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); mUiEventLogger = uiEventLogger; mDialogLaunchAnimator = dialogLaunchAnimator; + mNearbyMediaDevicesManager = nearbyMediaDevicesManagerOptional.orElse(null); mColorActiveItem = Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_active_item_main_content); mColorInactiveItem = Utils.getColorStateListDefaultColor(mContext, @@ -144,6 +157,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { void start(@NonNull Callback cb) { mMediaDevices.clear(); + mNearbyDeviceInfoMap.clear(); + if (mNearbyMediaDevicesManager != null) { + mNearbyMediaDevicesManager.registerNearbyDevicesCallback(this); + } if (!TextUtils.isEmpty(mPackageName)) { for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { if (TextUtils.equals(controller.getPackageName(), mPackageName)) { @@ -187,6 +204,10 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { mLocalMediaManager.stopScan(); } mMediaDevices.clear(); + if (mNearbyMediaDevicesManager != null) { + mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); + } + mNearbyDeviceInfoMap.clear(); } @Override @@ -417,6 +438,15 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { } mMediaDevices.clear(); mMediaDevices.addAll(targetMediaDevices); + attachRangeInfo(); + } + + private void attachRangeInfo() { + for (MediaDevice mediaDevice : mMediaDevices) { + if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) { + mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId())); + } + } } List<MediaDevice> getGroupMediaDevices() { @@ -604,7 +634,9 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { // We show the output group dialog from the output dialog. MediaOutputController controller = new MediaOutputController(mContext, mPackageName, mAboveStatusbar, mMediaSessionManager, mLocalBluetoothManager, mShadeController, - mActivityStarter, mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); + mActivityStarter, mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); + MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, controller); mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog); @@ -629,6 +661,20 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback { || mLocalMediaManager.isMediaSessionAvailableForVolumeControl(); } + @Override + public void onDevicesUpdated(List<NearbyDevice> nearbyDevices) throws RemoteException { + mNearbyDeviceInfoMap.clear(); + for (NearbyDevice nearbyDevice : nearbyDevices) { + mNearbyDeviceInfoMap.put(nearbyDevice.getMediaRoute2Id(), nearbyDevice.getRangeZone()); + } + mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); + } + + @Override + public IBinder asBinder() { + return null; + } + private final MediaController.Callback mCb = new MediaController.Callback() { @Override public void onMetadataChanged(MediaMetadata metadata) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt index 9e252ea1eddc..a7e54801bf47 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt @@ -22,9 +22,11 @@ import android.view.View import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.media.nearby.NearbyMediaDevicesManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.phone.ShadeController +import java.util.Optional import javax.inject.Inject /** @@ -38,7 +40,8 @@ class MediaOutputDialogFactory @Inject constructor( private val starter: ActivityStarter, private val notifCollection: CommonNotifCollection, private val uiEventLogger: UiEventLogger, - private val dialogLaunchAnimator: DialogLaunchAnimator + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager> ) { companion object { var mediaOutputDialog: MediaOutputDialog? = null @@ -50,8 +53,8 @@ class MediaOutputDialogFactory @Inject constructor( mediaOutputDialog?.dismiss() val controller = MediaOutputController(context, packageName, aboveStatusBar, - mediaSessionManager, lbm, shadeController, starter, notifCollection, - uiEventLogger, dialogLaunchAnimator) + mediaSessionManager, lbm, shadeController, starter, notifCollection, + uiEventLogger, dialogLaunchAnimator, nearbyMediaDevicesManagerOptional) val dialog = MediaOutputDialog(context, aboveStatusBar, controller, uiEventLogger) mediaOutputDialog = dialog diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt index 9dd8222ff6a1..3961f079748b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelper.kt @@ -16,6 +16,7 @@ package com.android.systemui.media.taptotransfer +import android.annotation.SuppressLint import android.app.StatusBarManager import android.content.Context import android.media.MediaRoute2Info @@ -23,16 +24,12 @@ import android.util.Log import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.media.taptotransfer.sender.AlmostCloseToEndCast -import com.android.systemui.media.taptotransfer.sender.AlmostCloseToStartCast -import com.android.systemui.media.taptotransfer.sender.TransferFailed -import com.android.systemui.media.taptotransfer.sender.TransferToReceiverTriggered -import com.android.systemui.media.taptotransfer.sender.TransferToThisDeviceSucceeded -import com.android.systemui.media.taptotransfer.sender.TransferToThisDeviceTriggered -import com.android.systemui.media.taptotransfer.sender.TransferToReceiverSucceeded +import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver +import com.android.systemui.media.taptotransfer.sender.ChipStateSender import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import java.io.PrintWriter +import java.lang.IllegalArgumentException import java.util.concurrent.Executor import javax.inject.Inject @@ -46,28 +43,6 @@ class MediaTttCommandLineHelper @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor ) { - /** - * A map from a display state string typed in the command line to the display int it represents. - */ - private val stateStringToStateInt: Map<String, Int> = mapOf( - AlmostCloseToStartCast::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, - AlmostCloseToEndCast::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, - TransferToReceiverTriggered::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, - TransferToThisDeviceTriggered::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, - TransferToReceiverSucceeded::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, - TransferToThisDeviceSucceeded::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, - TransferFailed::class.simpleName!! - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, - FAR_FROM_RECEIVER_STATE - to StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER - ) - init { commandRegistry.registerCommand(SENDER_COMMAND) { SenderCommand() } commandRegistry.registerCommand(RECEIVER_COMMAND) { ReceiverCommand() } @@ -76,21 +51,23 @@ class MediaTttCommandLineHelper @Inject constructor( /** All commands for the sender device. */ inner class SenderCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { - val routeInfo = MediaRoute2Info.Builder("id", args[0]) - .addFeature("feature") - .setPackageName(TEST_PACKAGE_NAME) - .build() - val commandName = args[1] @StatusBarManager.MediaTransferSenderState - val displayState = stateStringToStateInt[commandName] - if (displayState == null) { + val displayState: Int? + try { + displayState = ChipStateSender.getSenderStateIdFromName(commandName) + } catch (ex: IllegalArgumentException) { pw.println("Invalid command name $commandName") return } + @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager + val routeInfo = MediaRoute2Info.Builder("id", args[0]) + .addFeature("feature") + .setPackageName(TEST_PACKAGE_NAME) + .build() statusBarManager.updateMediaTapToTransferSenderDisplay( displayState, routeInfo, @@ -136,6 +113,17 @@ class MediaTttCommandLineHelper @Inject constructor( /** All commands for the receiver device. */ inner class ReceiverCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { + val commandName = args[0] + @StatusBarManager.MediaTransferReceiverState + val displayState: Int? + try { + displayState = ChipStateReceiver.getReceiverStateIdFromName(commandName) + } catch (ex: IllegalArgumentException) { + pw.println("Invalid command name $commandName") + return + } + + @SuppressLint("WrongConstant") // sysui is allowed to call STATUS_BAR_SERVICE val statusBarManager = context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager val routeInfo = MediaRoute2Info.Builder("id", "Test Name") @@ -143,24 +131,12 @@ class MediaTttCommandLineHelper @Inject constructor( .setPackageName(TEST_PACKAGE_NAME) .build() - when(val commandName = args[0]) { - CLOSE_TO_SENDER_STATE -> - statusBarManager.updateMediaTapToTransferReceiverDisplay( - StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, - routeInfo, - null, - null - ) - FAR_FROM_SENDER_STATE -> - statusBarManager.updateMediaTapToTransferReceiverDisplay( - StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER, - routeInfo, - null, - null - ) - else -> - pw.println("Invalid command name $commandName") - } + statusBarManager.updateMediaTapToTransferReceiverDisplay( + displayState, + routeInfo, + null, + null + ) } override fun help(pw: PrintWriter) { @@ -173,11 +149,5 @@ class MediaTttCommandLineHelper @Inject constructor( const val SENDER_COMMAND = "media-ttt-chip-sender" @VisibleForTesting const val RECEIVER_COMMAND = "media-ttt-chip-receiver" -@VisibleForTesting -const val FAR_FROM_RECEIVER_STATE = "FarFromReceiver" -@VisibleForTesting -const val CLOSE_TO_SENDER_STATE = "CloseToSender" -@VisibleForTesting -const val FAR_FROM_SENDER_STATE = "FarFromSender" private const val CLI_TAG = "MediaTransferCli" private const val TEST_PACKAGE_NAME = "com.android.systemui" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt new file mode 100644 index 000000000000..3cc99a8ef77e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.taptotransfer.common + +/** + * A superclass chip state that will be subclassed by the sender chip and receiver chip. + */ +interface ChipInfoCommon { + /** + * Returns the amount of time the given chip state should display on the screen before it times + * out and disappears. + */ + fun getTimeoutMs(): Long +} + +const val DEFAULT_TIMEOUT_MILLIS = 3000L diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt index 9c4b39d9cb77..71cacac7f4df 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt @@ -19,14 +19,18 @@ package com.android.systemui.media.taptotransfer.common import android.annotation.LayoutRes import android.annotation.SuppressLint import android.content.Context +import android.content.pm.PackageManager import android.graphics.PixelFormat +import android.graphics.drawable.Drawable +import android.os.PowerManager +import android.os.SystemClock +import android.util.Log import android.view.Gravity import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.annotation.VisibleForTesting import com.android.internal.widget.CachingIconView import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main @@ -40,14 +44,18 @@ import com.android.systemui.util.view.ViewUtil * * Subclasses need to override and implement [updateChipView], which is where they can control what * gets displayed to the user. + * + * The generic type T is expected to contain all the information necessary for the subclasses to + * display the chip in a certain state, since they receive <T> in [updateChipView]. */ -abstract class MediaTttChipControllerCommon<T : MediaTttChipState>( +abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( internal val context: Context, internal val logger: MediaTttLogger, private val windowManager: WindowManager, private val viewUtil: ViewUtil, @Main private val mainExecutor: DelayableExecutor, private val tapGestureDetector: TapGestureDetector, + private val powerManager: PowerManager, @LayoutRes private val chipLayoutRes: Int ) { /** The window layout parameters we'll use when attaching the view to a window. */ @@ -64,10 +72,10 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>( } /** The chip view currently being displayed. Null if the chip is not being displayed. */ - var chipView: ViewGroup? = null + private var chipView: ViewGroup? = null /** A [Runnable] that, when run, will cancel the pending timeout of the chip. */ - var cancelChipViewTimeout: Runnable? = null + private var cancelChipViewTimeout: Runnable? = null /** * Displays the chip with the current state. @@ -75,7 +83,7 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>( * This method handles inflating and attaching the view, then delegates to [updateChipView] to * display the correct information in the chip. */ - fun displayChip(chipState: T) { + fun displayChip(chipInfo: T) { val oldChipView = chipView if (chipView == null) { chipView = LayoutInflater @@ -84,19 +92,25 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>( } val currentChipView = chipView!! - updateChipView(chipState, currentChipView) + updateChipView(chipInfo, currentChipView) // Add view if necessary if (oldChipView == null) { tapGestureDetector.addOnGestureDetectedCallback(TAG, this::onScreenTapped) windowManager.addView(chipView, windowLayoutParams) + // Wake the screen so the user will see the chip + powerManager.wakeUp( + SystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_APPLICATION, + "com.android.systemui:media_tap_to_transfer_activated" + ) } // Cancel and re-set the chip timeout each time we get a new state. cancelChipViewTimeout?.run() cancelChipViewTimeout = mainExecutor.executeDelayed( { removeChip(MediaTttRemovalReason.REASON_TIMEOUT) }, - chipState.getTimeoutMs() + chipInfo.getTimeoutMs() ) } @@ -117,20 +131,28 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>( } /** - * A method implemented by subclasses to update [currentChipView] based on [chipState]. + * A method implemented by subclasses to update [currentChipView] based on [chipInfo]. */ - abstract fun updateChipView(chipState: T, currentChipView: ViewGroup) + abstract fun updateChipView(chipInfo: T, currentChipView: ViewGroup) /** * An internal method to set the icon on the view. * * This is in the common superclass since both the sender and the receiver show an icon. + * + * @param appPackageName the package name of the app playing the media. Will be used to fetch + * the app icon and app name if overrides aren't provided. */ - internal fun setIcon(chipState: T, currentChipView: ViewGroup) { + internal fun setIcon( + currentChipView: ViewGroup, + appPackageName: String?, + appIconDrawableOverride: Drawable? = null, + appNameOverride: CharSequence? = null, + ) { val appIconView = currentChipView.requireViewById<CachingIconView>(R.id.app_icon) - appIconView.contentDescription = chipState.getAppName(context) + appIconView.contentDescription = appNameOverride ?: getAppName(appPackageName) - val appIcon = chipState.getAppIcon(context) + val appIcon = appIconDrawableOverride ?: getAppIcon(appPackageName) val visibility = if (appIcon != null) { View.VISIBLE } else { @@ -140,6 +162,30 @@ abstract class MediaTttChipControllerCommon<T : MediaTttChipState>( appIconView.visibility = visibility } + /** Returns the icon of the app playing the media or null if we can't find it. */ + private fun getAppIcon(appPackageName: String?): Drawable? { + appPackageName ?: return null + return try { + context.packageManager.getApplicationIcon(appPackageName) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Cannot find icon for package $appPackageName", e) + null + } + } + + /** Returns the name of the app playing the media or null if we can't find it. */ + private fun getAppName(appPackageName: String?): String? { + appPackageName ?: return null + return try { + context.packageManager.getApplicationInfo( + appPackageName, PackageManager.ApplicationInfoFlags.of(0) + ).loadLabel(context.packageManager).toString() + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Cannot find name for package $appPackageName", e) + null + } + } + private fun onScreenTapped(e: MotionEvent) { val view = chipView ?: return // If the tap is within the chip bounds, we shouldn't hide the chip (in case users think the @@ -159,4 +205,3 @@ object MediaTttRemovalReason { const val REASON_TIMEOUT = "TIMEOUT" const val REASON_SCREEN_TAP = "SCREEN_TAP" } - diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt deleted file mode 100644 index 6f6018170f98..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipState.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.media.taptotransfer.common - -import android.content.Context -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.util.Log - -/** - * A superclass chip state that will be subclassed by the sender chip and receiver chip. - * - * @property appPackageName the package name of the app playing the media. Will be used to fetch the - * app icon and app name. - */ -open class MediaTttChipState( - internal val appPackageName: String?, -) { - open fun getAppIcon(context: Context): Drawable? { - appPackageName ?: return null - return try { - context.packageManager.getApplicationIcon(appPackageName) - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Cannot find icon for package $appPackageName", e) - null - } - } - - /** Returns the name of the app playing the media or null if we can't find it. */ - open fun getAppName(context: Context): String? { - appPackageName ?: return null - return try { - context.packageManager.getApplicationInfo( - appPackageName, PackageManager.ApplicationInfoFlags.of(0) - ).loadLabel(context.packageManager).toString() - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Cannot find name for package $appPackageName", e) - null - } - } - - /** - * Returns the amount of time this chip should display on the screen before it times out and - * disappears. [MediaTttChipControllerCommon] will ensure that the timeout resets each time we - * receive a new state. - */ - open fun getTimeoutMs(): Long = DEFAULT_TIMEOUT_MILLIS -} - -private const val DEFAULT_TIMEOUT_MILLIS = 3000L -private val TAG = MediaTttChipState::class.simpleName!! diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt index 6a4b62a8c1ae..a0e803f6bb8d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt @@ -16,35 +16,43 @@ package com.android.systemui.media.taptotransfer.receiver -import android.content.Context -import android.graphics.drawable.Drawable -import com.android.systemui.media.taptotransfer.common.MediaTttChipState +import android.app.StatusBarManager +import com.android.internal.logging.UiEventLogger /** * A class that stores all the information necessary to display the media tap-to-transfer chip on * the receiver device. - * - * @property appIconDrawable a drawable representing the icon of the app playing the media. If - * present, this will be used in [this.getAppIcon] instead of [appPackageName]. - * @property appName a name for the app playing the media. If present, this will be used in - * [this.getAppName] instead of [appPackageName]. */ -class ChipStateReceiver( - appPackageName: String?, - private val appIconDrawable: Drawable?, - private val appName: CharSequence? -) : MediaTttChipState(appPackageName) { - override fun getAppIcon(context: Context): Drawable? { - if (appIconDrawable != null) { - return appIconDrawable - } - return super.getAppIcon(context) - } +enum class ChipStateReceiver( + @StatusBarManager.MediaTransferSenderState val stateInt: Int, + val uiEvent: UiEventLogger.UiEventEnum, +) { + CLOSE_TO_SENDER( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_CLOSE_TO_SENDER, + ), + FAR_FROM_SENDER( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER, + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_FAR_FROM_SENDER, + ); + + companion object { + /** + * Returns the receiver state enum associated with the given [displayState] from + * [StatusBarManager]. + */ + fun getReceiverStateFromId( + @StatusBarManager.MediaTransferReceiverState displayState: Int + ) : ChipStateReceiver = values().first { it.stateInt == displayState } + - override fun getAppName(context: Context): String? { - if (appName != null) { - return appName.toString() - } - return super.getAppName(context) + /** + * Returns the state int from [StatusBarManager] associated with the given sender state + * name. + * + * @param name the name of one of the [ChipStateReceiver] enums. + */ + @StatusBarManager.MediaTransferReceiverState + fun getReceiverStateIdFromName(name: String): Int = valueOf(name).stateInt } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 1a96ddf69b28..44965d705802 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -18,15 +18,19 @@ package com.android.systemui.media.taptotransfer.receiver import android.app.StatusBarManager import android.content.Context +import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.media.MediaRoute2Info import android.os.Handler +import android.os.PowerManager import android.util.Log import android.view.ViewGroup import android.view.WindowManager import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.taptotransfer.common.ChipInfoCommon +import com.android.systemui.media.taptotransfer.common.DEFAULT_TIMEOUT_MILLIS import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.statusbar.CommandQueue @@ -49,14 +53,17 @@ class MediaTttChipControllerReceiver @Inject constructor( viewUtil: ViewUtil, mainExecutor: DelayableExecutor, tapGestureDetector: TapGestureDetector, + powerManager: PowerManager, @Main private val mainHandler: Handler, -) : MediaTttChipControllerCommon<ChipStateReceiver>( + private val uiEventLogger: MediaTttReceiverUiEventLogger, +) : MediaTttChipControllerCommon<ChipReceiverInfo>( context, logger, windowManager, viewUtil, mainExecutor, tapGestureDetector, + powerManager, R.layout.media_ttt_chip_receiver ) { private val commandQueueCallbacks = object : CommandQueue.Callbacks { @@ -82,45 +89,52 @@ class MediaTttChipControllerReceiver @Inject constructor( appIcon: Icon?, appName: CharSequence? ) { - logger.logStateChange(stateIntToString(displayState), routeInfo.id) - when(displayState) { - StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER -> { - val packageName = routeInfo.packageName - if (appIcon == null) { - displayChip(ChipStateReceiver(packageName, null, appName)) - } else { - appIcon.loadDrawableAsync( - context, - Icon.OnDrawableLoadedListener { drawable -> - displayChip( - ChipStateReceiver(packageName, drawable, appName) - )}, - // Notify the listener on the main handler since the listener will update - // the UI. - mainHandler - ) - } - } - StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER -> - removeChip(removalReason = FAR_FROM_SENDER) - else -> - Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState") + val chipState: ChipStateReceiver? = ChipStateReceiver.getReceiverStateFromId(displayState) + val stateName = chipState?.name ?: "Invalid" + logger.logStateChange(stateName, routeInfo.id) + + if (chipState == null) { + Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState") + return + } + uiEventLogger.logReceiverStateChange(chipState) + + if (chipState == ChipStateReceiver.FAR_FROM_SENDER) { + removeChip(removalReason = ChipStateReceiver.FAR_FROM_SENDER::class.simpleName!!) + return + } + if (appIcon == null) { + displayChip(ChipReceiverInfo(routeInfo, appIconDrawableOverride = null, appName)) + return } - } - override fun updateChipView(chipState: ChipStateReceiver, currentChipView: ViewGroup) { - setIcon(chipState, currentChipView) + appIcon.loadDrawableAsync( + context, + Icon.OnDrawableLoadedListener { drawable -> + displayChip(ChipReceiverInfo(routeInfo, drawable, appName)) + }, + // Notify the listener on the main handler since the listener will update + // the UI. + mainHandler + ) } - private fun stateIntToString(@StatusBarManager.MediaTransferReceiverState state: Int): String { - return when (state) { - StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER -> CLOSE_TO_SENDER - StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER -> FAR_FROM_SENDER - else -> "INVALID: $state" - } + override fun updateChipView(chipInfo: ChipReceiverInfo, currentChipView: ViewGroup) { + setIcon( + currentChipView, + chipInfo.routeInfo.packageName, + chipInfo.appIconDrawableOverride, + chipInfo.appNameOverride + ) } } +data class ChipReceiverInfo( + val routeInfo: MediaRoute2Info, + val appIconDrawableOverride: Drawable?, + val appNameOverride: CharSequence? +) : ChipInfoCommon { + override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS +} + private const val RECEIVER_TAG = "MediaTapToTransferRcvr" -private const val CLOSE_TO_SENDER = "CLOSE_TO_SENDER" -private const val FAR_FROM_SENDER = "FAR_FROM_SENDER" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt new file mode 100644 index 000000000000..39a276329a9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.taptotransfer.receiver + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** A class for analytics logging for the media tap-to-transfer chip on the receiver device. */ +@SysUISingleton +class MediaTttReceiverUiEventLogger @Inject constructor(private val logger: UiEventLogger) { + /** Logs that the receiver chip has changed states. */ + fun logReceiverStateChange(chipState: ChipStateReceiver) { + logger.log(chipState.uiEvent) + } +} + +enum class MediaTttReceiverUiEvents(val metricId: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs") + MEDIA_TTT_RECEIVER_CLOSE_TO_SENDER(982), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs") + MEDIA_TTT_RECEIVER_FAR_FROM_SENDER(983); + + override fun getId() = metricId +} diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index 22424a4e9c74..3c6805b4e881 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -16,188 +16,225 @@ package com.android.systemui.media.taptotransfer.sender +import android.app.StatusBarManager import android.content.Context +import android.media.MediaRoute2Info import android.view.View +import androidx.annotation.StringRes +import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R -import com.android.systemui.media.taptotransfer.common.MediaTttChipState +import com.android.systemui.media.taptotransfer.common.DEFAULT_TIMEOUT_MILLIS /** - * A class that stores all the information necessary to display the media tap-to-transfer chip on - * the sender device. + * A class enumerating all the possible states of the media tap-to-transfer chip on the sender + * device. * - * This is a sealed class where each subclass represents a specific chip state. Each subclass can - * contain additional information that is necessary for only that state. + * @property stateInt the integer from [StatusBarManager] corresponding with this state. + * @property stringResId the res ID of the string that should be displayed in the chip. Null if the + * state should not have the chip be displayed. + * @property isMidTransfer true if the state represents that a transfer is currently ongoing. + * @property isTransferFailure true if the state represents that the transfer has failed. + * @property timeout the amount of time this chip should display on the screen before it times out + * and disappears. */ -sealed class ChipStateSender( - appPackageName: String? -) : MediaTttChipState(appPackageName) { - /** Returns a fully-formed string with the text that the chip should display. */ - abstract fun getChipTextString(context: Context): String - - /** Returns true if the loading icon should be displayed and false otherwise. */ - open fun showLoading(): Boolean = false - +enum class ChipStateSender( + @StatusBarManager.MediaTransferSenderState val stateInt: Int, + val uiEvent: UiEventLogger.UiEventEnum, + @StringRes val stringResId: Int?, + val isMidTransfer: Boolean = false, + val isTransferFailure: Boolean = false, + val timeout: Long = DEFAULT_TIMEOUT_MILLIS +) { /** - * Returns a click listener for the undo button on the chip. Returns null if this chip state - * doesn't have an undo button. - * - * @param controllerSender passed as a parameter in case we want to display a new chip state - * when undo is clicked. + * A state representing that the two devices are close but not close enough to *start* a cast to + * the receiver device. The chip will instruct the user to move closer in order to initiate the + * transfer to the receiver. */ - open fun undoClickListener( - controllerSender: MediaTttChipControllerSender - ): View.OnClickListener? = null -} - -/** - * A state representing that the two devices are close but not close enough to *start* a cast to - * the receiver device. The chip will instruct the user to move closer in order to initiate the - * transfer to the receiver. - * - * @property otherDeviceName the name of the other device involved in the transfer. - */ -class AlmostCloseToStartCast( - appPackageName: String?, - private val otherDeviceName: String, -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_move_closer_to_start_cast, otherDeviceName) - } -} - -/** - * A state representing that the two devices are close but not close enough to *end* a cast that's - * currently occurring the receiver device. The chip will instruct the user to move closer in order - * to initiate the transfer from the receiver and back onto this device (the original sender). - * - * @property otherDeviceName the name of the other device involved in the transfer. - */ -class AlmostCloseToEndCast( - appPackageName: String?, - private val otherDeviceName: String, -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_move_closer_to_end_cast, otherDeviceName) - } -} - -/** - * A state representing that a transfer to the receiver device has been initiated (but not - * completed). - * - * @property otherDeviceName the name of the other device involved in the transfer. - */ -class TransferToReceiverTriggered( - appPackageName: String?, - private val otherDeviceName: String -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_transfer_playing_different_device, otherDeviceName) - } - - override fun showLoading() = true - override fun getTimeoutMs() = TRANSFER_TRIGGERED_TIMEOUT_MILLIS -} + ALMOST_CLOSE_TO_START_CAST( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST, + R.string.media_move_closer_to_start_cast, + ), -/** - * A state representing that a transfer from the receiver device and back to this device (the - * sender) has been initiated (but not completed). - */ -class TransferToThisDeviceTriggered( - appPackageName: String?, -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_transfer_playing_this_device) - } + /** + * A state representing that the two devices are close but not close enough to *end* a cast + * that's currently occurring the receiver device. The chip will instruct the user to move + * closer in order to initiate the transfer from the receiver and back onto this device (the + * original sender). + */ + ALMOST_CLOSE_TO_END_CAST( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST, + R.string.media_move_closer_to_end_cast, + ), - override fun showLoading() = true - override fun getTimeoutMs() = TRANSFER_TRIGGERED_TIMEOUT_MILLIS -} + /** + * A state representing that a transfer to the receiver device has been initiated (but not + * completed). + */ + TRANSFER_TO_RECEIVER_TRIGGERED( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED, + R.string.media_transfer_playing_different_device, + isMidTransfer = true, + timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS + ), -/** - * A state representing that a transfer to the receiver device has been successfully completed. - * - * @property otherDeviceName the name of the other device involved in the transfer. - * @property undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ -class TransferToReceiverSucceeded( - appPackageName: String?, - private val otherDeviceName: String, - val undoCallback: IUndoMediaTransferCallback? = null -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_transfer_playing_different_device, otherDeviceName) - } + /** + * A state representing that a transfer from the receiver device and back to this device (the + * sender) has been initiated (but not completed). + */ + TRANSFER_TO_THIS_DEVICE_TRIGGERED( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED, + R.string.media_transfer_playing_this_device, + isMidTransfer = true, + timeout = TRANSFER_TRIGGERED_TIMEOUT_MILLIS + ), - override fun undoClickListener( - controllerSender: MediaTttChipControllerSender - ): View.OnClickListener? { - if (undoCallback == null) { - return null + /** + * A state representing that a transfer to the receiver device has been successfully completed. + */ + TRANSFER_TO_RECEIVER_SUCCEEDED( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED, + R.string.media_transfer_playing_different_device + ) { + override fun undoClickListener( + controllerSender: MediaTttChipControllerSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + uiEventLogger: MediaTttSenderUiEventLogger + ): View.OnClickListener? { + if (undoCallback == null) { + return null + } + return View.OnClickListener { + uiEventLogger.logUndoClicked( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED + ) + undoCallback.onUndoTriggered() + // The external service should eventually send us a TransferToThisDeviceTriggered + // state, but that may take too long to go through the binder and the user may be + // confused ast o why the UI hasn't changed yet. So, we immediately change the UI + // here. + controllerSender.displayChip( + ChipSenderInfo( + TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo, undoCallback + ) + ) + } } + }, - return View.OnClickListener { - this.undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToThisDeviceTriggered state, - // but that may take too long to go through the binder and the user may be confused as - // to why the UI hasn't changed yet. So, we immediately change the UI here. - controllerSender.displayChip( - TransferToThisDeviceTriggered(this.appPackageName) - ) + /** + * A state representing that a transfer back to this device has been successfully completed. + */ + TRANSFER_TO_THIS_DEVICE_SUCCEEDED( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED, + R.string.media_transfer_playing_this_device + ) { + override fun undoClickListener( + controllerSender: MediaTttChipControllerSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + uiEventLogger: MediaTttSenderUiEventLogger + ): View.OnClickListener? { + if (undoCallback == null) { + return null + } + return View.OnClickListener { + uiEventLogger.logUndoClicked( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED + ) + undoCallback.onUndoTriggered() + // The external service should eventually send us a TransferToReceiverTriggered + // state, but that may take too long to go through the binder and the user may be + // confused as to why the UI hasn't changed yet. So, we immediately change the UI + // here. + controllerSender.displayChip( + ChipSenderInfo( + TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo, undoCallback + ) + ) + } } - } -} - -/** - * A state representing that a transfer back to this device has been successfully completed. - * - * @property otherDeviceName the name of the other device involved in the transfer. - * @property undoCallback if present, the callback that should be called when the user clicks the - * undo button. The undo button will only be shown if this is non-null. - */ -class TransferToThisDeviceSucceeded( - appPackageName: String?, - private val otherDeviceName: String, - val undoCallback: IUndoMediaTransferCallback? = null -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_transfer_playing_this_device) - } + }, + + /** A state representing that a transfer to the receiver device has failed. */ + TRANSFER_TO_RECEIVER_FAILED( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED, + R.string.media_transfer_failed, + isTransferFailure = true + ), + + /** A state representing that a transfer back to this device has failed. */ + TRANSFER_TO_THIS_DEVICE_FAILED( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED, + R.string.media_transfer_failed, + isTransferFailure = true + ), + + /** A state representing that this device is far away from any receiver device. */ + FAR_FROM_RECEIVER( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER, + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER, + stringResId = null + ); - override fun undoClickListener( - controllerSender: MediaTttChipControllerSender - ): View.OnClickListener? { - if (undoCallback == null) { + /** + * Returns a fully-formed string with the text that the chip should display. + * + * @param otherDeviceName the name of the other device involved in the transfer. + */ + fun getChipTextString(context: Context, otherDeviceName: String): String? { + if (stringResId == null) { return null } - - return View.OnClickListener { - this.undoCallback.onUndoTriggered() - // The external service should eventually send us a TransferToReceiverTriggered state, - // but that may take too long to go through the binder and the user may be confused as - // to why the UI hasn't changed yet. So, we immediately change the UI here. - controllerSender.displayChip( - TransferToReceiverTriggered( - this.appPackageName, - this.otherDeviceName - ) - ) - } + return context.getString(stringResId, otherDeviceName) } -} -/** A state representing that a transfer has failed. */ -class TransferFailed( - appPackageName: String?, -) : ChipStateSender(appPackageName) { - override fun getChipTextString(context: Context): String { - return context.getString(R.string.media_transfer_failed) + /** + * Returns a click listener for the undo button on the chip. Returns null if this chip state + * doesn't have an undo button. + * + * @param controllerSender passed as a parameter in case we want to display a new chip state + * when undo is clicked. + * @param undoCallback if present, the callback that should be called when the user clicks the + * undo button. The undo button will only be shown if this is non-null. + */ + open fun undoClickListener( + controllerSender: MediaTttChipControllerSender, + routeInfo: MediaRoute2Info, + undoCallback: IUndoMediaTransferCallback?, + uiEventLogger: MediaTttSenderUiEventLogger + ): View.OnClickListener? = null + + companion object { + /** + * Returns the sender state enum associated with the given [displayState] from + * [StatusBarManager]. + */ + fun getSenderStateFromId( + @StatusBarManager.MediaTransferSenderState displayState: Int, + ): ChipStateSender = values().first { it.stateInt == displayState } + + /** + * Returns the state int from [StatusBarManager] associated with the given sender state + * name. + * + * @param name the name of one of the [ChipStateSender] enums. + */ + @StatusBarManager.MediaTransferSenderState + fun getSenderStateIdFromName(name: String): Int = valueOf(name).stateInt } } // Give the Transfer*Triggered states a longer timeout since those states represent an active // process and we should keep the user informed about it as long as possible (but don't allow it to // continue indefinitely). -private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 15000L
\ No newline at end of file +private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 15000L diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index da2aac4d5ad7..9f5ec7e1a330 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -19,6 +19,7 @@ package com.android.systemui.media.taptotransfer.sender import android.app.StatusBarManager import android.content.Context import android.media.MediaRoute2Info +import android.os.PowerManager import android.util.Log import android.view.View import android.view.ViewGroup @@ -28,6 +29,7 @@ import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.taptotransfer.common.ChipInfoCommon import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttRemovalReason @@ -50,13 +52,16 @@ class MediaTttChipControllerSender @Inject constructor( viewUtil: ViewUtil, @Main mainExecutor: DelayableExecutor, tapGestureDetector: TapGestureDetector, -) : MediaTttChipControllerCommon<ChipStateSender>( + powerManager: PowerManager, + private val uiEventLogger: MediaTttSenderUiEventLogger +) : MediaTttChipControllerCommon<ChipSenderInfo>( context, logger, windowManager, viewUtil, mainExecutor, tapGestureDetector, + powerManager, R.layout.media_ttt_chip ) { private var currentlyDisplayedChipState: ChipStateSender? = null @@ -82,104 +87,83 @@ class MediaTttChipControllerSender @Inject constructor( routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback? ) { - logger.logStateChange(stateIntToString(displayState), routeInfo.id) - val appPackageName = routeInfo.packageName - val otherDeviceName = routeInfo.name.toString() - val chipState = when(displayState) { - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST -> - AlmostCloseToStartCast(appPackageName, otherDeviceName) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST -> - AlmostCloseToEndCast(appPackageName, otherDeviceName) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED -> - TransferToReceiverTriggered(appPackageName, otherDeviceName) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED -> - TransferToThisDeviceTriggered(appPackageName) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED -> - TransferToReceiverSucceeded(appPackageName, otherDeviceName, undoCallback) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED -> - TransferToThisDeviceSucceeded(appPackageName, otherDeviceName, undoCallback) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED, - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED -> - TransferFailed(appPackageName) - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER -> { - removeChip(removalReason = FAR_FROM_RECEIVER) - null - } - else -> { - Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState") - null - } + val chipState: ChipStateSender? = ChipStateSender.getSenderStateFromId(displayState) + val stateName = chipState?.name ?: "Invalid" + logger.logStateChange(stateName, routeInfo.id) + + if (chipState == null) { + Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState") + return } + uiEventLogger.logSenderStateChange(chipState) - chipState?.let { - displayChip(it) + if (chipState == ChipStateSender.FAR_FROM_RECEIVER) { + removeChip(removalReason = ChipStateSender.FAR_FROM_RECEIVER::class.simpleName!!) + } else { + displayChip(ChipSenderInfo(chipState, routeInfo, undoCallback)) } } /** Displays the chip view for the given state. */ - override fun updateChipView(chipState: ChipStateSender, currentChipView: ViewGroup) { + override fun updateChipView( + chipInfo: ChipSenderInfo, + currentChipView: ViewGroup) { + val chipState = chipInfo.state currentlyDisplayedChipState = chipState // App icon - setIcon(chipState, currentChipView) + setIcon(currentChipView, chipInfo.routeInfo.packageName) // Text + val otherDeviceName = chipInfo.routeInfo.name.toString() currentChipView.requireViewById<TextView>(R.id.text).apply { - text = chipState.getChipTextString(context) + text = chipState.getChipTextString(context, otherDeviceName) } // Loading currentChipView.requireViewById<View>(R.id.loading).visibility = - if (chipState.showLoading()) { View.VISIBLE } else { View.GONE } + chipState.isMidTransfer.visibleIfTrue() + // Undo val undoView = currentChipView.requireViewById<View>(R.id.undo) - val undoClickListener = chipState.undoClickListener(this) + val undoClickListener = chipState.undoClickListener( + this, chipInfo.routeInfo, chipInfo.undoCallback, uiEventLogger + ) undoView.setOnClickListener(undoClickListener) - undoView.visibility = if (undoClickListener != null) { View.VISIBLE } else { View.GONE } + undoView.visibility = (undoClickListener != null).visibleIfTrue() // Failure - val showFailure = chipState is TransferFailed currentChipView.requireViewById<View>(R.id.failure_icon).visibility = - if (showFailure) { View.VISIBLE } else { View.GONE } + chipState.isTransferFailure.visibleIfTrue() } override fun removeChip(removalReason: String) { // Don't remove the chip if we're mid-transfer since the user should still be able to // see the status of the transfer. (But do remove it if it's finally timed out.) - if ((currentlyDisplayedChipState is TransferToReceiverTriggered || - currentlyDisplayedChipState is TransferToThisDeviceTriggered) - && removalReason != MediaTttRemovalReason.REASON_TIMEOUT) { + if (currentlyDisplayedChipState?.isMidTransfer == true + && removalReason != MediaTttRemovalReason.REASON_TIMEOUT) { return } super.removeChip(removalReason) currentlyDisplayedChipState = null } - private fun stateIntToString(@StatusBarManager.MediaTransferSenderState state: Int): String { - return when(state) { - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST -> - "ALMOST_CLOSE_TO_START_CAST" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST -> - "ALMOST_CLOSE_TO_END_CAST" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED -> - "TRANSFER_TO_RECEIVER_TRIGGERED" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED -> - "TRANSFER_TO_THIS_DEVICE_TRIGGERED" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED -> - "TRANSFER_TO_RECEIVER_SUCCEEDED" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_SUCCEEDED -> - "TRANSFER_TO_THIS_DEVICE_SUCCEEDED" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED -> - "TRANSFER_TO_RECEIVER_FAILED" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED -> - "TRANSFER_TO_THIS_DEVICE_FAILED" - StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER -> - FAR_FROM_RECEIVER - else -> "INVALID: $state" + private fun Boolean.visibleIfTrue(): Int { + return if (this) { + View.VISIBLE + } else { + View.GONE } } } +data class ChipSenderInfo( + val state: ChipStateSender, + val routeInfo: MediaRoute2Info, + val undoCallback: IUndoMediaTransferCallback? = null +) : ChipInfoCommon { + override fun getTimeoutMs() = state.timeout +} + const val SENDER_TAG = "MediaTapToTransferSender" -private const val FAR_FROM_RECEIVER = "FAR_FROM_RECEIVER" diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt new file mode 100644 index 000000000000..af3c1b60bacf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLogger.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.taptotransfer.sender + +import android.util.Log +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** A class for analytics logging for the media tap-to-transfer chip on the sender device. */ +@SysUISingleton +class MediaTttSenderUiEventLogger @Inject constructor(private val logger: UiEventLogger) { + /** Logs that the sender chip has changed states. */ + fun logSenderStateChange(chipState: ChipStateSender) { + logger.log(chipState.uiEvent) + } + + /** + * Logs that the undo button was clicked. + * + * @param undoUiEvent the uiEvent specific to which undo button was clicked. + */ + fun logUndoClicked(undoUiEvent: UiEventLogger.UiEventEnum) { + val isUndoEvent = + undoUiEvent == MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED + || undoUiEvent == + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED + if (!isUndoEvent) { + Log.w( + MediaTttSenderUiEventLogger::class.simpleName!!, + "Must pass an undo-specific UiEvent." + ) + return + } + logger.log(undoUiEvent) + } +} + +enum class MediaTttSenderUiEvents(val metricId: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "The undo button on the media ttt chip on the sender device was clicked " + + "to undo the transfer to the receiver device") + MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED(971), + @UiEvent(doc = "The undo button on the media ttt chip on the sender device was clicked " + + "to undo the transfer back to this device") + MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED(972), + + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST(973), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST(974), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED(975), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED(976), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED(977), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED(978), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED(979), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED(980), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_SENDER_* docs") + MEDIA_TTT_SENDER_FAR_FROM_RECEIVER(981); + + override fun getId() = metricId +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index e19483ae7845..b4e20fd7f32b 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -97,7 +97,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { "SHOWING_AUTO_SAVER_SUGGESTION", }; - private static final String ACTION_SHOW_BATTERY_SETTINGS = "PNW.batterySettings"; + private static final String ACTION_SHOW_BATTERY_SAVER_SETTINGS = "PNW.batterySaverSettings"; private static final String ACTION_START_SAVER = "PNW.startSaver"; private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning"; private static final String ACTION_CLICKED_TEMP_WARNING = "PNW.clickedTempWarning"; @@ -138,6 +138,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Receiver mReceiver = new Receiver(); private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY); + private final Intent mOpenBatterySaverSettings = + settings(Settings.ACTION_BATTERY_SAVER_SETTINGS); private final boolean mUseSevereDialog; private int mBatteryLevel; @@ -287,7 +289,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { .setStyle(new Notification.BigTextStyle().bigText(contentText)) .setVisibility(Notification.VISIBILITY_PUBLIC); if (hasBatterySettings()) { - nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS)); + nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SAVER_SETTINGS)); } // Make the notification red if the percentage goes below a certain amount or the time // remaining estimate is disabled @@ -748,7 +750,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { public void init() { IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_SHOW_BATTERY_SETTINGS); + filter.addAction(ACTION_SHOW_BATTERY_SAVER_SETTINGS); filter.addAction(ACTION_START_SAVER); filter.addAction(ACTION_DISMISSED_WARNING); filter.addAction(ACTION_CLICKED_TEMP_WARNING); @@ -768,9 +770,9 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); Slog.i(TAG, "Received " + action); - if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) { + if (action.equals(ACTION_SHOW_BATTERY_SAVER_SETTINGS)) { dismissLowBatteryNotification(); - mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT); + mContext.startActivityAsUser(mOpenBatterySaverSettings, UserHandle.CURRENT); } else if (action.equals(ACTION_START_SAVER)) { setSaverMode(true, true); dismissLowBatteryNotification(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index 00142799c541..865f09337fa3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -199,9 +199,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { /** */ public void setListening(boolean listening, boolean expanded) { - // TODO(218268829): checking for split shade is workaround but when proper fix lands - // "|| mShouldUseSplitNotificationShade" should be removed - setListening(listening && (expanded || mShouldUseSplitNotificationShade)); + setListening(listening && expanded); if (mView.isListening()) { refreshAllTiles(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 17f42b1a3a43..5adb9e55a9df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -99,15 +99,57 @@ class LockscreenShadeTransitionController @Inject constructor( internal var pulseHeightAnimator: ValueAnimator? = null /** - * Distance that the full shade transition takes in order for scrim to fully transition to - * the shade (in alpha) + * Distance that the full shade transition takes in order to complete. + */ + private var fullTransitionDistance = 0 + + /** + * Distance that the full transition takes in order for us to fully transition to the shade by + * tapping on a button, such as "expand". + */ + private var fullTransitionDistanceByTap = 0 + + /** + * Distance that the full shade transition takes in order for scrim to fully transition to the + * shade (in alpha) */ private var scrimTransitionDistance = 0 /** - * Distance that the full transition takes in order for us to fully transition to the shade + * Distance that the full shade transition takes in order for the notification shelf to fully + * expand. */ - private var fullTransitionDistance = 0 + private var notificationShelfTransitionDistance = 0 + + /** + * Distance that the full shade transition takes in order for the Quick Settings to fully fade + * and expand. + */ + private var qsTransitionDistance = 0 + + /** + * Distance that the full shade transition takes in order for the keyguard content on + * NotificationPanelViewController to fully fade (e.g. Clock & Smartspace). + */ + private var npvcKeyguardContentAlphaTransitionDistance = 0 + + /** + * Distance that the full shade transition takes in order for depth of the wallpaper to fully + * change. + */ + private var depthControllerTransitionDistance = 0 + + /** + * Distance that the full shade transition takes in order for the UDFPS Keyguard View to fully + * fade. + */ + private var udfpsTransitionDistance = 0 + + /** + * Used for StatusBar to know that a transition is in progress. At the moment it only checks + * whether the progress is > 0, therefore this value is not very important. + */ + private var statusBarTransitionDistance = 0 /** * Flag to make sure that the dragDownAmount is applied to the listeners even when in the @@ -130,7 +172,7 @@ class LockscreenShadeTransitionController @Inject constructor( * The distance until we're showing the notifications when pulsing */ val distanceUntilShowingPulsingNotifications - get() = scrimTransitionDistance + get() = fullTransitionDistance /** * The udfpsKeyguardViewController if it exists. @@ -177,10 +219,24 @@ class LockscreenShadeTransitionController @Inject constructor( } private fun updateResources() { + fullTransitionDistance = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_full_transition_distance) + fullTransitionDistanceByTap = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_transition_by_tap_distance) scrimTransitionDistance = context.resources.getDimensionPixelSize( R.dimen.lockscreen_shade_scrim_transition_distance) - fullTransitionDistance = context.resources.getDimensionPixelSize( + notificationShelfTransitionDistance = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_notif_shelf_transition_distance) + qsTransitionDistance = context.resources.getDimensionPixelSize( R.dimen.lockscreen_shade_qs_transition_distance) + npvcKeyguardContentAlphaTransitionDistance = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_npvc_keyguard_content_alpha_transition_distance) + depthControllerTransitionDistance = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_depth_controller_transition_distance) + udfpsTransitionDistance = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_udfps_keyguard_transition_distance) + statusBarTransitionDistance = context.resources.getDimensionPixelSize( + R.dimen.lockscreen_shade_status_bar_transition_distance) useSplitShade = Utils.shouldUseSplitNotificationShade(context.resources) } @@ -337,11 +393,16 @@ class LockscreenShadeTransitionController @Inject constructor( if (field != value || forceApplyAmount) { field = value if (!nsslController.isInLockedDownShade() || field == 0f || forceApplyAmount) { - qSDragProgress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance) - nsslController.setTransitionToFullShadeAmount(field, qSDragProgress) + val notificationShelfProgress = + MathUtils.saturate(dragDownAmount / notificationShelfTransitionDistance) + nsslController.setTransitionToFullShadeAmount(field, notificationShelfProgress) + + qSDragProgress = MathUtils.saturate(dragDownAmount / qsTransitionDistance) qS.setTransitionToFullShadeAmount(field, qSDragProgress) + notificationPanelController.setTransitionToFullShadeAmount(field, false /* animate */, 0 /* delay */) + mediaHierarchyManager.setTransitionToFullShadeAmount(field) transitionToShadeAmountCommon(field) } @@ -357,11 +418,23 @@ class LockscreenShadeTransitionController @Inject constructor( private fun transitionToShadeAmountCommon(dragDownAmount: Float) { val scrimProgress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance) scrimController.setTransitionToFullShadeProgress(scrimProgress) + // Fade out all content only visible on the lockscreen - notificationPanelController.setKeyguardOnlyContentAlpha(1.0f - scrimProgress) - depthController.transitionToFullShadeProgress = scrimProgress - udfpsKeyguardViewController?.setTransitionToFullShadeProgress(scrimProgress) - centralSurfaces.setTransitionToFullShadeProgress(scrimProgress) + val npvcProgress = + MathUtils.saturate(dragDownAmount / npvcKeyguardContentAlphaTransitionDistance) + notificationPanelController.setKeyguardOnlyContentAlpha(1.0f - npvcProgress) + + if (depthControllerTransitionDistance > 0) { + val depthProgress = + MathUtils.saturate(dragDownAmount / depthControllerTransitionDistance) + depthController.transitionToFullShadeProgress = depthProgress + } + + val udfpsProgress = MathUtils.saturate(dragDownAmount / udfpsTransitionDistance) + udfpsKeyguardViewController?.setTransitionToFullShadeProgress(udfpsProgress) + + val statusBarProgress = MathUtils.saturate(dragDownAmount / statusBarTransitionDistance) + centralSurfaces.setTransitionToFullShadeProgress(statusBarProgress) } private fun setDragDownAmountAnimated( @@ -404,7 +477,7 @@ class LockscreenShadeTransitionController @Inject constructor( // be a couple of frames later. if we're setting it to 0, it will use the // default inset and therefore flicker dragDownAmount = 1f - setDragDownAmountAnimated(fullTransitionDistance.toFloat(), delay = delay) { + setDragDownAmountAnimated(fullTransitionDistanceByTap.toFloat(), delay = delay) { // End listener: // Reset dragDownAmount = 0f diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 24f44e6aa613..5b9dbd0f3361 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -404,7 +404,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private float mBackgroundXFactor = 1f; - private boolean mQsExpanded; + /** + * Indicates QS are full screen and pushing notifications out of the screen. + * It's different from QS just being expanded as in split shade QS can be expanded and + * still don't take full screen nor influence notifications. + */ + private boolean mQsFullScreen; private boolean mForwardScrollable; private boolean mBackwardScrollable; private NotificationShelf mShelf; @@ -1130,7 +1135,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.LAYOUT_ALGORITHM) private void updateAlgorithmLayoutMinHeight() { - mAmbientState.setLayoutMinHeight(mQsExpanded || isHeadsUpTransition() + mAmbientState.setLayoutMinHeight(mQsFullScreen || isHeadsUpTransition() ? getLayoutMinHeight() : 0); } @@ -1361,7 +1366,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable translationY = 0; if (mShouldShowShelfOnly) { stackHeight = mTopPadding + mShelf.getIntrinsicHeight(); - } else if (mQsExpanded) { + } else if (mQsFullScreen) { int stackStartPosition = mContentHeight - mTopPadding + mIntrinsicPadding; int stackEndPosition = mMaxTopPadding + mShelf.getIntrinsicHeight(); if (stackStartPosition <= stackEndPosition) { @@ -2318,7 +2323,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) private void updateScrollability() { - boolean scrollable = !mQsExpanded && getScrollRange() > 0; + boolean scrollable = !mQsFullScreen && getScrollRange() > 0; if (scrollable != mScrollable) { mScrollable = scrollable; setFocusable(scrollable); @@ -4839,14 +4844,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) - public void setQsExpanded(boolean qsExpanded) { - mQsExpanded = qsExpanded; + public void setQsFullScreen(boolean qsFullScreen) { + mQsFullScreen = qsFullScreen; updateAlgorithmLayoutMinHeight(); updateScrollability(); } - boolean isQsExpanded() { - return mQsExpanded; + boolean isQsFullScreen() { + return mQsFullScreen; } @ShadeViewRefactor(RefactorComponent.SHADE_VIEW) @@ -5807,10 +5812,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mSwipeHelper.resetExposedMenuView(animate, force); } - boolean isUsingSplitNotificationShade() { - return mShouldUseSplitNotificationShade; - } - static boolean matchesSelection( ExpandableNotificationRow row, @SelectedRows int selection) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index e64a0d6220e1..7df8e7df486e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -111,13 +111,13 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; +import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; @@ -1086,8 +1086,8 @@ public class NotificationStackScrollLayoutController { } } - public void setQsExpanded(boolean expanded) { - mView.setQsExpanded(expanded); + public void setQsFullScreen(boolean fullScreen) { + mView.setQsFullScreen(fullScreen); updateShowEmptyShadeView(); } @@ -1204,7 +1204,7 @@ public class NotificationStackScrollLayoutController { public void updateShowEmptyShadeView() { Trace.beginSection("NSSLC.updateShowEmptyShadeView"); mShowEmptyShadeView = mBarState != KEYGUARD - && (!mView.isQsExpanded() || mView.isUsingSplitNotificationShade()) + && !mView.isQsFullScreen() && getVisibleNotificationCount() == 0; mView.updateEmptyShadeView( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 22a47aad4e2f..cc8a70388ed0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -358,6 +358,12 @@ public class NotificationPanelViewController extends PanelViewController { private boolean mConflictingQsExpansionGesture; private boolean mPanelExpanded; + + /** + * Indicates that QS is in expanded state which can happen by: + * - single pane shade: expanding shade and then expanding QS + * - split shade: just expanding shade (QS are expanded automatically) + */ private boolean mQsExpanded; private boolean mQsExpandedWhenExpandingStarted; private boolean mQsFullyExpanded; @@ -401,7 +407,11 @@ public class NotificationPanelViewController extends PanelViewController { private boolean mIsExpanding; private boolean mBlockTouches; - // Used for two finger gesture as well as accessibility shortcut to QS. + + /** + * Determines if QS should be already expanded when expanding shade. + * Used for split shade, two finger gesture as well as accessibility shortcut to QS. + */ private boolean mQsExpandImmediate; private boolean mTwoFingerQsExpandPossible; private String mHeaderDebugInfo; @@ -1467,6 +1477,11 @@ public class NotificationPanelViewController extends PanelViewController { constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END); if (animate) { ChangeBounds transition = new ChangeBounds(); + if (mShouldUseSplitNotificationShade) { + // Excluding media from the transition on split-shade, as it doesn't transition + // horizontally properly. + transition.excludeTarget(R.id.status_view_media_container, true); + } transition.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); transition.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); TransitionManager.beginDelayedTransition(mNotificationContainerParent, transition); @@ -1684,11 +1699,16 @@ public class NotificationPanelViewController extends PanelViewController { if (mQsExpanded) { mQsExpandImmediate = true; - mNotificationStackScrollLayoutController.setShouldShowShelfOnly(true); + setShowShelfOnly(true); } super.collapse(delayed, speedUpFactor); } + private void setShowShelfOnly(boolean shelfOnly) { + mNotificationStackScrollLayoutController.setShouldShowShelfOnly( + shelfOnly && !mShouldUseSplitNotificationShade); + } + public void closeQs() { cancelQsAnimation(); setQsExpansion(mQsMinExpansionHeight); @@ -1725,7 +1745,7 @@ public class NotificationPanelViewController extends PanelViewController { public void expandWithQs() { if (isQsExpansionEnabled()) { mQsExpandImmediate = true; - mNotificationStackScrollLayoutController.setShouldShowShelfOnly(true); + setShowShelfOnly(true); } if (isFullyCollapsed()) { expand(true /* animate */); @@ -1924,7 +1944,15 @@ public class NotificationPanelViewController extends PanelViewController { mFalsingManager.isFalseTouch(QS_COLLAPSE); } - flingSettings(vel, expandsQs && !isCancelMotionEvent ? FLING_EXPAND : FLING_COLLAPSE); + int flingType; + if (expandsQs && !isCancelMotionEvent) { + flingType = FLING_EXPAND; + } else if (mShouldUseSplitNotificationShade) { + flingType = FLING_HIDE; + } else { + flingType = FLING_COLLAPSE; + } + flingSettings(vel, flingType); } private void logQsSwipeDown(float y) { @@ -1989,8 +2017,10 @@ public class NotificationPanelViewController extends PanelViewController { return false; } final int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f - && mBarState != KEYGUARD && !mQsExpanded && isQsExpansionEnabled()) { + boolean collapsedQs = !mQsExpanded && !mShouldUseSplitNotificationShade; + boolean expandedShadeCollapsedQs = getExpandedFraction() == 1f && mBarState != KEYGUARD + && collapsedQs && isQsExpansionEnabled(); + if (action == MotionEvent.ACTION_DOWN && expandedShadeCollapsedQs) { // Down in the empty area while fully expanded - go to QS. mQsTracking = true; traceQsJank(true /* startTracing */, false /* wasCancelled */); @@ -2005,7 +2035,7 @@ public class NotificationPanelViewController extends PanelViewController { } if (!mQsExpandImmediate && mQsTracking) { onQsTouch(event); - if (!mConflictingQsExpansionGesture) { + if (!mConflictingQsExpansionGesture && !mShouldUseSplitNotificationShade) { return true; } } @@ -2019,7 +2049,7 @@ public class NotificationPanelViewController extends PanelViewController { < mStatusBarMinHeight) { mMetricsLogger.count(COUNTER_PANEL_OPEN_QS, 1); mQsExpandImmediate = true; - mNotificationStackScrollLayoutController.setShouldShowShelfOnly(true); + setShowShelfOnly(true); requestPanelHeightUpdate(); // Normally, we start listening when the panel is expanded, but here we need to start @@ -2090,6 +2120,9 @@ public class NotificationPanelViewController extends PanelViewController { if (!isFullyCollapsed()) { return; } + if (mShouldUseSplitNotificationShade) { + mQsExpandImmediate = true; + } mExpectingSynthesizedDown = true; onTrackingStarted(); updatePanelExpanded(); @@ -2296,12 +2329,10 @@ public class NotificationPanelViewController extends PanelViewController { } private void updateQsState() { - mNotificationStackScrollLayoutController.setQsExpanded(mQsExpanded); + boolean qsFullScreen = mQsExpanded && !mShouldUseSplitNotificationShade; + mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen); mNotificationStackScrollLayoutController.setScrollingEnabled( - mBarState != KEYGUARD - && (!mQsExpanded - || mQsExpansionFromOverscroll - || mShouldUseSplitNotificationShade)); + mBarState != KEYGUARD && (!qsFullScreen || mQsExpansionFromOverscroll)); if (mKeyguardUserSwitcherController != null && mQsExpanded && !mStackScrollerOverscrolling) { @@ -2346,7 +2377,7 @@ public class NotificationPanelViewController extends PanelViewController { private void updateQsExpansion() { if (mQs == null) return; final float squishiness; - if (mQsExpandImmediate || mQsExpanded) { + if ((mQsExpandImmediate || mQsExpanded) && !mShouldUseSplitNotificationShade) { squishiness = 1; } else if (mLockscreenShadeTransitionController.getQSDragProgress() > 0) { squishiness = mLockscreenShadeTransitionController.getQSDragProgress(); @@ -3194,7 +3225,7 @@ public class NotificationPanelViewController extends PanelViewController { setListening(true); } mQsExpandImmediate = false; - mNotificationStackScrollLayoutController.setShouldShowShelfOnly(false); + setShowShelfOnly(false); mTwoFingerQsExpandPossible = false; updateTrackingHeadsUp(null); mExpandingFromHeadsUp = false; @@ -3250,9 +3281,7 @@ public class NotificationPanelViewController extends PanelViewController { mScrimController.onTrackingStarted(); if (mQsFullyExpanded) { mQsExpandImmediate = true; - if (!mShouldUseSplitNotificationShade) { - mNotificationStackScrollLayoutController.setShouldShowShelfOnly(true); - } + setShowShelfOnly(true); } if (mBarState == KEYGUARD || mBarState == StatusBarState.SHADE_LOCKED) { mAffordanceHelper.animateHideLeftRightIcon(); @@ -3957,10 +3986,6 @@ public class NotificationPanelViewController extends PanelViewController { mNotificationStackScrollLayoutController.runAfterAnimationFinished(r); } - public void setScrollingEnabled(boolean b) { - mNotificationStackScrollLayoutController.setScrollingEnabled(b); - } - private Runnable mHideExpandedRunnable; private final Runnable mMaybeHideExpandedRunnable = new Runnable() { @Override @@ -4871,7 +4896,11 @@ public class NotificationPanelViewController extends PanelViewController { private void updateQSMinHeight() { float previousMin = mQsMinExpansionHeight; - mQsMinExpansionHeight = mKeyguardShowing ? 0 : mQs.getQsMinExpansionHeight(); + if (mKeyguardShowing || mShouldUseSplitNotificationShade) { + mQsMinExpansionHeight = 0; + } else { + mQsMinExpansionHeight = mQs.getQsMinExpansionHeight(); + } if (mQsExpansionHeight == previousMin) { mQsExpansionHeight = mQsMinExpansionHeight; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index d7c8a9160807..ddc907666f1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -584,13 +584,6 @@ public class UserSwitcherController implements Dumpable { .setPackage(mCreateSupervisedUserPackage) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - // TODO(b/209659998): [to-be-removed] fallback activity for supervised user creation. - if (mContext.getPackageManager().resolveActivity(intent, 0) == null) { - intent.setPackage(null) - .setClassName("com.android.settings", - "com.android.settings.users.AddSupervisedUserActivity"); - } - mContext.startActivity(intent); } diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java index 71d8e3344937..7e3bce589f7e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java @@ -199,10 +199,13 @@ public class Utils { /** * Gets the {@link R.dimen#split_shade_header_height}. * - * Currently, it's the same as {@link com.android.internal.R.dimen#quick_qs_offset_height}. + * It should be fine to not ignore cutouts as split shade might not want to react to them: + * for split shade header, which is only on bigger screens, either cutout won't be a problem + * (it's usually centered and in split shade that's likely empty area) or we probably want to + * handle it differently. */ public static int getSplitShadeStatusBarHeight(Context context) { - return SystemBarUtils.getQuickQsOffsetHeight(context); + return context.getResources().getDimensionPixelSize(R.dimen.split_shade_header_height); } /** diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index c3c3f90539fd..60567c49bfdf 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -51,6 +51,7 @@ import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; @@ -116,6 +117,7 @@ public final class WMShell extends CoreStartable private final CommandQueue mCommandQueue; private final ConfigurationController mConfigurationController; + private final KeyguardStateController mKeyguardStateController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final NavigationModeController mNavigationModeController; private final ScreenLifecycle mScreenLifecycle; @@ -129,7 +131,7 @@ public final class WMShell extends CoreStartable private KeyguardUpdateMonitorCallback mSplitScreenKeyguardCallback; private KeyguardUpdateMonitorCallback mPipKeyguardCallback; private KeyguardUpdateMonitorCallback mOneHandedKeyguardCallback; - private KeyguardUpdateMonitorCallback mCompatUIKeyguardCallback; + private KeyguardStateController.Callback mCompatUIKeyguardCallback; private WakefulnessLifecycle.Observer mWakefulnessObserver; @Inject @@ -143,6 +145,7 @@ public final class WMShell extends CoreStartable Optional<DragAndDrop> dragAndDropOptional, CommandQueue commandQueue, ConfigurationController configurationController, + KeyguardStateController keyguardStateController, KeyguardUpdateMonitor keyguardUpdateMonitor, NavigationModeController navigationModeController, ScreenLifecycle screenLifecycle, @@ -154,6 +157,7 @@ public final class WMShell extends CoreStartable super(context); mCommandQueue = commandQueue; mConfigurationController = configurationController; + mKeyguardStateController = keyguardStateController; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mNavigationModeController = navigationModeController; mScreenLifecycle = screenLifecycle; @@ -362,13 +366,13 @@ public final class WMShell extends CoreStartable @VisibleForTesting void initCompatUi(CompatUI sizeCompatUI) { - mCompatUIKeyguardCallback = new KeyguardUpdateMonitorCallback() { + mCompatUIKeyguardCallback = new KeyguardStateController.Callback() { @Override - public void onKeyguardOccludedChanged(boolean occluded) { - sizeCompatUI.onKeyguardOccludedChanged(occluded); + public void onKeyguardShowingChanged() { + sizeCompatUI.onKeyguardShowingChanged(mKeyguardStateController.isShowing()); } }; - mKeyguardUpdateMonitor.registerCallback(mCompatUIKeyguardCallback); + mKeyguardStateController.addCallback(mCompatUIKeyguardCallback); } void initDragAndDrop(DragAndDrop dragAndDrop) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java index 2be30b39763e..13e582196ffb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java @@ -42,6 +42,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; @@ -50,6 +51,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Optional; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -66,6 +69,8 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { private NotificationEntryManager mNotificationEntryManager = mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); + private NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( + NearbyMediaDevicesManager.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl; @@ -80,7 +85,8 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mMediaOutputController); mMediaOutputBaseDialogImpl.onCreate(new Bundle()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index 789822e262d5..6230700a6a2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -33,9 +33,11 @@ import android.content.Context; import android.graphics.drawable.Icon; import android.media.MediaDescription; import android.media.MediaMetadata; +import android.media.NearbyDevice; import android.media.RoutingSessionInfo; import android.media.session.MediaController; import android.media.session.MediaSessionManager; +import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.text.TextUtils; @@ -51,17 +53,21 @@ import com.android.settingslib.media.MediaDevice; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.phone.ShadeController; +import com.google.common.collect.ImmutableList; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; +import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -86,6 +92,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class); + private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class); + private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class); private MediaMetadata mMediaMetadata = mock(MediaMetadata.class); private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class); private ShadeController mShadeController = mock(ShadeController.class); @@ -93,12 +101,15 @@ public class MediaOutputControllerTest extends SysuiTestCase { private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); + private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( + NearbyMediaDevicesManager.class); private Context mSpyContext; private MediaOutputController mMediaOutputController; private LocalMediaManager mLocalMediaManager; private List<MediaController> mMediaControllers = new ArrayList<>(); private List<MediaDevice> mMediaDevices = new ArrayList<>(); + private List<NearbyDevice> mNearbyDevices = new ArrayList<>(); private MediaDescription mMediaDescription; private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>(); @@ -115,7 +126,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); @@ -127,6 +139,13 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID); mMediaDevices.add(mMediaDevice1); mMediaDevices.add(mMediaDevice2); + + when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID); + when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); + when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID); + when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); + mNearbyDevices.add(mNearbyDevice1); + mNearbyDevices.add(mNearbyDevice2); } @Test @@ -159,7 +178,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void start_withoutPackageName_verifyMediaControllerInit() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); mMediaOutputController.start(mCb); @@ -167,6 +187,13 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test + public void start_nearbyMediaDevicesManagerNotNull_registersNearbyDevicesCallback() { + mMediaOutputController.start(mCb); + + verify(mNearbyMediaDevicesManager).registerNearbyDevicesCallback(any()); + } + + @Test public void stop_withPackageName_verifyMediaControllerDeinit() { mMediaOutputController.start(mCb); reset(mMediaController); @@ -180,7 +207,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void stop_withoutPackageName_verifyMediaControllerDeinit() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); mMediaOutputController.start(mCb); @@ -189,6 +217,39 @@ public class MediaOutputControllerTest extends SysuiTestCase { verify(mMediaController, never()).unregisterCallback(any()); } + + @Test + public void stop_nearbyMediaDevicesManagerNotNull_unregistersNearbyDevicesCallback() { + mMediaOutputController.start(mCb); + reset(mMediaController); + + mMediaOutputController.stop(); + + verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any()); + } + + @Test + public void onDevicesUpdated_unregistersNearbyDevicesCallback() throws RemoteException { + mMediaOutputController.start(mCb); + + mMediaOutputController.onDevicesUpdated(ImmutableList.of()); + + verify(mNearbyMediaDevicesManager).unregisterNearbyDevicesCallback(any()); + } + + @Test + public void onDeviceListUpdate_withNearbyDevices_updatesRangeInformation() + throws RemoteException { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_CLOSE); + verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_FAR); + } + @Test public void onDeviceListUpdate_verifyDeviceListCallback() { mMediaOutputController.start(mCb); @@ -451,7 +512,8 @@ public class MediaOutputControllerTest extends SysuiTestCase { public void getNotificationLargeIcon_withoutPackageName_returnsNull() { mMediaOutputController = new MediaOutputController(mSpyContext, null, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotifCollection, mUiEventLogger, mDialogLaunchAnimator); + mNotifCollection, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); assertThat(mMediaOutputController.getNotificationIcon()).isNull(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java index 8a3ea562269d..cb52e7c20464 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java @@ -37,6 +37,7 @@ import com.android.settingslib.media.LocalMediaManager; import com.android.settingslib.media.MediaDevice; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; @@ -48,6 +49,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; +import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -67,6 +69,8 @@ public class MediaOutputDialogTest extends SysuiTestCase { mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); + private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( + NearbyMediaDevicesManager.class); private MediaOutputDialog mMediaOutputDialog; private MediaOutputController mMediaOutputController; @@ -76,7 +80,8 @@ public class MediaOutputDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputDialog = new MediaOutputDialog(mContext, false, mMediaOutputController, mUiEventLogger); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java index e8cd6c88956d..f186f57fd0e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java @@ -35,6 +35,7 @@ import com.android.settingslib.media.MediaDevice; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.phone.ShadeController; @@ -46,6 +47,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; +import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -66,6 +68,8 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { mock(NotificationEntryManager.class); private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class); private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class); + private NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock( + NearbyMediaDevicesManager.class); private MediaOutputGroupDialog mMediaOutputGroupDialog; private MediaOutputController mMediaOutputController; @@ -75,7 +79,8 @@ public class MediaOutputGroupDialogTest extends SysuiTestCase { public void setUp() { mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE, false, mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter, - mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator); + mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator, + Optional.of(mNearbyMediaDevicesManager)); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false, mMediaOutputController); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt index 794bc09715af..2a130535c657 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/MediaTttCommandLineHelperTest.kt @@ -21,13 +21,8 @@ import android.content.Context import android.media.MediaRoute2Info import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.media.taptotransfer.sender.AlmostCloseToEndCast -import com.android.systemui.media.taptotransfer.sender.AlmostCloseToStartCast -import com.android.systemui.media.taptotransfer.sender.TransferFailed -import com.android.systemui.media.taptotransfer.sender.TransferToReceiverTriggered -import com.android.systemui.media.taptotransfer.sender.TransferToThisDeviceSucceeded -import com.android.systemui.media.taptotransfer.sender.TransferToThisDeviceTriggered -import com.android.systemui.media.taptotransfer.sender.TransferToReceiverSucceeded +import com.android.systemui.media.taptotransfer.receiver.ChipStateReceiver +import com.android.systemui.media.taptotransfer.sender.ChipStateSender import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.util.concurrency.FakeExecutor @@ -88,7 +83,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun sender_almostCloseToStartCast_serviceCallbackCalled() { commandRegistry.onShellCommand( - pw, getSenderCommand(AlmostCloseToStartCast::class.simpleName!!) + pw, getSenderCommand(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.name) ) val routeInfoCaptor = argumentCaptor<MediaRoute2Info>() @@ -103,7 +98,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun sender_almostCloseToEndCast_serviceCallbackCalled() { commandRegistry.onShellCommand( - pw, getSenderCommand(AlmostCloseToEndCast::class.simpleName!!) + pw, getSenderCommand(ChipStateSender.ALMOST_CLOSE_TO_END_CAST.name) ) val routeInfoCaptor = argumentCaptor<MediaRoute2Info>() @@ -118,7 +113,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun sender_transferToReceiverTriggered_chipDisplayWithCorrectState() { commandRegistry.onShellCommand( - pw, getSenderCommand(TransferToReceiverTriggered::class.simpleName!!) + pw, getSenderCommand(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.name) ) val routeInfoCaptor = argumentCaptor<MediaRoute2Info>() @@ -133,7 +128,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun sender_transferToThisDeviceTriggered_chipDisplayWithCorrectState() { commandRegistry.onShellCommand( - pw, getSenderCommand(TransferToThisDeviceTriggered::class.simpleName!!) + pw, getSenderCommand(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED.name) ) verify(statusBarManager).updateMediaTapToTransferSenderDisplay( @@ -146,7 +141,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun sender_transferToReceiverSucceeded_chipDisplayWithCorrectState() { commandRegistry.onShellCommand( - pw, getSenderCommand(TransferToReceiverSucceeded::class.simpleName!!) + pw, getSenderCommand(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED.name) ) val routeInfoCaptor = argumentCaptor<MediaRoute2Info>() @@ -161,7 +156,7 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun sender_transferToThisDeviceSucceeded_chipDisplayWithCorrectState() { commandRegistry.onShellCommand( - pw, getSenderCommand(TransferToThisDeviceSucceeded::class.simpleName!!) + pw, getSenderCommand(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED.name) ) val routeInfoCaptor = argumentCaptor<MediaRoute2Info>() @@ -174,8 +169,10 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { } @Test - fun sender_transferFailed_serviceCallbackCalled() { - commandRegistry.onShellCommand(pw, getSenderCommand(TransferFailed::class.simpleName!!)) + fun sender_transferToReceiverFailed_serviceCallbackCalled() { + commandRegistry.onShellCommand( + pw, getSenderCommand(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED.name) + ) verify(statusBarManager).updateMediaTapToTransferSenderDisplay( eq(StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_FAILED), @@ -185,8 +182,23 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { } @Test + fun sender_transferToThisDeviceFailed_serviceCallbackCalled() { + commandRegistry.onShellCommand( + pw, getSenderCommand(ChipStateSender.TRANSFER_TO_THIS_DEVICE_FAILED.name) + ) + + verify(statusBarManager).updateMediaTapToTransferSenderDisplay( + eq(StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_FAILED), + any(), + nullable(), + nullable()) + } + + @Test fun sender_farFromReceiver_serviceCallbackCalled() { - commandRegistry.onShellCommand(pw, getSenderCommand(FAR_FROM_RECEIVER_STATE)) + commandRegistry.onShellCommand( + pw, getSenderCommand(ChipStateSender.FAR_FROM_RECEIVER.name) + ) verify(statusBarManager).updateMediaTapToTransferSenderDisplay( eq(StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_FAR_FROM_RECEIVER), @@ -197,7 +209,9 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun receiver_closeToSender_serviceCallbackCalled() { - commandRegistry.onShellCommand(pw, getReceiverCommand(CLOSE_TO_SENDER_STATE)) + commandRegistry.onShellCommand( + pw, getReceiverCommand(ChipStateReceiver.CLOSE_TO_SENDER.name) + ) verify(statusBarManager).updateMediaTapToTransferReceiverDisplay( eq(StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER), @@ -209,7 +223,9 @@ class MediaTttCommandLineHelperTest : SysuiTestCase() { @Test fun receiver_farFromSender_serviceCallbackCalled() { - commandRegistry.onShellCommand(pw, getReceiverCommand(FAR_FROM_SENDER_STATE)) + commandRegistry.onShellCommand( + pw, getReceiverCommand(ChipStateReceiver.FAR_FROM_SENDER.name) + ) verify(statusBarManager).updateMediaTapToTransferReceiverDisplay( eq(StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER), diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt index adb59eca1e08..ccce5778c150 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt @@ -17,7 +17,10 @@ package com.android.systemui.media.taptotransfer.common import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.graphics.drawable.Drawable +import android.os.PowerManager import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -40,6 +43,7 @@ import org.junit.Before import org.junit.Test import org.mockito.ArgumentCaptor import org.mockito.Mock +import org.mockito.Mockito.eq import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -48,12 +52,16 @@ import org.mockito.MockitoAnnotations @SmallTest class MediaTttChipControllerCommonTest : SysuiTestCase() { - private lateinit var controllerCommon: MediaTttChipControllerCommon<MediaTttChipState> + private lateinit var controllerCommon: MediaTttChipControllerCommon<ChipInfo> private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor - private lateinit var appIconDrawable: Drawable + private lateinit var appIconFromPackageName: Drawable + @Mock + private lateinit var packageManager: PackageManager + @Mock + private lateinit var applicationInfo: ApplicationInfo @Mock private lateinit var logger: MediaTttLogger @Mock @@ -62,25 +70,36 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { private lateinit var viewUtil: ViewUtil @Mock private lateinit var tapGestureDetector: TapGestureDetector + @Mock + private lateinit var powerManager: PowerManager @Before fun setUp() { MockitoAnnotations.initMocks(this) - appIconDrawable = context.getDrawable(R.drawable.ic_cake)!! + + appIconFromPackageName = context.getDrawable(R.drawable.ic_cake)!! + whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(appIconFromPackageName) + whenever(applicationInfo.loadLabel(packageManager)).thenReturn(APP_NAME) + whenever(packageManager.getApplicationInfo( + eq(PACKAGE_NAME), any<PackageManager.ApplicationInfoFlags>() + )).thenReturn(applicationInfo) + context.setMockPackageManager(packageManager) + fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) controllerCommon = TestControllerCommon( - context, logger, windowManager, viewUtil, fakeExecutor, tapGestureDetector + context, logger, windowManager, viewUtil, fakeExecutor, tapGestureDetector, powerManager ) } @Test - fun displayChip_chipAddedAndGestureDetectionStarted() { + fun displayChip_chipAddedAndGestureDetectionStartedAndScreenOn() { controllerCommon.displayChip(getState()) verify(windowManager).addView(any(), any()) verify(tapGestureDetector).addOnGestureDetectedCallback(any(), any()) + verify(powerManager).wakeUp(any(), any(), any()) } @Test @@ -100,7 +119,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { controllerCommon.displayChip(state) reset(windowManager) - fakeClock.advanceTime(state.getTimeoutMs() - 1) + fakeClock.advanceTime(TIMEOUT_MS - 1) verify(windowManager, never()).removeView(any()) } @@ -111,7 +130,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { controllerCommon.displayChip(state) reset(windowManager) - fakeClock.advanceTime(state.getTimeoutMs() + 1) + fakeClock.advanceTime(TIMEOUT_MS + 1) verify(windowManager).removeView(any()) } @@ -128,7 +147,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { controllerCommon.displayChip(getState()) // Wait until the timeout for the first display would've happened - fakeClock.advanceTime(state.getTimeoutMs() - waitTime + 1) + fakeClock.advanceTime(TIMEOUT_MS - waitTime + 1) // Verify we didn't hide the chip verify(windowManager, never()).removeView(any()) @@ -145,7 +164,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { controllerCommon.displayChip(getState()) // Ensure we still hide the chip eventually - fakeClock.advanceTime(state.getTimeoutMs() + 1) + fakeClock.advanceTime(TIMEOUT_MS + 1) verify(windowManager).removeView(any()) } @@ -172,16 +191,45 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { } @Test - fun setIcon_viewHasIconAndContentDescription() { + fun setIcon_nullAppIconDrawable_iconIsFromPackageName() { + controllerCommon.displayChip(getState()) + val chipView = getChipView() + + controllerCommon.setIcon(chipView, PACKAGE_NAME, appIconDrawableOverride = null, null) + + assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconFromPackageName) + } + + @Test + fun displayChip_hasAppIconDrawable_iconIsDrawable() { controllerCommon.displayChip(getState()) val chipView = getChipView() - val state = TestChipState(PACKAGE_NAME) - controllerCommon.setIcon(state, chipView) + val drawable = context.getDrawable(R.drawable.ic_alarm)!! + controllerCommon.setIcon(chipView, PACKAGE_NAME, drawable, null) - assertThat(chipView.getAppIconView().drawable).isEqualTo(appIconDrawable) - assertThat(chipView.getAppIconView().contentDescription) - .isEqualTo(state.getAppName(context)) + assertThat(chipView.getAppIconView().drawable).isEqualTo(drawable) + } + + @Test + fun displayChip_nullAppName_iconContentDescriptionIsFromPackageName() { + controllerCommon.displayChip(getState()) + val chipView = getChipView() + + controllerCommon.setIcon(chipView, PACKAGE_NAME, null, appNameOverride = null) + + assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + } + + @Test + fun displayChip_hasAppName_iconContentDescriptionIsAppNameOverride() { + controllerCommon.displayChip(getState()) + val chipView = getChipView() + + val appName = "Override App Name" + controllerCommon.setIcon(chipView, PACKAGE_NAME, null, appName) + + assertThat(chipView.getAppIconView().contentDescription).isEqualTo(appName) } @Test @@ -218,7 +266,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { verify(windowManager, never()).removeView(any()) } - private fun getState() = MediaTttChipState(PACKAGE_NAME) + private fun getState() = ChipInfo() private fun getChipView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) @@ -235,22 +283,27 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { viewUtil: ViewUtil, @Main mainExecutor: DelayableExecutor, tapGestureDetector: TapGestureDetector, - ) : MediaTttChipControllerCommon<MediaTttChipState>( + powerManager: PowerManager + ) : MediaTttChipControllerCommon<ChipInfo>( context, logger, windowManager, viewUtil, mainExecutor, tapGestureDetector, + powerManager, R.layout.media_ttt_chip ) { - override fun updateChipView(chipState: MediaTttChipState, currentChipView: ViewGroup) { + override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) { + } } - inner class TestChipState(appPackageName: String?) : MediaTttChipState(appPackageName) { - override fun getAppIcon(context: Context) = appIconDrawable + inner class ChipInfo : ChipInfoCommon { + override fun getTimeoutMs() = TIMEOUT_MS } } private const val PACKAGE_NAME = "com.android.systemui" +private const val APP_NAME = "Fake App Name" +private const val TIMEOUT_MS = 10000L diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index 56f45896436c..355d3fe4b8d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -22,6 +22,7 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.media.MediaRoute2Info import android.os.Handler +import android.os.PowerManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -29,6 +30,7 @@ import android.view.ViewGroup import android.view.WindowManager import android.widget.ImageView import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.media.taptotransfer.common.MediaTttLogger @@ -63,6 +65,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Mock private lateinit var logger: MediaTttLogger @Mock + private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var viewUtil: ViewUtil @@ -70,6 +74,8 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { private lateinit var commandQueue: CommandQueue private lateinit var commandQueueCallback: CommandQueue.Callbacks private lateinit var fakeAppIconDrawable: Drawable + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var receiverUiEventLogger: MediaTttReceiverUiEventLogger @Before fun setUp() { @@ -83,6 +89,9 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { )).thenReturn(applicationInfo) context.setMockPackageManager(packageManager) + uiEventLoggerFake = UiEventLoggerFake() + receiverUiEventLogger = MediaTttReceiverUiEventLogger(uiEventLoggerFake) + controllerReceiver = MediaTttChipControllerReceiver( commandQueue, context, @@ -91,7 +100,9 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { viewUtil, FakeExecutor(FakeSystemClock()), TapGestureDetector(context), + powerManager, Handler.getMain(), + receiverUiEventLogger, ) val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) @@ -110,6 +121,9 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { ) assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(appName) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_CLOSE_TO_SENDER.id + ) } @Test @@ -122,6 +136,9 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { ) verify(windowManager, never()).addView(any(), any()) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_FAR_FROM_SENDER.id + ) } @Test @@ -157,44 +174,6 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { verify(logger).logStateChange(any(), any()) } - @Test - fun displayChip_nullAppIconDrawable_iconIsFromPackageName() { - val state = ChipStateReceiver(PACKAGE_NAME, appIconDrawable = null, "appName") - - controllerReceiver.displayChip(state) - - assertThat(getChipView().getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) - } - - @Test - fun displayChip_hasAppIconDrawable_iconIsDrawable() { - val drawable = context.getDrawable(R.drawable.ic_alarm)!! - val state = ChipStateReceiver(PACKAGE_NAME, drawable, "appName") - - controllerReceiver.displayChip(state) - - assertThat(getChipView().getAppIconView().drawable).isEqualTo(drawable) - } - - @Test - fun displayChip_nullAppName_iconContentDescriptionIsFromPackageName() { - val state = ChipStateReceiver(PACKAGE_NAME, appIconDrawable = null, appName = null) - - controllerReceiver.displayChip(state) - - assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(APP_NAME) - } - - @Test - fun displayChip_hasAppName_iconContentDescriptionIsAppNameOverride() { - val appName = "Override App Name" - val state = ChipStateReceiver(PACKAGE_NAME, appIconDrawable = null, appName) - - controllerReceiver.displayChip(state) - - assertThat(getChipView().getAppIconView().contentDescription).isEqualTo(appName) - } - private fun getChipView(): ViewGroup { val viewCaptor = ArgumentCaptor.forClass(View::class.java) verify(windowManager).addView(viewCaptor.capture(), any()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLoggerTest.kt new file mode 100644 index 000000000000..ee10ddc521f1 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLoggerTest.kt @@ -0,0 +1,30 @@ +package com.android.systemui.media.taptotransfer.receiver + +import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +@SmallTest +class MediaTttReceiverUiEventLoggerTest : SysuiTestCase() { + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var logger: MediaTttReceiverUiEventLogger + + @Before + fun setUp() { + uiEventLoggerFake = UiEventLoggerFake() + logger = MediaTttReceiverUiEventLogger(uiEventLoggerFake) + } + + @Test + fun logReceiverStateChange_eventAssociatedWithStateIsLogged() { + val state = ChipStateReceiver.CLOSE_TO_SENDER + + logger.logReceiverStateChange(state) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(state.uiEvent.id) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index fd1d76a5d02d..ef5315428a60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -21,6 +21,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.media.MediaRoute2Info +import android.os.PowerManager import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -29,6 +30,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -65,6 +67,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { @Mock private lateinit var logger: MediaTttLogger @Mock + private lateinit var powerManager: PowerManager + @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var viewUtil: ViewUtil @@ -74,6 +78,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock private lateinit var fakeExecutor: FakeExecutor + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var senderUiEventLogger: MediaTttSenderUiEventLogger @Before fun setUp() { @@ -89,6 +95,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) + uiEventLoggerFake = UiEventLoggerFake() + senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) controllerSender = MediaTttChipControllerSender( commandQueue, @@ -97,7 +105,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { windowManager, viewUtil, fakeExecutor, - TapGestureDetector(context) + TapGestureDetector(context), + powerManager, + senderUiEventLogger ) val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) @@ -113,8 +123,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToStartCast().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + almostCloseToStartCast().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_START_CAST.id + ) } @Test @@ -125,8 +139,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(almostCloseToEndCast().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + almostCloseToEndCast().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_ALMOST_CLOSE_TO_END_CAST.id + ) } @Test @@ -137,8 +155,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(transferToReceiverTriggered().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_TRIGGERED.id + ) } @Test @@ -149,8 +171,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(transferToThisDeviceTriggered().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_TRIGGERED.id + ) } @Test @@ -161,8 +187,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(transferToReceiverSucceeded().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToReceiverSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_SUCCEEDED.id + ) } @Test @@ -173,8 +203,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(transferToThisDeviceSucceeded().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToThisDeviceSucceeded().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_SUCCEEDED.id + ) } @Test @@ -185,8 +219,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(transferFailed().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToReceiverFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED.id + ) } @Test @@ -197,8 +235,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { null ) - assertThat(getChipView().getChipText()) - .isEqualTo(transferFailed().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToThisDeviceFailed().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_THIS_DEVICE_FAILED.id + ) } @Test @@ -210,6 +252,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { ) verify(windowManager, never()).addView(any(), any()) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_FAR_FROM_RECEIVER.id + ) } @Test @@ -250,7 +295,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(chipView.getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) @@ -264,7 +311,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(chipView.getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) @@ -278,7 +327,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(chipView.getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) @@ -292,7 +343,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(chipView.getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.VISIBLE) assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) @@ -306,7 +359,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(chipView.getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) } @@ -355,8 +410,12 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { getChipView().getUndoButton().performClick() - assertThat(getChipView().getChipText()) - .isEqualTo(transferToThisDeviceTriggered().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToThisDeviceTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED.id + ) } @Test @@ -367,7 +426,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(chipView.getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.GONE) } @@ -416,19 +477,41 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { getChipView().getUndoButton().performClick() - assertThat(getChipView().getChipText()) - .isEqualTo(transferToReceiverTriggered().getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + transferToReceiverTriggered().state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED.id + ) + } + + @Test + fun transferToReceiverFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { + val state = transferToReceiverFailed() + controllerSender.displayChip(state) + + val chipView = getChipView() + assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) + assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) + assertThat(getChipView().getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) + assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) + assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) + assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) } @Test - fun transferFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { - val state = transferFailed() + fun transferToThisDeviceFailed_appIcon_noDeviceName_noLoadingIcon_noUndo_failureIcon() { + val state = transferToThisDeviceFailed() controllerSender.displayChip(state) val chipView = getChipView() assertThat(chipView.getAppIconView().drawable).isEqualTo(fakeAppIconDrawable) assertThat(chipView.getAppIconView().contentDescription).isEqualTo(APP_NAME) - assertThat(chipView.getChipText()).isEqualTo(state.getChipTextString(context)) + assertThat(getChipView().getChipText()).isEqualTo( + state.state.getChipTextString(context, OTHER_DEVICE_NAME) + ) assertThat(chipView.getLoadingIconVisibility()).isEqualTo(View.GONE) assertThat(chipView.getUndoButton().visibility).isEqualTo(View.GONE) assertThat(chipView.getFailureIcon().visibility).isEqualTo(View.VISIBLE) @@ -475,7 +558,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { @Test fun changeFromTransferTriggeredToTransferFailed_failureIconAppears() { controllerSender.displayChip(transferToReceiverTriggered()) - controllerSender.displayChip(transferFailed()) + controllerSender.displayChip(transferToReceiverFailed()) assertThat(getChipView().getFailureIcon().visibility).isEqualTo(View.VISIBLE) } @@ -498,7 +581,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { fakeClock.advanceTime(1000L) controllerSender.removeChip("fakeRemovalReason") - fakeClock.advanceTime(state.getTimeoutMs() + 1) + fakeClock.advanceTime(state.state.timeout + 1) verify(windowManager).removeView(any()) } @@ -521,7 +604,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { fakeClock.advanceTime(1000L) controllerSender.removeChip("fakeRemovalReason") - fakeClock.advanceTime(state.getTimeoutMs() + 1) + fakeClock.advanceTime(state.state.timeout + 1) verify(windowManager).removeView(any()) } @@ -546,34 +629,35 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { /** Helper method providing default parameters to not clutter up the tests. */ private fun almostCloseToStartCast() = - AlmostCloseToStartCast(PACKAGE_NAME, OTHER_DEVICE_NAME) + ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST, routeInfo) /** Helper method providing default parameters to not clutter up the tests. */ private fun almostCloseToEndCast() = - AlmostCloseToEndCast(PACKAGE_NAME, OTHER_DEVICE_NAME) + ChipSenderInfo(ChipStateSender.ALMOST_CLOSE_TO_END_CAST, routeInfo) /** Helper method providing default parameters to not clutter up the tests. */ private fun transferToReceiverTriggered() = - TransferToReceiverTriggered(PACKAGE_NAME, OTHER_DEVICE_NAME) + ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED, routeInfo) /** Helper method providing default parameters to not clutter up the tests. */ private fun transferToThisDeviceTriggered() = - TransferToThisDeviceTriggered(PACKAGE_NAME) + ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_TRIGGERED, routeInfo) /** Helper method providing default parameters to not clutter up the tests. */ private fun transferToReceiverSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - TransferToReceiverSucceeded( - PACKAGE_NAME, OTHER_DEVICE_NAME, undoCallback - ) + ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_SUCCEEDED, routeInfo, undoCallback) /** Helper method providing default parameters to not clutter up the tests. */ private fun transferToThisDeviceSucceeded(undoCallback: IUndoMediaTransferCallback? = null) = - TransferToThisDeviceSucceeded( - PACKAGE_NAME, OTHER_DEVICE_NAME, undoCallback - ) + ChipSenderInfo(ChipStateSender.TRANSFER_TO_THIS_DEVICE_SUCCEEDED, routeInfo, undoCallback) + + /** Helper method providing default parameters to not clutter up the tests. */ + private fun transferToReceiverFailed() = + ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) /** Helper method providing default parameters to not clutter up the tests. */ - private fun transferFailed() = TransferFailed(PACKAGE_NAME) + private fun transferToThisDeviceFailed() = + ChipSenderInfo(ChipStateSender.TRANSFER_TO_RECEIVER_FAILED, routeInfo) } private const val APP_NAME = "Fake app name" diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt new file mode 100644 index 000000000000..263637a6b6e5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderUiEventLoggerTest.kt @@ -0,0 +1,46 @@ +package com.android.systemui.media.taptotransfer.sender + +import androidx.test.filters.SmallTest +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +@SmallTest +class MediaTttSenderUiEventLoggerTest : SysuiTestCase() { + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var logger: MediaTttSenderUiEventLogger + + @Before + fun setUp () { + uiEventLoggerFake = UiEventLoggerFake() + logger = MediaTttSenderUiEventLogger(uiEventLoggerFake) + } + + @Test + fun logSenderStateChange_eventAssociatedWithStateIsLogged() { + val state = ChipStateSender.ALMOST_CLOSE_TO_END_CAST + logger.logSenderStateChange(state) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(state.uiEvent.id) + } + + @Test + fun logUndoClicked_undoEventLogged() { + val undoEvent = MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED + + logger.logUndoClicked(undoEvent) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(undoEvent.id) + } + + @Test + fun logUndoClicked_notUndoEvent_eventNotLogged() { + logger.logUndoClicked(MediaTttSenderUiEvents.MEDIA_TTT_SENDER_TRANSFER_TO_RECEIVER_FAILED) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index 25a5b900f15a..067caa98102b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -87,6 +87,8 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { row = helper.createRow() context.getOrCreateTestableResources() .addOverride(R.bool.config_use_split_notification_shade, false) + context.getOrCreateTestableResources() + .addOverride(R.dimen.lockscreen_shade_depth_controller_transition_distance, 100) transitionController = LockscreenShadeTransitionController( statusBarStateController = statusbarStateController, logger = logger, @@ -247,6 +249,17 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() { } @Test + fun testDragDownAmount_depthDistanceIsZero_doesNotSetProgress() { + context.getOrCreateTestableResources() + .addOverride(R.dimen.lockscreen_shade_depth_controller_transition_distance, 0) + configurationController.notifyConfigurationChanged() + + transitionController.dragDownAmount = 10f + + verify(depthController, never()).transitionToFullShadeProgress + } + + @Test fun setDragDownAmount_setsValueOnMediaHierarchyManager() { transitionController.dragDownAmount = 10f diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 1561b5a5d22e..bf16e0ab39c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -69,11 +69,11 @@ import com.android.systemui.statusbar.notification.collection.render.SectionHead import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; +import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.HeadsUpManagerPhone; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.ShadeController; -import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.ZenModeController; @@ -278,18 +278,17 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mStateListenerArgumentCaptor.capture(), anyInt()); StatusBarStateController.StateListener stateListener = mStateListenerArgumentCaptor.getValue(); - when(mNotificationStackScrollLayout.isUsingSplitNotificationShade()).thenReturn(true); stateListener.onStateChanged(SHADE); mController.getView().removeAllViews(); - mController.setQsExpanded(false); + mController.setQsFullScreen(false); reset(mNotificationStackScrollLayout); mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( /* visible= */ true, /* notifVisibleInShade= */ false); - mController.setQsExpanded(true); + mController.setQsFullScreen(true); reset(mNotificationStackScrollLayout); mController.updateShowEmptyShadeView(); verify(mNotificationStackScrollLayout).updateEmptyShadeView( @@ -411,11 +410,11 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { boolean toShow) { if (toShow) { statusBarStateListener.onStateChanged(SHADE); - mController.setQsExpanded(false); + mController.setQsFullScreen(false); mController.getView().removeAllViews(); } else { statusBarStateListener.onStateChanged(KEYGUARD); - mController.setQsExpanded(true); + mController.setQsFullScreen(true); mController.getView().addContainerView(mock(ExpandableNotificationRow.class)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java index 7726938db3b0..185942e6fbc8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -32,6 +32,7 @@ import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.ShellCommandHandler; @@ -66,6 +67,7 @@ public class WMShellTest extends SysuiTestCase { @Mock CommandQueue mCommandQueue; @Mock ConfigurationController mConfigurationController; + @Mock KeyguardStateController mKeyguardStateController; @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock NavigationModeController mNavigationModeController; @Mock ScreenLifecycle mScreenLifecycle; @@ -90,9 +92,9 @@ public class WMShellTest extends SysuiTestCase { Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mHideDisplayCutout), Optional.of(mShellCommandHandler), Optional.of(mCompatUI), Optional.of(mDragAndDrop), - mCommandQueue, mConfigurationController, mKeyguardUpdateMonitor, - mNavigationModeController, mScreenLifecycle, mSysUiState, mProtoTracer, - mWakefulnessLifecycle, mUserInfoController, mSysUiMainExecutor); + mCommandQueue, mConfigurationController, mKeyguardStateController, + mKeyguardUpdateMonitor, mNavigationModeController, mScreenLifecycle, mSysUiState, + mProtoTracer, mWakefulnessLifecycle, mUserInfoController, mSysUiMainExecutor); } @Test @@ -132,6 +134,6 @@ public class WMShellTest extends SysuiTestCase { public void initCompatUI_registersCallbacks() { mWMShell.initCompatUi(mCompatUI); - verify(mKeyguardUpdateMonitor).registerCallback(any(KeyguardUpdateMonitorCallback.class)); + verify(mKeyguardStateController).addCallback(any(KeyguardStateController.Callback.class)); } } diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index 86777a2f62fd..e20b15a3e807 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -1327,14 +1327,13 @@ public class TouchExplorer extends BaseEventStreamTransformation if (mState.isServiceDetectingGestures() && mState.isTouchInteracting()) { // Cancel without deleting events. mHandler.removeCallbacks(mSendHoverEnterAndMoveDelayed); - mSendHoverEnterAndMoveDelayed.run(); - mSendHoverEnterAndMoveDelayed.clear(); - final MotionEvent prototype = mState.getLastReceivedEvent(); - final MotionEvent rawEvent = mState.getLastReceivedRawEvent(); final int pointerId = mReceivedPointerTracker.getPrimaryPointerId(); final int pointerIdBits = (1 << pointerId); final int policyFlags = mState.getLastReceivedPolicyFlags(); - mSendHoverExitDelayed.post(prototype, rawEvent, pointerIdBits, policyFlags); + mSendHoverEnterAndMoveDelayed.setPointerIdBits(pointerIdBits); + mSendHoverEnterAndMoveDelayed.setPolicyFlags(policyFlags); + mSendHoverEnterAndMoveDelayed.run(); + mSendHoverEnterAndMoveDelayed.clear(); } } diff --git a/services/backup/backuplib/java/com/android/server/backup/TransportManager.java b/services/backup/backuplib/java/com/android/server/backup/TransportManager.java index 21a22f44f3dd..930f49e4d117 100644 --- a/services/backup/backuplib/java/com/android/server/backup/TransportManager.java +++ b/services/backup/backuplib/java/com/android/server/backup/TransportManager.java @@ -16,6 +16,9 @@ package com.android.server.backup; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + import android.annotation.Nullable; import android.annotation.UserIdInt; import android.annotation.WorkerThread; @@ -28,7 +31,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.os.Binder; import android.os.Bundle; import android.os.RemoteException; import android.util.ArrayMap; @@ -42,8 +44,8 @@ import com.android.internal.util.Preconditions; import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.OnTransportRegisteredListener; import com.android.server.backup.transport.TransportConnection; -import com.android.server.backup.transport.TransportConnectionManager; import com.android.server.backup.transport.TransportConnectionListener; +import com.android.server.backup.transport.TransportConnectionManager; import com.android.server.backup.transport.TransportNotAvailableException; import com.android.server.backup.transport.TransportNotRegisteredException; import com.android.server.backup.transport.TransportStats; @@ -58,6 +60,7 @@ import java.util.function.Predicate; /** Handles in-memory bookkeeping of all BackupTransport objects. */ public class TransportManager { private static final String TAG = "BackupTransportManager"; + private static final boolean MORE_DEBUG = false; @VisibleForTesting public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST"; @@ -130,14 +133,61 @@ public class TransportManager { } } + void onPackageEnabled(String packageName) { + onPackageAdded(packageName); + } + + void onPackageDisabled(String packageName) { + onPackageRemoved(packageName); + } + @WorkerThread void onPackageChanged(String packageName, String... components) { + // Determine if the overall package has changed and not just its + // components - see {@link EXTRA_CHANGED_COMPONENT_NAME_LIST}. When we + // know a package was enabled/disabled we'll handle that directly and + // not continue with onPackageChanged. + if (components.length == 1 && components[0].equals(packageName)) { + int enabled; + try { + enabled = mPackageManager.getApplicationEnabledSetting(packageName); + } catch (IllegalArgumentException ex) { + // packageName doesn't exist: likely due to a race with it being uninstalled. + if (MORE_DEBUG) { + Slog.d(TAG, "Package " + packageName + " was changed, but no longer exists."); + } + return; + } + switch (enabled) { + case COMPONENT_ENABLED_STATE_ENABLED: { + if (MORE_DEBUG) { + Slog.d(TAG, "Package " + packageName + " was enabled."); + } + onPackageEnabled(packageName); + return; + } + case COMPONENT_ENABLED_STATE_DISABLED: { + if (MORE_DEBUG) { + Slog.d(TAG, "Package " + packageName + " was disabled."); + } + onPackageDisabled(packageName); + return; + } + default: { + Slog.w(TAG, "Package " + packageName + " enabled setting: " + enabled); + return; + } + } + } // Unfortunately this can't be atomic because we risk a deadlock if // registerTransportsFromPackage() is put inside the synchronized block Set<ComponentName> transportComponents = new ArraySet<>(components.length); for (String componentName : components) { transportComponents.add(new ComponentName(packageName, componentName)); } + if (transportComponents.isEmpty()) { + return; + } synchronized (mTransportLock) { mRegisteredTransportsDescriptionMap.keySet().removeIf(transportComponents::contains); } diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index b1c91ba4a79d..81a8680cdbf0 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -368,8 +368,7 @@ public final class PendingIntentRecord extends IIntentSender.Stub { // Apply any launch flags from the ActivityOptions. This is to ensure that the caller // can specify a consistent launch mode even if the PendingIntent is immutable - final ActivityOptions opts = options != null ? ActivityOptions.fromBundle(options) - : null; + final ActivityOptions opts = ActivityOptions.fromBundle(options); if (opts != null) { finalIntent.addFlags(opts.getPendingIntentLaunchFlags()); } diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java index 11c451e01d4c..28c7cad3b184 100644 --- a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java +++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java @@ -54,8 +54,8 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { return bits; } - private boolean isPipeOpened() { - return mPipe != null; + private synchronized FileDescriptor getPipeFD() { + return mPipe; } private synchronized boolean openPipe() { @@ -107,14 +107,16 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { return msg; } - private void sendMessage(final byte[] msg) throws ErrnoException, InterruptedIOException { + private static void sendMessage( + final FileDescriptor fd, + final byte[] msg) throws ErrnoException, InterruptedIOException { final byte[] lengthBits = new byte[4]; final ByteBuffer bb = ByteBuffer.wrap(lengthBits); bb.order(ByteOrder.LITTLE_ENDIAN); bb.putInt(msg.length); - Os.write(mPipe, lengthBits, 0, lengthBits.length); - Os.write(mPipe, msg, 0, msg.length); + Os.write(fd, lengthBits, 0, lengthBits.length); + Os.write(fd, msg, 0, msg.length); } EmulatorClipboardMonitor(final Consumer<ClipData> setAndroidClipboard) { @@ -162,17 +164,22 @@ class EmulatorClipboardMonitor implements Consumer<ClipData> { } private void setHostClipboardImpl(final String value) { - if (LOG_CLIBOARD_ACCESS) { - Slog.i(TAG, "Setting the host clipboard to '" + value + "'"); - } + final FileDescriptor pipeFD = getPipeFD(); - try { - if (isPipeOpened()) { - sendMessage(value.getBytes()); - } - } catch (ErrnoException | InterruptedIOException e) { - Slog.e(TAG, "Failed to set host clipboard " + e.getMessage()); - } catch (IllegalArgumentException e) { + if (pipeFD != null) { + Thread t = new Thread(() -> { + if (LOG_CLIBOARD_ACCESS) { + Slog.i(TAG, "Setting the host clipboard to '" + value + "'"); + } + + try { + sendMessage(pipeFD, value.getBytes()); + } catch (ErrnoException | InterruptedIOException e) { + Slog.e(TAG, "Failed to set host clipboard " + e.getMessage()); + } catch (IllegalArgumentException e) { + } + }); + t.start(); } } } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 682f0dfe067c..c0df095c3289 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -1320,6 +1320,7 @@ public class Vpn { .setLegacyTypeName("VPN") .setBypassableVpn(mConfig.allowBypass && !mLockdown) .setVpnRequiresValidation(mConfig.requiresInternetValidation) + .setLocalRoutesExcludedForVpn(mConfig.excludeLocalRoutes) .build(); capsBuilder.setOwnerUid(mOwnerUID); @@ -3386,6 +3387,7 @@ public class Vpn { mConfig.startTime = SystemClock.elapsedRealtime(); mConfig.proxyInfo = profile.proxy; mConfig.requiresInternetValidation = profile.requiresInternetValidation; + mConfig.excludeLocalRoutes = profile.excludeLocalRoutes; switch (profile.type) { case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index c0950bf13a1c..d249d578cf67 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -654,14 +654,17 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { protected int handleTextViewOn(HdmiCecMessage message) { assertRunOnServiceThread(); - // Note that <Text View On> (and <Image View On>) command won't be handled here in - // most cases. A dedicated microcontroller should be in charge while Android system - // is in sleep mode, and the command need not be passed up to this service. - // The only situation where the command reaches this handler is that sleep mode is - // implemented in such a way that Android system is not really put to standby mode - // but only the display is set to blank. Then the command leads to the effect of + // Note that if the device is in sleep mode, the <Text View On> (and <Image View On>) + // command won't be handled here in most cases. A dedicated microcontroller should be in + // charge while the Android system is in sleep mode, and the command doesn't need to be + // passed up to this service. + // The only situations where the command reaches this handler are + // 1. if sleep mode is implemented in such a way that Android system is not really put to + // standby mode but only the display is set to blank. Then the command leads to // turning on the display by the invocation of PowerManager.wakeUp(). - if (mService.isPowerStandbyOrTransient() && getAutoWakeup()) { + // 2. if the device is in dream mode, not sleep mode. Then this command leads to + // waking up the device from dream mode by the invocation of PowerManager.wakeUp(). + if (getAutoWakeup()) { mService.wakeUp(); } return Constants.HANDLED; diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index 6cb3b3b6740d..e6fd40902386 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -32,6 +32,7 @@ import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; +import com.android.internal.inputmethod.InputMethodNavButtonFlags; import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethod; @@ -108,10 +109,10 @@ final class IInputMethodInvoker { @AnyThread void initializeInternal(IBinder token, IInputMethodPrivilegedOperations privOps, int configChanges, boolean stylusHwSupported, - boolean shouldShowImeSwitcherWhenImeIsShown) { + @InputMethodNavButtonFlags int navButtonFlags) { try { mTarget.initializeInternal(token, privOps, configChanges, stylusHwSupported, - shouldShowImeSwitcherWhenImeIsShown); + navButtonFlags); } catch (RemoteException e) { logRemoteException(e); } @@ -147,20 +148,19 @@ final class IInputMethodInvoker { @AnyThread void startInput(IBinder startInputToken, IInputContext inputContext, EditorInfo attribute, - boolean restarting, boolean shouldShowImeSwitcherWhenImeIsShown) { + boolean restarting, @InputMethodNavButtonFlags int navButtonFlags) { try { mTarget.startInput(startInputToken, inputContext, attribute, restarting, - shouldShowImeSwitcherWhenImeIsShown); + navButtonFlags); } catch (RemoteException e) { logRemoteException(e); } } @AnyThread - void onShouldShowImeSwitcherWhenImeIsShownChanged(boolean shouldShowImeSwitcherWhenImeIsShown) { + void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { try { - mTarget.onShouldShowImeSwitcherWhenImeIsShownChanged( - shouldShowImeSwitcherWhenImeIsShown); + mTarget.onNavButtonFlagsChanged(navButtonFlags); } catch (RemoteException e) { logRemoteException(e); } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 882e2e397d2c..584193654bfa 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -156,6 +156,7 @@ import com.android.internal.inputmethod.IInputMethodPrivilegedOperations; import com.android.internal.inputmethod.ImeTracing; import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.InputMethodNavButtonFlags; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; @@ -2412,12 +2413,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub true /* direct */); } - final boolean shouldShowImeSwitcherWhenImeIsShown = - shouldShowImeSwitcherWhenImeIsShownLocked(); + @InputMethodNavButtonFlags + final int navButtonFlags = getInputMethodNavButtonFlagsLocked(); final SessionState session = mCurClient.curSession; setEnabledSessionLocked(session); session.method.startInput(startInputToken, mCurInputContext, mCurAttribute, restarting, - shouldShowImeSwitcherWhenImeIsShown); + navButtonFlags); if (mShowRequested) { if (DEBUG) Slog.v(TAG, "Attach new input asks to show input"); showCurrentInputLocked(mCurFocusedWindow, getAppShowFlagsLocked(), null, @@ -2681,7 +2682,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + mCurTokenDisplayId); } inputMethod.initializeInternal(token, new InputMethodPrivilegedOperationsImpl(this, token), - configChanges, supportStylusHw, shouldShowImeSwitcherWhenImeIsShownLocked()); + configChanges, supportStylusHw, getInputMethodNavButtonFlagsLocked()); } @AnyThread @@ -2938,9 +2939,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - boolean shouldShowImeSwitcherWhenImeIsShownLocked() { - return shouldShowImeSwitcherLocked( + @InputMethodNavButtonFlags + private int getInputMethodNavButtonFlagsLocked() { + final boolean shouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherLocked( InputMethodService.IME_ACTIVE | InputMethodService.IME_VISIBLE); + return shouldShowImeSwitcherWhenImeIsShown + ? InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; } @GuardedBy("ImfLock.class") @@ -3203,7 +3207,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the same enabled IMEs list. mSwitchingController.resetCircularListLocked(mContext); - sendShouldShowImeSwitcherWhenImeIsShownLocked(); + sendOnNavButtonFlagsChangedLocked(); } @GuardedBy("ImfLock.class") @@ -4680,7 +4684,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub case MSG_HARD_KEYBOARD_SWITCH_CHANGED: mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1); synchronized (ImfLock.class) { - sendShouldShowImeSwitcherWhenImeIsShownLocked(); + sendOnNavButtonFlagsChangedLocked(); } return true; case MSG_SYSTEM_UNLOCK_USER: { @@ -4950,7 +4954,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the same enabled IMEs list. mSwitchingController.resetCircularListLocked(mContext); - sendShouldShowImeSwitcherWhenImeIsShownLocked(); + sendOnNavButtonFlagsChangedLocked(); // Notify InputMethodListListeners of the new installed InputMethods. final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList); @@ -4959,14 +4963,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - void sendShouldShowImeSwitcherWhenImeIsShownLocked() { + void sendOnNavButtonFlagsChangedLocked() { final IInputMethodInvoker curMethod = mBindingController.getCurMethod(); if (curMethod == null) { // No need to send the data if the IME is not yet bound. return; } - curMethod.onShouldShowImeSwitcherWhenImeIsShownChanged( - shouldShowImeSwitcherWhenImeIsShownLocked()); + curMethod.onNavButtonFlagsChanged(getInputMethodNavButtonFlagsLocked()); } @GuardedBy("ImfLock.class") diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index 98bde11ad517..c255fe14c03e 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -203,7 +203,7 @@ final class InputMethodMenuController { attrs.setTitle("Select input method"); w.setAttributes(attrs); mService.updateSystemUiLocked(); - mService.sendShouldShowImeSwitcherWhenImeIsShownLocked(); + mService.sendOnNavButtonFlagsChangedLocked(); mSwitchingDialog.show(); } } @@ -239,7 +239,7 @@ final class InputMethodMenuController { mSwitchingDialogTitleView = null; mService.updateSystemUiLocked(); - mService.sendShouldShowImeSwitcherWhenImeIsShownLocked(); + mService.sendOnNavButtonFlagsChangedLocked(); mDialogBuilder = null; mIms = null; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c4731aa7b522..d65dc13b72e3 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -3746,6 +3746,10 @@ public class NotificationManagerService extends SystemService { if (!hadChannel && hasChannel && !hasRequestedNotificationPermission && startingTaskId != ActivityTaskManager.INVALID_TASK_ID) { hasRequestedNotificationPermission = true; + if (mPermissionPolicyInternal == null) { + mPermissionPolicyInternal = + LocalServices.getService(PermissionPolicyInternal.class); + } mHandler.post(new ShowNotificationPermissionPromptRunnable(pkg, UserHandle.getUserId(uid), startingTaskId, mPermissionPolicyInternal)); @@ -3764,19 +3768,7 @@ public class NotificationManagerService extends SystemService { try { int uid = mPackageManager.getPackageUid(pkg, 0, UserHandle.getUserId(Binder.getCallingUid())); - List<ActivityManager.AppTask> tasks = mAtm.getAppTasks(pkg, uid); - for (int i = 0; i < tasks.size(); i++) { - ActivityManager.RecentTaskInfo task = tasks.get(i).getTaskInfo(); - if (mPermissionPolicyInternal == null) { - mPermissionPolicyInternal = - LocalServices.getService(PermissionPolicyInternal.class); - } - if (mPermissionPolicyInternal != null - && mPermissionPolicyInternal.canShowPermissionPromptForTask(task)) { - taskId = task.taskId; - break; - } - } + taskId = mAtm.getTaskToShowPermissionDialogOn(pkg, uid); } catch (RemoteException e) { // Do nothing } @@ -4068,13 +4060,12 @@ public class NotificationManagerService extends SystemService { @Override public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd( - String pkg, int userId) { + String pkg, int uid) { checkCallerIsSystem(); - if (!areNotificationsEnabledForPackage(pkg, - mPackageManagerInternal.getPackageUid(pkg, 0, userId))) { + if (!areNotificationsEnabledForPackage(pkg, uid)) { return ParceledListSlice.emptyList(); } - return mPreferencesHelper.getNotificationChannelsBypassingDnd(pkg, userId); + return mPreferencesHelper.getNotificationChannelsBypassingDnd(pkg, uid); } @Override diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 11858904a69a..1f7d65e6c604 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -1669,14 +1669,14 @@ public class PreferencesHelper implements RankingConfig { } /** - * Gets all notification channels associated with the given pkg and userId that can bypass dnd + * Gets all notification channels associated with the given pkg and uid that can bypass dnd */ public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg, - int userId) { + int uid) { List<NotificationChannel> channels = new ArrayList<>(); synchronized (mPackagePreferences) { final PackagePreferences r = mPackagePreferences.get( - packagePreferencesKey(pkg, userId)); + packagePreferencesKey(pkg, uid)); if (r != null) { for (NotificationChannel channel : r.channels.values()) { if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) { diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java index 2a6dd8410acb..b0d40efed690 100644 --- a/services/core/java/com/android/server/notification/ZenModeFiltering.java +++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java @@ -112,7 +112,7 @@ public class ZenModeFiltering { } if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) { if (consolidatedPolicy.allowRepeatCallers() - && REPEAT_CALLERS.isRepeat(context, extras)) { + && REPEAT_CALLERS.isRepeat(context, extras, null)) { ZenLog.traceMatchesCallFilter(true, "repeat caller"); return true; } @@ -229,7 +229,8 @@ public class ZenModeFiltering { } if (isCall(record)) { if (policy.allowRepeatCallers() - && REPEAT_CALLERS.isRepeat(mContext, extras(record))) { + && REPEAT_CALLERS.isRepeat( + mContext, extras(record), record.getPhoneNumbers())) { ZenLog.traceNotIntercepted(record, "repeatCaller"); return false; } @@ -350,6 +351,9 @@ public class ZenModeFiltering { private final ArrayMap<String, Long> mOtherCalls = new ArrayMap<>(); private int mThresholdMinutes; + // Record all people URIs in the extras bundle as well as the provided phoneNumbers set + // as callers. The phoneNumbers set is used to pass in any additional phone numbers + // associated with the people URIs as separately retrieved from contacts. private synchronized void recordCall(Context context, Bundle extras, ArraySet<String> phoneNumbers) { setThresholdMinutes(context); @@ -362,7 +366,13 @@ public class ZenModeFiltering { recordCallers(extraPeople, phoneNumbers, now); } - private synchronized boolean isRepeat(Context context, Bundle extras) { + // Determine whether any people in the provided extras bundle or phone number set is + // a repeat caller. The extras bundle contains the people associated with a specific + // notification, and will suffice for most callers; the phoneNumbers array may be used + // to additionally check any specific phone numbers previously retrieved from contacts + // associated with the people in the extras bundle. + private synchronized boolean isRepeat(Context context, Bundle extras, + ArraySet<String> phoneNumbers) { setThresholdMinutes(context); if (mThresholdMinutes <= 0 || extras == null) return false; final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras); @@ -370,7 +380,7 @@ public class ZenModeFiltering { final long now = System.currentTimeMillis(); cleanUp(mTelCalls, now); cleanUp(mOtherCalls, now); - return checkCallers(context, extraPeople); + return checkCallers(context, extraPeople, phoneNumbers); } private synchronized void cleanUp(ArrayMap<String, Long> calls, long now) { @@ -433,7 +443,31 @@ public class ZenModeFiltering { } } - private synchronized boolean checkCallers(Context context, String[] people) { + // helper function to check mTelCalls array for a number, and also check its decoded + // version + private synchronized boolean checkForNumber(String number, String defaultCountryCode) { + if (mTelCalls.containsKey(number)) { + // check directly via map first + return true; + } else { + // see if a number that matches via areSameNumber exists + String numberToCheck = Uri.decode(number); + if (numberToCheck != null) { + for (String prev : mTelCalls.keySet()) { + if (PhoneNumberUtils.areSamePhoneNumber( + numberToCheck, prev, defaultCountryCode)) { + return true; + } + } + } + } + return false; + } + + // Check whether anyone in the provided array of people URIs or phone number set matches a + // previously recorded phone call. + private synchronized boolean checkCallers(Context context, String[] people, + ArraySet<String> phoneNumbers) { // get the default country code for checking telephone numbers final String defaultCountryCode = context.getSystemService(TelephonyManager.class).getNetworkCountryIso(); @@ -443,20 +477,8 @@ public class ZenModeFiltering { final Uri uri = Uri.parse(person); if ("tel".equals(uri.getScheme())) { String number = uri.getSchemeSpecificPart(); - if (mTelCalls.containsKey(number)) { - // check directly via map first + if (checkForNumber(number, defaultCountryCode)) { return true; - } else { - // see if a number that matches via areSameNumber exists - String numberToCheck = Uri.decode(number); - if (numberToCheck != null) { - for (String prev : mTelCalls.keySet()) { - if (PhoneNumberUtils.areSamePhoneNumber( - numberToCheck, prev, defaultCountryCode)) { - return true; - } - } - } } } else { if (mOtherCalls.containsKey(person)) { @@ -464,6 +486,17 @@ public class ZenModeFiltering { } } } + + // also check any passed-in phone numbers + if (phoneNumbers != null) { + for (String num : phoneNumbers) { + if (checkForNumber(num, defaultCountryCode)) { + return true; + } + } + } + + // no matches return false; } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index c5c39f82a1fd..6e4d19f8d277 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -86,6 +86,7 @@ import static com.android.server.pm.PackageManagerServiceUtils.compareSignatures import static com.android.server.pm.PackageManagerServiceUtils.compressedFileExists; import static com.android.server.pm.PackageManagerServiceUtils.deriveAbiOverride; import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo; +import static com.android.server.pm.SharedUidMigration.BEST_EFFORT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -287,6 +288,12 @@ final class InstallPackageHelper { SharedUserSetting sharedUserSetting = mPm.mSettings.getSharedUserSettingLPr(pkgSetting); if (sharedUserSetting != null) { sharedUserSetting.addPackage(pkgSetting); + if (parsedPackage.isLeavingSharedUid() + && SharedUidMigration.applyStrategy(BEST_EFFORT) + && sharedUserSetting.isSingleUser()) { + // Attempt the transparent shared UID migration + mPm.mSettings.convertSharedUserSettingsLPw(sharedUserSetting); + } } if (reconciledPkg.mInstallArgs != null && reconciledPkg.mInstallArgs.mForceQueryableOverride) { @@ -2216,23 +2223,8 @@ final class InstallPackageHelper { } incrementalStorages.add(storage); } - int previousAppId = 0; - if (reconciledPkg.mScanResult.needsNewAppId()) { - // Only set previousAppId if the app is migrating out of shared UID - previousAppId = reconciledPkg.mScanResult.mPreviousAppId; - - if (pkg.shouldInheritKeyStoreKeys()) { - // Migrate keystore data - mAppDataHelper.migrateKeyStoreData( - previousAppId, reconciledPkg.mPkgSetting.getAppId()); - } - - if (reconciledPkg.mInstallResult.mRemovedInfo.mRemovedAppId == previousAppId) { - // If the previous app ID is removed, clear the keys - mAppDataHelper.clearKeystoreData(UserHandle.USER_ALL, previousAppId); - } - } - mAppDataHelper.prepareAppDataPostCommitLIF(pkg, previousAppId); + // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088) + mAppDataHelper.prepareAppDataPostCommitLIF(pkg, 0); if (reconciledPkg.mPrepareResult.mClearCodeCache) { mAppDataHelper.clearAppDataLIF(pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL @@ -3026,8 +3018,7 @@ final class InstallPackageHelper { installPackageFromSystemLIF(stubPkg.getPath(), mPm.mUserManager.getUserIds() /*allUserHandles*/, null /*origUserHandles*/, - true /*writeSettings*/, - Process.INVALID_UID /*previousAppId*/); + true /*writeSettings*/); } catch (PackageManagerException pme) { // Serious WTF; we have to be able to install the stub Slog.wtf(TAG, "Failed to restore system package:" + stubPkg.getPackageName(), @@ -3154,10 +3145,8 @@ final class InstallPackageHelper { try { synchronized (mPm.mInstallLock) { final int[] origUsers = outInfo == null ? null : outInfo.mOrigUsers; - final int previousAppId = disabledPs.getAppId() != deletedPs.getAppId() - ? deletedPs.getAppId() : Process.INVALID_UID; installPackageFromSystemLIF(disabledPs.getPathString(), allUserHandles, - origUsers, writeSettings, previousAppId); + origUsers, writeSettings); } } catch (PackageManagerException e) { Slog.w(TAG, "Failed to restore system package:" + deletedPs.getPackageName() + ": " @@ -3200,7 +3189,7 @@ final class InstallPackageHelper { @GuardedBy("mPm.mInstallLock") private void installPackageFromSystemLIF(@NonNull String codePathString, @NonNull int[] allUserHandles, @Nullable int[] origUserHandles, - boolean writeSettings, int previousAppId) + boolean writeSettings) throws PackageManagerException { final File codePath = new File(codePathString); @ParsingPackageUtils.ParseFlags int parseFlags = @@ -3223,13 +3212,12 @@ final class InstallPackageHelper { mAppDataHelper.prepareAppDataAfterInstallLIF(pkg); - setPackageInstalledForSystemPackage(pkg, allUserHandles, - origUserHandles, writeSettings, previousAppId); + setPackageInstalledForSystemPackage(pkg, allUserHandles, origUserHandles, writeSettings); } private void setPackageInstalledForSystemPackage(@NonNull AndroidPackage pkg, @NonNull int[] allUserHandles, @Nullable int[] origUserHandles, - boolean writeSettings, int previousAppId) { + boolean writeSettings) { // writer synchronized (mPm.mLock) { PackageSetting ps = mPm.mSettings.getPackageLPr(pkg.getPackageName()); @@ -3263,7 +3251,7 @@ final class InstallPackageHelper { // The method below will take care of removing obsolete permissions and granting // install permissions. - mPm.mPermissionManager.onPackageInstalled(pkg, previousAppId, + mPm.mPermissionManager.onPackageInstalled(pkg, Process.INVALID_UID, PermissionManagerServiceInternal.PackageInstalledParams.DEFAULT, UserHandle.USER_ALL); for (final int userId : allUserHandles) { @@ -3701,7 +3689,14 @@ final class InstallPackageHelper { } disabledPkgSetting = mPm.mSettings.getDisabledSystemPkgLPr( parsedPackage.getPackageName()); - if (parsedPackage.getSharedUserId() != null && !parsedPackage.isLeavingSharedUid()) { + + boolean ignoreSharedUserId = false; + if (installedPkgSetting == null) { + // We can directly ignore sharedUserSetting for new installs + ignoreSharedUserId = parsedPackage.isLeavingSharedUid(); + } + + if (!ignoreSharedUserId && parsedPackage.getSharedUserId() != null) { sharedUserSetting = mPm.mSettings.getSharedUserLPw( parsedPackage.getSharedUserId(), 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/); diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index 36bad3e604d5..76b9830cbde9 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -220,6 +220,8 @@ public class Installer extends SystemService { if (!checkBeforeRemote()) { return buildPlaceholderCreateAppDataResult(); } + // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088) + args.previousAppId = 0; try { return mInstalld.createAppData(args); } catch (Exception e) { @@ -234,6 +236,10 @@ public class Installer extends SystemService { Arrays.fill(results, buildPlaceholderCreateAppDataResult()); return results; } + // Hardcode previousAppId to 0 to disable any data migration (http://b/221088088) + for (final CreateAppDataArgs arg : args) { + arg.previousAppId = 0; + } try { return mInstalld.createAppDataBatched(args); } catch (Exception e) { diff --git a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java b/services/core/java/com/android/server/pm/IntentResolverInterceptor.java new file mode 100644 index 000000000000..0ee07b650cf5 --- /dev/null +++ b/services/core/java/com/android/server/pm/IntentResolverInterceptor.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 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.pm; + +import static com.android.server.wm.ActivityInterceptorCallback.INTENT_RESOLVER_ORDERED_ID; + +import android.Manifest; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.app.ActivityTaskManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.RemoteException; +import android.provider.DeviceConfig; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.server.LocalServices; +import com.android.server.wm.ActivityInterceptorCallback; +import com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo; +import com.android.server.wm.ActivityTaskManagerInternal; + +/** + * Service to register an {@code ActivityInterceptorCallback} that modifies any {@code Intent} + * that's being used to launch a user-space {@code ChooserActivity}, by adding + * EXTRA_PERMISSION_TOKEN, a Binder representing a single-use-only permission to invoke the + * #startActivityAsCaller() API (which normally isn't available in user-space); and setting the + * destination component to the delegated component when appropriate. + */ +public final class IntentResolverInterceptor { + private static final String TAG = "IntentResolverIntercept"; + + private final Context mContext; + private boolean mUseDelegateChooser; + + private final ActivityInterceptorCallback mActivityInterceptorCallback = + new ActivityInterceptorCallback() { + @Nullable + @Override + public ActivityInterceptResult intercept(ActivityInterceptorInfo info) { + if (mUseDelegateChooser && isChooserActivity(info)) { + return new ActivityInterceptResult( + modifyChooserIntent(info.intent), + info.checkedOptions); + } + return null; + } + }; + + public IntentResolverInterceptor(Context context) { + mContext = context; + } + + /** + * Start listening for intents and USE_DELEGATE_CHOOSER property changes. + */ + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) + public void registerListeners() { + LocalServices.getService(ActivityTaskManagerInternal.class) + .registerActivityStartInterceptor(INTENT_RESOLVER_ORDERED_ID, + mActivityInterceptorCallback); + + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + mContext.getMainExecutor(), properties -> updateUseDelegateChooser()); + updateUseDelegateChooser(); + } + + @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) + private void updateUseDelegateChooser() { + mUseDelegateChooser = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.USE_DELEGATE_CHOOSER, + false); + } + + private Intent modifyChooserIntent(Intent intent) { + intent.setComponent(getUnbundledChooserComponentName()); + addStartActivityPermissionTokenToIntent(intent, getUnbundledChooserComponentName()); + return intent; + } + + private static boolean isChooserActivity(ActivityInterceptorInfo info) { + ComponentName targetComponent = new ComponentName(info.aInfo.packageName, info.aInfo.name); + + return targetComponent.equals(getSystemChooserComponentName()) + || targetComponent.equals(getUnbundledChooserComponentName()); + } + + private static Intent addStartActivityPermissionTokenToIntent( + Intent intent, ComponentName grantee) { + try { + intent.putExtra( + ActivityTaskManager.EXTRA_PERMISSION_TOKEN, + ActivityTaskManager.getService().requestStartActivityPermissionToken(grantee)); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to add permission token to chooser intent"); + } + return intent; + } + + private static ComponentName getSystemChooserComponentName() { + return new ComponentName("android", "com.android.internal.app.ChooserActivity"); + } + + private static ComponentName getUnbundledChooserComponentName() { + return ComponentName.unflattenFromString( + Resources.getSystem().getString(R.string.config_chooserActivity)); + } +} diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 6b3ce773fb63..e9896617d8e8 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -1109,13 +1109,11 @@ public class LauncherAppsService extends SystemService { // Note the target activity doesn't have to be exported. // Flag for bubble - if (startActivityOptions != null) { - ActivityOptions options = ActivityOptions.fromBundle(startActivityOptions); - if (options.isApplyActivityFlagsForBubbles()) { - // Flag for bubble to make behaviour match documentLaunchMode=always. - intents[0].addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - } + ActivityOptions options = ActivityOptions.fromBundle(startActivityOptions); + if (options != null && options.isApplyActivityFlagsForBubbles()) { + // Flag for bubble to make behaviour match documentLaunchMode=always. + intents[0].addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + intents[0].addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); } intents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index f07418f6007f..a70cff98ed93 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -958,6 +958,7 @@ public class PackageManagerService extends IPackageManager.Stub private final ResolveIntentHelper mResolveIntentHelper; private final DexOptHelper mDexOptHelper; private final SuspendPackageHelper mSuspendPackageHelper; + private final IntentResolverInterceptor mIntentResolverInterceptor; /** * Invalidate the package info cache, which includes updating the cached computer. @@ -1712,6 +1713,8 @@ public class PackageManagerService extends IPackageManager.Stub mSharedLibraries.setDeletePackageHelper(mDeletePackageHelper); + mIntentResolverInterceptor = null; + registerObservers(false); invalidatePackageInfoCache(); } @@ -2240,6 +2243,8 @@ public class PackageManagerService extends IPackageManager.Stub mServiceStartWithDelay = SystemClock.uptimeMillis() + (60 * 1000L); + mIntentResolverInterceptor = new IntentResolverInterceptor(mContext); + Slog.i(TAG, "Fix for b/169414761 is applied"); } @@ -6397,6 +6402,11 @@ public class PackageManagerService extends IPackageManager.Stub // Prune unused static shared libraries which have been cached a period of time schedulePruneUnusedStaticSharedLibraries(false /* delay */); + + // TODO(b/222706900): Remove this intent interceptor before T launch + if (mIntentResolverInterceptor != null) { + mIntentResolverInterceptor.registerListeners(); + } } /** diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index f06ae1e06187..2bae00f91b82 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -477,6 +477,7 @@ public class PackageSetting extends SettingBase implements PackageStateInternal public void setSharedUserAppId(int sharedUserAppId) { mSharedUserAppId = sharedUserAppId; + onChanged(); } @Override diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 4d1519c361c9..88df843ec8c5 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -284,7 +284,11 @@ final class RemovePackageHelper { List<AndroidPackage> sharedUserPkgs = sus != null ? sus.getPackages() : Collections.emptyList(); mPermissionManager.onPackageUninstalled(packageName, deletedPs.getAppId(), - deletedPs.getPkg(), sharedUserPkgs, UserHandle.USER_ALL); + deletedPkg, sharedUserPkgs, UserHandle.USER_ALL); + // After permissions are handled, check if the shared user can be migrated + if (sus != null) { + mPm.mSettings.checkAndConvertSharedUserSettingsLPw(sus); + } } mPm.clearPackagePreferredActivitiesLPw( deletedPs.getPackageName(), changedUsers, UserHandle.USER_ALL); diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java index 33f0b54fb2de..4e8313bf1891 100644 --- a/services/core/java/com/android/server/pm/ScanPackageUtils.java +++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java @@ -165,25 +165,15 @@ final class ScanPackageUtils { } } - int previousAppId = Process.INVALID_UID; - if (pkgSetting != null && oldSharedUserSetting != sharedUserSetting) { - if (oldSharedUserSetting != null && sharedUserSetting == null) { - previousAppId = pkgSetting.getAppId(); - // Log that something is leaving shareduid and keep going - Slog.i(TAG, - "Package " + parsedPackage.getPackageName() + " shared user changed from " - + oldSharedUserSetting.name + " to " + "<nothing>."); - } else { - PackageManagerService.reportSettingsProblem(Log.WARN, - "Package " + parsedPackage.getPackageName() + " shared user changed from " - + (oldSharedUserSetting != null - ? oldSharedUserSetting.name : "<nothing>") - + " to " - + (sharedUserSetting != null ? sharedUserSetting.name : "<nothing>") - + "; replacing with new"); - pkgSetting = null; - } + PackageManagerService.reportSettingsProblem(Log.WARN, + "Package " + parsedPackage.getPackageName() + " shared user changed from " + + (oldSharedUserSetting != null + ? oldSharedUserSetting.name : "<nothing>") + + " to " + + (sharedUserSetting != null ? sharedUserSetting.name : "<nothing>") + + "; replacing with new"); + pkgSetting = null; } String[] usesSdkLibraries = null; @@ -474,8 +464,8 @@ final class ScanPackageUtils { return new ScanResult(request, true, pkgSetting, changedAbiCodePath, !createNewPackage /* existingSettingCopied */, - previousAppId, sdkLibraryInfo, staticSharedLibraryInfo, - dynamicSharedLibraryInfos); + Process.INVALID_UID /* previousAppId */ , sdkLibraryInfo, + staticSharedLibraryInfo, dynamicSharedLibraryInfos); } /** diff --git a/services/core/java/com/android/server/pm/ScanResult.java b/services/core/java/com/android/server/pm/ScanResult.java index f77be1f9b2d3..e2860ca327e7 100644 --- a/services/core/java/com/android/server/pm/ScanResult.java +++ b/services/core/java/com/android/server/pm/ScanResult.java @@ -70,7 +70,9 @@ final class ScanResult { mPkgSetting = pkgSetting; mChangedAbiCodePath = changedAbiCodePath; mExistingSettingCopied = existingSettingCopied; - mPreviousAppId = previousAppId; + // Hardcode mPreviousAppId to INVALID_UID (http://b/221088088) + // This will disable all migration code paths in PMS and PermMS + mPreviousAppId = Process.INVALID_UID; mSdkSharedLibraryInfo = sdkSharedLibraryInfo; mStaticSharedLibraryInfo = staticSharedLibraryInfo; mDynamicSharedLibraryInfos = dynamicSharedLibraryInfos; diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 86d7b517ea76..021c3db35756 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -29,6 +29,7 @@ import static android.os.Process.PACKAGE_INFO_GID; import static android.os.Process.SYSTEM_UID; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; +import static com.android.server.pm.SharedUidMigration.BEST_EFFORT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1424,6 +1425,35 @@ public final class Settings implements Watchable, Snappable { } } + /** + * Transparently convert a SharedUserSetting into PackageSettings without changing appId. + * The sharedUser passed to this method has to be {@link SharedUserSetting#isSingleUser()}. + */ + void convertSharedUserSettingsLPw(SharedUserSetting sharedUser) { + final PackageSetting ps = sharedUser.getPackageSettings().valueAt(0); + replaceAppIdLPw(sharedUser.getAppId(), ps); + + // Unlink the SharedUserSetting + ps.setSharedUserAppId(INVALID_UID); + if (!sharedUser.getDisabledPackageSettings().isEmpty()) { + final PackageSetting disabledPs = sharedUser.getDisabledPackageSettings().valueAt(0); + disabledPs.setSharedUserAppId(INVALID_UID); + } + mSharedUsers.remove(sharedUser.getName()); + } + + /** + * Check and convert eligible SharedUserSettings to PackageSettings. + */ + void checkAndConvertSharedUserSettingsLPw(SharedUserSetting sharedUser) { + if (!sharedUser.isSingleUser()) return; + final AndroidPackage pkg = sharedUser.getPackageSettings().valueAt(0).getPkg(); + if (pkg != null && pkg.isLeavingSharedUid() + && SharedUidMigration.applyStrategy(BEST_EFFORT)) { + convertSharedUserSettingsLPw(sharedUser); + } + } + PreferredIntentResolver editPreferredActivitiesLPw(int userId) { PreferredIntentResolver pir = mPreferredActivities.get(userId); if (pir == null) { diff --git a/services/core/java/com/android/server/pm/SharedUidMigration.java b/services/core/java/com/android/server/pm/SharedUidMigration.java new file mode 100644 index 000000000000..e44ef662efff --- /dev/null +++ b/services/core/java/com/android/server/pm/SharedUidMigration.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 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.pm; + +import android.annotation.IntDef; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.SystemProperties; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A utility class hosting code configuring shared UID migration behavior. + */ +public final class SharedUidMigration { + + /** + * The system property key used to configure the shared UID migration strategy. + */ + public static final String PROPERTY_KEY = "persist.debug.pm.shared_uid_migration_strategy"; + + /** + * Only leave shared UID for newly installed packages. + */ + public static final int NEW_INSTALL_ONLY = 1; + /** + * Opportunistically migrate any single-package shared UID group. + */ + public static final int BEST_EFFORT = 2; + /** + * Run appId transitions when the device reboots. + */ + public static final int TRANSITION_AT_BOOT = 3; + /** + * Run appId transitions as soon as the upgrade is installed. + */ + public static final int LIVE_TRANSITION = 4; + + @IntDef({ + NEW_INSTALL_ONLY, + BEST_EFFORT, + TRANSITION_AT_BOOT, + LIVE_TRANSITION, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Strategy {} + + @Strategy + private static final int DEFAULT = BEST_EFFORT; + + /** + * Whether shared UID migration is fully disabled. Disabled means the sharedUserMaxSdkVersion + * attribute will be directly ignored in the parsing phase. + */ + public static boolean isDisabled() { + return !PackageManager.ENABLE_SHARED_UID_MIGRATION; + } + + /** + * If the system is userdebug, returns the strategy to use by getting the system property + * {@link #PROPERTY_KEY}, or else returns the default strategy enabled in the build. + */ + public static int getCurrentStrategy() { + if (!Build.IS_USERDEBUG) { + return DEFAULT; + } + + final int s = SystemProperties.getInt(PROPERTY_KEY, DEFAULT); + // No transition strategies can be used (http://b/221088088) + if (s > BEST_EFFORT || s < NEW_INSTALL_ONLY) { + return DEFAULT; + } + return s; + } + + /** + * Should the strategy be used based on the current active strategy. + */ + public static boolean applyStrategy(@Strategy int strategy) { + return !isDisabled() && getCurrentStrategy() >= strategy; + } + + private SharedUidMigration() {} +} diff --git a/services/core/java/com/android/server/pm/SharedUserSetting.java b/services/core/java/com/android/server/pm/SharedUserSetting.java index 4d0bbc6fe437..23f0de8a5f71 100644 --- a/services/core/java/com/android/server/pm/SharedUserSetting.java +++ b/services/core/java/com/android/server/pm/SharedUserSetting.java @@ -221,6 +221,25 @@ public final class SharedUserSetting extends SettingBase implements SharedUserAp } /** + * A shared user is considered "single user" if there is exactly one single package + * currently using it. In the case when that package is also a system app, the APK on + * the system partition has to also leave shared UID. + */ + public boolean isSingleUser() { + if (mPackages.size() != 1) { + return false; + } + if (mDisabledPackages.size() > 1) { + return false; + } + if (mDisabledPackages.size() == 1) { + final AndroidPackage pkg = mDisabledPackages.valueAt(0).getPkg(); + return pkg != null && pkg.isLeavingSharedUid(); + } + return true; + } + + /** * Determine the targetSdkVersion for a sharedUser and update pkg.applicationInfo.seInfo * to ensure that all apps within the sharedUser share an SELinux domain. Use the lowest * targetSdkVersion of all apps within the shared user, which corresponds to the least diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 63469cb24f2f..124df47345ae 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -122,6 +122,12 @@ public class PackageInfoUtils { info.isStub = pkg.isStub(); info.coreApp = pkg.isCoreApp(); + if (!pkgSetting.hasSharedUser()) { + // It is possible that this shared UID app has left + info.sharedUserId = null; + info.sharedUserLabel = 0; + } + if ((flags & PackageManager.GET_ACTIVITIES) != 0) { final int N = pkg.getActivities().size(); if (N > 0) { diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index ed1ab01e1d12..3eaca9dddcc4 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -91,6 +91,7 @@ import com.android.internal.R; import com.android.internal.os.ClassLoaderFactory; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; +import com.android.server.pm.SharedUidMigration; import com.android.server.pm.permission.CompatibilityPermissionInfo; import com.android.server.pm.pkg.component.ComponentMutateUtils; import com.android.server.pm.pkg.component.ComponentParseUtils; @@ -1047,8 +1048,11 @@ public class ParsingPackageUtils { } } - int maxSdkVersion = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa); - boolean leaving = (maxSdkVersion != 0) && (maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT); + boolean leaving = false; + if (!SharedUidMigration.isDisabled()) { + int max = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa); + leaving = (max != 0) && (max < Build.VERSION.RESOURCES_SDK_INT); + } return input.success(pkg .setLeavingSharedUid(leaving) diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java index 68e078c519ba..9213c87f12ec 100644 --- a/services/core/java/com/android/server/policy/KeyCombinationManager.java +++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java @@ -48,7 +48,7 @@ public class KeyCombinationManager { // The rule has been triggered by current keys. @GuardedBy("mLock") private TwoKeysCombinationRule mTriggeredRule; - private final Handler mHandler = new Handler(); + private final Handler mHandler; // Keys in a key combination must be pressed within this interval of each other. private static final long COMBINE_KEY_DELAY_MILLIS = 150; @@ -93,6 +93,11 @@ public class KeyCombinationManager { return false; } + // The excessive delay before it dispatching to client. + long getKeyInterceptDelayMs() { + return COMBINE_KEY_DELAY_MILLIS; + } + abstract void execute(); abstract void cancel(); @@ -103,7 +108,8 @@ public class KeyCombinationManager { } } - public KeyCombinationManager() { + public KeyCombinationManager(Handler handler) { + mHandler = handler; } void addRule(TwoKeysCombinationRule rule) { @@ -195,10 +201,18 @@ public class KeyCombinationManager { */ long getKeyInterceptTimeout(int keyCode) { synchronized (mLock) { - if (forAllActiveRules((rule) -> rule.shouldInterceptKey(keyCode))) { - return mDownTimes.get(keyCode) + COMBINE_KEY_DELAY_MILLIS; + if (mDownTimes.get(keyCode) == 0) { + return 0; + } + long delayMs = 0; + for (final TwoKeysCombinationRule rule : mActiveRules) { + if (rule.shouldInterceptKey(keyCode)) { + delayMs = Math.max(delayMs, rule.getKeyInterceptDelayMs()); + } } - return 0; + // Make sure the delay is less than COMBINE_KEY_DELAY_MILLIS. + delayMs = Math.min(delayMs, COMBINE_KEY_DELAY_MILLIS); + return mDownTimes.get(keyCode) + delayMs; } } diff --git a/services/core/java/com/android/server/policy/PermissionPolicyInternal.java b/services/core/java/com/android/server/policy/PermissionPolicyInternal.java index 20b7ccd39287..92b9944b74cf 100644 --- a/services/core/java/com/android/server/policy/PermissionPolicyInternal.java +++ b/services/core/java/com/android/server/policy/PermissionPolicyInternal.java @@ -57,6 +57,7 @@ public abstract class PermissionPolicyInternal { * prompt should be shown if the app targets S-, is currently running in a visible, focused * task, has the REVIEW_REQUIRED flag set on its implicit notification permission, and has * created at least one notification channel (even if it has since been deleted). + * * @param packageName The package whose permission is being checked * @param userId The user for whom the package is being started * @param taskId The task the notification prompt should be attached to @@ -66,10 +67,22 @@ public abstract class PermissionPolicyInternal { /** * Determine if a particular task is in the proper state to show a system-triggered permission - * prompt. A prompt can be shown if the task is focused, visible, and running. + * prompt. A prompt can be shown if the task is focused, visible, and running and + * 1. The intent is a launcher intent (action is ACTION_MAIN, category is LAUNCHER), or + * 2. The activity belongs to the same package as the one which launched the task originally, + * and the task was started with a launcher intent + * * @param taskInfo The task to be checked + * @param currPkg The package of the current top visible activity + * @param intent The intent of the current top visible activity + */ + public abstract boolean shouldShowNotificationDialogForTask(@Nullable TaskInfo taskInfo, + @Nullable String currPkg, @Nullable Intent intent); + + /** + * @return true if an intent will resolve to a permission request dialog activity */ - public abstract boolean canShowPermissionPromptForTask(@Nullable TaskInfo taskInfo); + public abstract boolean isIntentToPermissionDialog(@NonNull Intent intent); /** * @return Whether the policy is initialized for a user. diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java index f2ce0d4c49d3..9328dd1a5f19 100644 --- a/services/core/java/com/android/server/policy/PermissionPolicyService.java +++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java @@ -21,6 +21,8 @@ import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_FOREGROUND; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_NONE; +import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; +import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS_FOR_OTHER; import static android.content.pm.PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION; import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED; import static android.content.pm.PackageManager.FLAG_PERMISSION_REVOKED_COMPAT; @@ -1016,7 +1018,7 @@ public final class PermissionPolicyService extends SystemService { ActivityInterceptorInfo info) { String action = info.intent.getAction(); ActivityInterceptResult result = null; - if (!PackageManager.ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action) + if (!ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action) && !PackageManager.ACTION_REQUEST_PERMISSIONS.equals(action)) { return null; } @@ -1033,7 +1035,7 @@ public final class PermissionPolicyService extends SystemService { && !mContinueNotifGrantMessageUids.contains(info.realCallingUid)) { return result; } - if (PackageManager.ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action)) { + if (ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action)) { String otherPkg = info.intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); if (otherPkg == null || (mPackageManager.getPermissionFlags( POST_NOTIFICATIONS, otherPkg, UserHandle.of(info.userId)) @@ -1052,8 +1054,8 @@ public final class PermissionPolicyService extends SystemService { public void onActivityLaunched(TaskInfo taskInfo, ActivityInfo activityInfo, ActivityInterceptorInfo info) { super.onActivityLaunched(taskInfo, activityInfo, info); - if (!shouldShowNotificationDialogOrClearFlags(info.intent, - info.checkedOptions)) { + if (!shouldShowNotificationDialogOrClearFlags(taskInfo, + activityInfo.packageName, info.intent, info.checkedOptions, true)) { return; } UserHandle user = UserHandle.of(taskInfo.userId); @@ -1085,7 +1087,7 @@ public final class PermissionPolicyService extends SystemService { return false; } - if (PackageManager.ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(intent.getAction()) + if (ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(intent.getAction()) && (callingUid != Process.SYSTEM_UID || !SYSTEM_PKG.equals(callingPackage))) { return false; } @@ -1104,18 +1106,48 @@ public final class PermissionPolicyService extends SystemService { launchNotificationPermissionRequestDialog(packageName, user, taskId); } + @Override + public boolean isIntentToPermissionDialog(@NonNull Intent intent) { + return Objects.equals(intent.getPackage(), + mPackageManager.getPermissionControllerPackageName()) + && (Objects.equals(intent.getAction(), ACTION_REQUEST_PERMISSIONS_FOR_OTHER) + || Objects.equals(intent.getAction(), ACTION_REQUEST_PERMISSIONS)); + } + + @Override + public boolean shouldShowNotificationDialogForTask(TaskInfo taskInfo, String currPkg, + Intent intent) { + return shouldShowNotificationDialogOrClearFlags( + taskInfo, currPkg, intent, null, false); + } + /** - * Determine if we should show a notification dialog, or clear the REVIEW_REQUIRED flag, - * from a particular package for a particular intent. Returns true if: + * Determine if a particular task is in the proper state to show a system-triggered + * permission prompt. A prompt can be shown if the task is just starting, or the task is + * currently focused, visible, and running, and, * 1. The isEligibleForLegacyPermissionPrompt ActivityOption is set, or - * 2. The intent is a launcher intent (action is ACTION_MAIN, category is LAUNCHER) + * 2. The intent is a launcher intent (action is ACTION_MAIN, category is LAUNCHER), or + * 3. The activity belongs to the same package as the one which launched the task + * originally, and the task was started with a launcher intent + * @param taskInfo The task to be checked + * @param currPkg The package of the current top visible activity + * @param intent The intent of the current top visible activity */ - private boolean shouldShowNotificationDialogOrClearFlags(Intent intent, - ActivityOptions options) { - if ((options != null && options.isEligibleForLegacyPermissionPrompt())) { - return true; + private boolean shouldShowNotificationDialogOrClearFlags(TaskInfo taskInfo, String currPkg, + Intent intent, ActivityOptions options, boolean activityStart) { + if (intent == null || currPkg == null || taskInfo == null + || (!(taskInfo.isFocused && taskInfo.isVisible && taskInfo.isRunning) + && !activityStart)) { + return false; } + return isLauncherIntent(intent) + || (options != null && options.isEligibleForLegacyPermissionPrompt()) + || (currPkg.equals(taskInfo.baseActivity.getPackageName()) + && isLauncherIntent(taskInfo.baseIntent)); + } + + private boolean isLauncherIntent(Intent intent) { return Intent.ACTION_MAIN.equals(intent.getAction()) && intent.getCategories() != null && (intent.getCategories().contains(Intent.CATEGORY_LAUNCHER) @@ -1144,7 +1176,7 @@ public final class PermissionPolicyService extends SystemService { Intent grantPermission = mPackageManager .buildRequestPermissionsIntent(new String[] { POST_NOTIFICATIONS }); grantPermission.setAction( - PackageManager.ACTION_REQUEST_PERMISSIONS_FOR_OTHER); + ACTION_REQUEST_PERMISSIONS_FOR_OTHER); grantPermission.putExtra(Intent.EXTRA_PACKAGE_NAME, pkgName); ActivityOptions options = new ActivityOptions(new Bundle()); @@ -1170,12 +1202,6 @@ public final class PermissionPolicyService extends SystemService { } } - @Override - public boolean canShowPermissionPromptForTask(@Nullable TaskInfo taskInfo) { - return taskInfo != null && taskInfo.isFocused && taskInfo.isVisible - && taskInfo.isRunning; - } - /** * Check if the intent action is removed for the calling package (often based on target SDK * version). If the action is removed, we'll silently cancel the activity launch. diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index cc93d4c1c3ff..e79148d52715 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2124,7 +2124,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } private void initKeyCombinationRules() { - mKeyCombinationManager = new KeyCombinationManager(); + mKeyCombinationManager = new KeyCombinationManager(mHandler); final boolean screenshotChordEnabled = mContext.getResources().getBoolean( com.android.internal.R.bool.config_enableScreenshotChord); @@ -2215,11 +2215,20 @@ public class PhoneWindowManager implements WindowManagerPolicy { mBackKeyHandled = true; interceptAccessibilityGestureTv(); } - @Override void cancel() { cancelAccessibilityGestureTv(); } + @Override + long getKeyInterceptDelayMs() { + // Use a timeout of 0 to prevent additional latency in processing of + // this key. This will potentially cause some unwanted UI actions if the + // user does end up triggering the key combination later, but in most + // cases, the user will simply hit a single key, and this will allow us + // to process it without first waiting to see if the combination is + // going to be triggered. + return 0; + } }); mKeyCombinationManager.addRule( @@ -2229,11 +2238,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { mBackKeyHandled = true; interceptBugreportGestureTv(); } - @Override void cancel() { cancelBugreportGestureTv(); } + @Override + long getKeyInterceptDelayMs() { + return 0; + } }); } } diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java index b05b44bcb1d2..77da75118958 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSettings.java +++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java @@ -355,8 +355,10 @@ final class VibrationSettings { } } - if (!shouldVibrateForRingerModeLocked(usage)) { - return Vibration.Status.IGNORED_FOR_RINGER_MODE; + if (!attrs.isFlagSet(VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY)) { + if (!shouldVibrateForRingerModeLocked(usage)) { + return Vibration.Status.IGNORED_FOR_RINGER_MODE; + } } } return null; @@ -386,12 +388,12 @@ final class VibrationSettings { * Return {@code true} if the device should vibrate for current ringer mode. * * <p>This checks the current {@link AudioManager#getRingerModeInternal()} against user settings - * for ringtone usage only. All other usages are allowed by this method. + * for ringtone and notification usages. All other usages are allowed by this method. */ @GuardedBy("mLock") private boolean shouldVibrateForRingerModeLocked(@VibrationAttributes.Usage int usageHint) { - if (usageHint != USAGE_RINGTONE) { - // Only ringtone vibrations are disabled when phone is on silent mode. + if ((usageHint != USAGE_RINGTONE) && (usageHint != USAGE_NOTIFICATION)) { + // Only ringtone and notification vibrations are disabled when phone is on silent mode. return true; } // If audio manager was not loaded yet then assume most restrictive mode. diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 9ec1079e46cc..1260e5dbc9f7 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -464,10 +464,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { && shouldCancelVibration( mCurrentExternalVibration.externalVibration.getVibrationAttributes(), usageFilter)) { - endVibrationLocked(mCurrentExternalVibration, Vibration.Status.CANCELLED); mCurrentExternalVibration.externalVibration.mute(); - mCurrentExternalVibration = null; - setExternalControl(false); + endExternalVibrateLocked(Vibration.Status.CANCELLED, + /* continueExternalControl= */ false); } } finally { Binder.restoreCallingIdentity(ident); @@ -1283,7 +1282,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } /** Holder for a {@link ExternalVibration}. */ - private final class ExternalVibrationHolder { + private final class ExternalVibrationHolder implements IBinder.DeathRecipient { public final ExternalVibration externalVibration; public int scale; @@ -1308,6 +1307,18 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mEndTimeDebug = System.currentTimeMillis(); } + public void binderDied() { + synchronized (mLock) { + if (mCurrentExternalVibration != null) { + if (DEBUG) { + Slog.d(TAG, "External vibration finished because binder died"); + } + endExternalVibrateLocked(Vibration.Status.CANCELLED, + /* continueExternalControl= */ false); + } + } + } + public Vibration.DebugInfo getDebugInfo() { return new Vibration.DebugInfo( mStartTimeDebug, mEndTimeDebug, /* effect= */ null, /* originalEffect= */ null, @@ -1450,10 +1461,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + /** + * Ends the external vibration, and clears related service state. + * + * @param status the status to end the associated Vibration with + * @param continueExternalControl indicates whether external control will continue. If not, the + * HAL will have external control turned off. + */ + @GuardedBy("mLock") + private void endExternalVibrateLocked(Vibration.Status status, + boolean continueExternalControl) { + Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "endExternalVibrateLocked"); + try { + if (mCurrentExternalVibration == null) { + return; + } + endVibrationLocked(mCurrentExternalVibration, status); + mCurrentExternalVibration.externalVibration.unlinkToDeath( + mCurrentExternalVibration); + mCurrentExternalVibration = null; + if (!continueExternalControl) { + setExternalControl(false); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + } + } + /** Implementation of {@link IExternalVibratorService} to be triggered on external control. */ @VisibleForTesting final class ExternalVibratorService extends IExternalVibratorService.Stub { - ExternalVibrationDeathRecipient mCurrentExternalDeathRecipient; @Override public int onExternalVibrationStart(ExternalVibration vib) { @@ -1469,7 +1506,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return IExternalVibratorService.SCALE_MUTE; } - ExternalVibrationHolder cancelingExternalVibration = null; + boolean alreadyUnderExternalControl = false; boolean waitForCompletion = false; int scale; synchronized (mLock) { @@ -1504,13 +1541,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // // Note that this doesn't support multiple concurrent external controls, as we // would need to mute the old one still if it came from a different controller. + alreadyUnderExternalControl = true; mCurrentExternalVibration.externalVibration.mute(); - endVibrationLocked(mCurrentExternalVibration, Vibration.Status.CANCELLED); - cancelingExternalVibration = mCurrentExternalVibration; + endExternalVibrateLocked(Vibration.Status.CANCELLED, + /* continueExternalControl= */ true); } mCurrentExternalVibration = new ExternalVibrationHolder(vib); - mCurrentExternalDeathRecipient = new ExternalVibrationDeathRecipient(); - vib.linkToDeath(mCurrentExternalDeathRecipient); + vib.linkToDeath(mCurrentExternalVibration); mCurrentExternalVibration.scale = mVibrationScaler.getExternalVibrationScale( vib.getVibrationAttributes().getUsage()); scale = mCurrentExternalVibration.scale; @@ -1520,14 +1557,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) { Slog.e(TAG, "Timed out waiting for vibration to cancel"); synchronized (mLock) { - stopExternalVibrateLocked(Vibration.Status.IGNORED_ERROR_CANCELLING); + endExternalVibrateLocked(Vibration.Status.IGNORED_ERROR_CANCELLING, + /* continueExternalControl= */ false); } return IExternalVibratorService.SCALE_MUTE; } } - if (cancelingExternalVibration == null) { - // We only need to set external control if it was not already set by another - // external vibration. + if (!alreadyUnderExternalControl) { if (DEBUG) { Slog.d(TAG, "Vibrator going under external control."); } @@ -1547,29 +1583,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.e(TAG, "Stopping external vibration" + vib); } - stopExternalVibrateLocked(Vibration.Status.FINISHED); + endExternalVibrateLocked(Vibration.Status.FINISHED, + /* continueExternalControl= */ false); } } } - @GuardedBy("mLock") - private void stopExternalVibrateLocked(Vibration.Status status) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "stopExternalVibrateLocked"); - try { - if (mCurrentExternalVibration == null) { - return; - } - endVibrationLocked(mCurrentExternalVibration, status); - mCurrentExternalVibration.externalVibration.unlinkToDeath( - mCurrentExternalDeathRecipient); - mCurrentExternalDeathRecipient = null; - mCurrentExternalVibration = null; - setExternalControl(false); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - private boolean hasExternalControlCapability() { for (int i = 0; i < mVibrators.size(); i++) { if (mVibrators.valueAt(i).hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) { @@ -1578,19 +1597,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } return false; } - - private class ExternalVibrationDeathRecipient implements IBinder.DeathRecipient { - public void binderDied() { - synchronized (mLock) { - if (mCurrentExternalVibration != null) { - if (DEBUG) { - Slog.d(TAG, "External vibration finished because binder died"); - } - stopExternalVibrateLocked(Vibration.Status.CANCELLED); - } - } - } - } } /** Provide limited functionality from {@link VibratorManagerService} as shell commands. */ diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java index 06c58baee1f9..1d65cbb70ffe 100644 --- a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java +++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java @@ -58,6 +58,7 @@ public abstract class ActivityInterceptorCallback { @IntDef(suffix = { "_ORDERED_ID" }, value = { FIRST_ORDERED_ID, PERMISSION_POLICY_ORDERED_ID, + INTENT_RESOLVER_ORDERED_ID, VIRTUAL_DEVICE_SERVICE_ORDERED_ID, LAST_ORDERED_ID // Update this when adding new ids }) @@ -75,6 +76,11 @@ public abstract class ActivityInterceptorCallback { public static final int PERMISSION_POLICY_ORDERED_ID = 1; /** + * The identifier for {@link com.android.server.pm.IntentResolverInterceptor}. + */ + public static final int INTENT_RESOLVER_ORDERED_ID = 2; + + /** * The identifier for {@link com.android.server.companion.virtual.VirtualDeviceManagerService} * interceptor. */ diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e66f309d2ccc..647ca9456393 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -2452,7 +2452,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // either way, abort and reset the sequence. if (parcelable == null || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING - || mStartingWindow == null) { + || mStartingWindow == null + || finishing) { if (parcelable != null) { parcelable.clearIfNeeded(); } diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index c5fcd12efa78..cdeb86c3ee77 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -355,10 +355,10 @@ class ActivityStartInterceptor { * @return The intercepting intent if needed. */ private Intent interceptWithConfirmCredentialsIfNeeded(ActivityInfo aInfo, int userId) { - if (!mService.mAmInternal.shouldConfirmCredentials(userId)) { + if ((aInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0 + || !mService.mAmInternal.shouldConfirmCredentials(userId)) { return null; } - // TODO(b/28935539): should allow certain activities to bypass work challenge final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE); final KeyguardManager km = (KeyguardManager) mServiceContext diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index cecfccd1f836..01dfb91d12be 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -680,4 +680,15 @@ public abstract class ActivityTaskManagerInternal { /** Get the app tasks for a package */ public abstract List<ActivityManager.AppTask> getAppTasks(String pkgName, int uid); + + /** + * Determine if there exists a task which meets the criteria set by the PermissionPolicyService + * to show a system-owned permission dialog over, for a given package + * @see PermissionPolicyInternal.shouldShowNotificationDialogForTask + * + * @param pkgName The package whose activity must be top + * @param uid The uid that must have a top activity + * @return a task ID if a valid task ID is found. Otherwise, return INVALID_TASK_ID + */ + public abstract int getTaskToShowPermissionDialogOn(String pkgName, int uid); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index bfccdf97c680..6d8b3b1d63dd 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6754,5 +6754,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } return tasks; } + + @Override + public int getTaskToShowPermissionDialogOn(String pkgName, int uid) { + synchronized (ActivityTaskManagerService.this.mGlobalLock) { + return ActivityTaskManagerService.this.mRootWindowContainer + .getTaskToShowPermissionDialogOn(pkgName, uid); + } + } } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 5573f161d1c1..d3d7d5e3780e 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -954,7 +954,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // This is the first time we failed -- restart process and // retry. r.launchFailed = true; - proc.removeActivity(r, true /* keepAssociation */); + r.detachFromProcess(); throw e; } } finally { @@ -1046,6 +1046,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // If a dead object exception was thrown -- fall through to // restart the application. knownToBeDead = true; + // Remove the process record so it won't be considered as alive. + mService.mProcessNames.remove(wpc.mName, wpc.mUid); + mService.mProcessMap.remove(wpc.getPid()); } r.notifyUnknownVisibilityLaunchedForKeyguardTransition(); diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index 9e889ad11b8e..220d9ec8febb 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -329,8 +329,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume void hideImmediately(WindowToken windowToken) { final boolean original = mHideImmediately; mHideImmediately = true; + final Operation op = new Operation(Operation.ACTION_FADE); + mTargetWindowTokens.put(windowToken, op); fadeWindowToken(false /* show */, windowToken, ANIMATION_TYPE_FIXED_TRANSFORM); + op.mLeash = windowToken.getAnimationLeash(); mHideImmediately = original; + if (DEBUG) Slog.d(TAG, "hideImmediately " + windowToken.getTopChild()); } /** Returns {@code true} if the window will rotate independently. */ diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index b9cd657da0e2..4b824483bffd 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -149,6 +149,7 @@ import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; import com.android.server.am.AppTimeTracker; import com.android.server.am.UserState; +import com.android.server.policy.PermissionPolicyInternal; import com.android.server.policy.WindowManagerPolicy; import java.io.FileDescriptor; @@ -2750,57 +2751,62 @@ class RootWindowContainer extends WindowContainer<DisplayContent> @Nullable ActivityOptions options, @Nullable Task candidateTask, @Nullable Task sourceTask, boolean onTop, @Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) { - int taskId = INVALID_TASK_ID; - int displayId = INVALID_DISPLAY; - TaskDisplayArea taskDisplayArea = null; - - // We give preference to the launch preference in activity options. + // First preference goes to the launch root task set in the activity options. if (options != null) { - taskId = options.getLaunchTaskId(); - displayId = options.getLaunchDisplayId(); - final WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); - taskDisplayArea = daToken != null - ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null; - - final Task rootTask = Task.fromWindowContainerToken(options.getLaunchRootTask()); - if (rootTask != null) { - return rootTask; + final Task candidateRoot = Task.fromWindowContainerToken(options.getLaunchRootTask()); + if (canLaunchOnDisplay(r, candidateRoot)) { + return candidateRoot; } } - // First preference for root task goes to the task Id set in the activity options. Use - // the root task associated with that if possible. - if (taskId != INVALID_TASK_ID) { - // Temporarily set the task id to invalid in case in re-entry. - options.setLaunchTaskId(INVALID_TASK_ID); - final Task task = anyTaskForId(taskId, - MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE, options, onTop); - options.setLaunchTaskId(taskId); - if (task != null) { - return task.getRootTask(); + // Next preference goes to the task id set in the activity options. + if (options != null) { + final int candidateTaskId = options.getLaunchTaskId(); + if (candidateTaskId != INVALID_TASK_ID) { + // Temporarily set the task id to invalid in case in re-entry. + options.setLaunchTaskId(INVALID_TASK_ID); + final Task task = anyTaskForId(candidateTaskId, + MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE, options, onTop); + options.setLaunchTaskId(candidateTaskId); + if (canLaunchOnDisplay(r, task)) { + return task.getRootTask(); + } } } - final int activityType = resolveActivityType(r, options, candidateTask); - Task rootTask = null; - - // Next preference for root task goes to the taskDisplayArea candidate. - if (launchParams != null && launchParams.mPreferredTaskDisplayArea != null - && canLaunchOnDisplay(r, launchParams.mPreferredTaskDisplayArea.getDisplayId())) { + // Next preference goes to the TaskDisplayArea candidate from launchParams + // or activity options. + TaskDisplayArea taskDisplayArea = null; + if (launchParams != null && launchParams.mPreferredTaskDisplayArea != null) { taskDisplayArea = launchParams.mPreferredTaskDisplayArea; + } else if (options != null) { + final WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); + taskDisplayArea = daToken != null + ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null; + if (taskDisplayArea == null) { + final int launchDisplayId = options.getLaunchDisplayId(); + if (launchDisplayId != INVALID_DISPLAY) { + final DisplayContent displayContent = getDisplayContent(launchDisplayId); + if (displayContent != null) { + taskDisplayArea = displayContent.getDefaultTaskDisplayArea(); + } + } + } } - if (taskDisplayArea == null && displayId != INVALID_DISPLAY - && canLaunchOnDisplay(r, displayId)) { - taskDisplayArea = getDisplayContent(displayId).getDefaultTaskDisplayArea(); - } + + final int activityType = resolveActivityType(r, options, candidateTask); if (taskDisplayArea != null) { - return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, - sourceTask, launchParams, launchFlags, activityType, onTop); + if (canLaunchOnDisplay(r, taskDisplayArea.getDisplayId())) { + return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, + sourceTask, launchParams, launchFlags, activityType, onTop); + } else { + taskDisplayArea = null; + } } // Give preference to the root task and display of the input task and activity if they // match the mode we want to launch into. - TaskDisplayArea container = null; + Task rootTask = null; if (candidateTask != null) { rootTask = candidateTask.getRootTask(); } @@ -2810,10 +2816,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> int windowingMode = launchParams != null ? launchParams.mWindowingMode : WindowConfiguration.WINDOWING_MODE_UNDEFINED; if (rootTask != null) { - container = rootTask.getDisplayArea(); - if (container != null && canLaunchOnDisplay(r, container.mDisplayContent.mDisplayId)) { + taskDisplayArea = rootTask.getDisplayArea(); + if (taskDisplayArea != null + && canLaunchOnDisplay(r, taskDisplayArea.mDisplayContent.mDisplayId)) { if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) { - windowingMode = container.resolveWindowingMode(r, options, candidateTask); + windowingMode = taskDisplayArea.resolveWindowingMode(r, options, candidateTask); } // Always allow organized tasks that created by organizer since the activity type // of an organized task is decided by the activity type of its top child, which @@ -2822,19 +2829,32 @@ class RootWindowContainer extends WindowContainer<DisplayContent> || rootTask.mCreatedByOrganizer) { return rootTask; } + } else { + taskDisplayArea = null; } + } - if (container == null - || !canLaunchOnDisplay(r, container.mDisplayContent.mDisplayId)) { - container = getDefaultTaskDisplayArea(); - if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) { - windowingMode = container.resolveWindowingMode(r, options, candidateTask); - } + // Falling back to default task container + if (taskDisplayArea == null) { + taskDisplayArea = getDefaultTaskDisplayArea(); + } + return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, sourceTask, + launchParams, launchFlags, activityType, onTop); + } + + private boolean canLaunchOnDisplay(ActivityRecord r, Task task) { + if (task == null) { + Slog.w(TAG, "canLaunchOnDisplay(), invalid task: " + task); + return false; + } + + if (!task.isAttached()) { + Slog.w(TAG, "canLaunchOnDisplay(), Task is not attached: " + task); + return false; } - return container.getOrCreateRootTask(r, options, candidateTask, sourceTask, launchParams, - launchFlags, activityType, onTop); + return canLaunchOnDisplay(r, task.getTaskDisplayArea().getDisplayId()); } /** @return true if activity record is null or can be launched on provided display. */ @@ -2842,7 +2862,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> if (r == null) { return true; } - return r.canBeLaunchedOnDisplay(displayId); + if (!r.canBeLaunchedOnDisplay(displayId)) { + Slog.w(TAG, "Not allow to launch " + r + " on display " + displayId); + return false; + } + return true; } int resolveActivityType(@Nullable ActivityRecord r, @Nullable ActivityOptions options, @@ -3309,6 +3333,36 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } /** + * Iterate over all task fragments, to see if there exists one that meets the + * PermissionPolicyService's criteria to show a permission dialog. + */ + public int getTaskToShowPermissionDialogOn(String pkgName, int uid) { + PermissionPolicyInternal pPi = mService.getPermissionPolicyInternal(); + if (pPi == null) { + return INVALID_TASK_ID; + } + + final int[] validTaskId = {INVALID_TASK_ID}; + forAllLeafTaskFragments(fragment -> { + ActivityRecord record = fragment.getActivity((r) -> { + // skip hidden (or about to hide) apps, or the permission dialog + return r.canBeTopRunning() && r.isVisibleRequested() + && !pPi.isIntentToPermissionDialog(r.intent); + }); + if (record != null && record.isUid(uid) + && Objects.equals(pkgName, record.packageName) + && pPi.shouldShowNotificationDialogForTask(record.getTask().getTaskInfo(), + pkgName, record.intent)) { + validTaskId[0] = record.getTask().mTaskId; + return true; + } + return false; + }); + + return validTaskId[0]; + } + + /** * Dumps the activities matching the given {@param name} in the either the focused root task * or all visible root tasks if {@param dumpVisibleRootTasksOnly} is true. */ diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 7fe34f47c53c..923ac41cec77 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -504,13 +504,6 @@ class Task extends TaskFragment { */ boolean mInRemoveTask; - // When non-null, this is a transaction that will get applied on the next frame returned after - // a relayout is requested from the client. While this is only valid on a leaf task; since the - // transaction can effect an ancestor task, this also needs to keep track of the ancestor task - // that this transaction manipulates because deferUntilFrame acts on individual surfaces. - SurfaceControl.Transaction mMainWindowSizeChangeTransaction; - Task mMainWindowSizeChangeTask; - private final AnimatingActivityRegistry mAnimatingActivityRegistry = new AnimatingActivityRegistry(); @@ -4390,17 +4383,16 @@ class Task extends TaskFragment { leaf.setMainWindowSizeChangeTransaction(t, origin); return; } - mMainWindowSizeChangeTransaction = t; - mMainWindowSizeChangeTask = t == null ? null : origin; - } - - SurfaceControl.Transaction getMainWindowSizeChangeTransaction() { - return mMainWindowSizeChangeTransaction; + final WindowState w = getTopVisibleAppMainWindow(); + if (w != null) { + w.applyWithNextDraw((d) -> { + d.merge(t); + }); + } else { + t.apply(); + } } - Task getMainWindowSizeChangeTask() { - return mMainWindowSizeChangeTask; - } void setActivityWindowingMode(int windowingMode) { PooledConsumer c = PooledLambda.obtainConsumer(ActivityRecord::setWindowingMode, diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 29d17429fa6a..b096bf1412cb 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1526,6 +1526,9 @@ class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListe if (task != null && task.voiceSession != null) { flags |= FLAG_IS_VOICE_INTERACTION; } + if (task != null && task.isTranslucent(null)) { + flags |= FLAG_TRANSLUCENT; + } final ActivityRecord record = wc.asActivityRecord(); if (record != null) { if (record.mUseTransferredAnimation) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 3dad2ba40627..db687f627bfa 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5988,15 +5988,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP finishDrawing(null); mWmService.mH.removeMessages(WINDOW_STATE_BLAST_SYNC_TIMEOUT, this); if (!useBLASTSync()) return; - - final Task task = getTask(); - if (task != null) { - final SurfaceControl.Transaction t = task.getMainWindowSizeChangeTransaction(); - if (t != null) { - mSyncTransaction.merge(t); - } - task.setMainWindowSizeChangeTransaction(null); - } } @Override @@ -6033,10 +6024,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mRedrawForSyncReported) { return false; } - final Task task = getTask(); - if (task != null && task.getMainWindowSizeChangeTransaction() != null) { - return true; - } return useBLASTSync(); } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index c17961e8a564..285a6d5bdf5f 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -442,50 +442,6 @@ class WindowStateAnimator { return mService.useBLASTSync() && mWin.useBLASTSync(); } - private boolean shouldConsumeMainWindowSizeTransaction() { - // We only consume the transaction when the client is calling relayout - // because this is the only time we know the frameNumber will be valid - // due to the client renderer being paused. Put otherwise, only when - // mInRelayout is true can we guarantee the next frame will contain - // the most recent configuration. - if (!mWin.mInRelayout) return false; - // Since we can only do this for one window, we focus on the main application window - if (mAttrType != TYPE_BASE_APPLICATION) return false; - final Task task = mWin.getTask(); - if (task == null) return false; - if (task.getMainWindowSizeChangeTransaction() == null) return false; - // Likewise we only focus on the task root, since we can only use one window - if (!mWin.mActivityRecord.isRootOfTask()) return false; - return true; - } - - void setSurfaceBoundariesLocked(SurfaceControl.Transaction t) { - if (mSurfaceController == null) { - return; - } - - final WindowState w = mWin; - final Task task = w.getTask(); - if (shouldConsumeMainWindowSizeTransaction()) { - if (isInBlastSync()) { - // If we're in a sync transaction, there's no need to call defer transaction. - // The sync transaction will contain the buffer so the bounds change transaction - // will only be applied with the buffer. - t.merge(task.getMainWindowSizeChangeTransaction()); - task.setMainWindowSizeChangeTransaction(null); - } else { - mWin.applyWithNextDraw(finishedFrame -> { - final SurfaceControl.Transaction sizeChangedTransaction = - task.getMainWindowSizeChangeTransaction(); - if (sizeChangedTransaction != null) { - finishedFrame.merge(sizeChangedTransaction); - task.setMainWindowSizeChangeTransaction(null); - } - }); - } - } - } - void prepareSurfaceLocked(SurfaceControl.Transaction t) { final WindowState w = mWin; if (!hasSurface()) { @@ -501,8 +457,6 @@ class WindowStateAnimator { computeShownFrameLocked(); - setSurfaceBoundariesLocked(t); - if (w.isParentWindowHidden() || !w.isOnScreen()) { hide(t, "prepareSurfaceLocked"); mWallpaperControllerLocked.hideWallpapers(w); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 7178cbd460c0..5388f171854d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -87,6 +87,7 @@ import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_UNKNOWN; import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_SET_ERROR_FAILURE_SETTING; import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_SET_NO_ERROR; import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER; +import static android.app.admin.DevicePolicyManager.STATE_USER_SETUP_FINALIZED; import static android.app.admin.DevicePolicyManager.STATE_USER_UNMANAGED; import static android.app.admin.DevicePolicyManager.STATUS_ACCOUNTS_NOT_EMPTY; import static android.app.admin.DevicePolicyManager.STATUS_CANNOT_ADD_MANAGED_PROFILE; @@ -9225,7 +9226,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // directly setting profile-owner or device-owner. if (getUserProvisioningState(userId) != DevicePolicyManager.STATE_USER_UNMANAGED - || newState != DevicePolicyManager.STATE_USER_SETUP_FINALIZED) { + || newState != STATE_USER_SETUP_FINALIZED) { throw new IllegalStateException("Not allowed to change provisioning state " + "unless current provisioning state is unmanaged, and new state" + "is finalized."); @@ -9259,7 +9260,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { case DevicePolicyManager.STATE_USER_SETUP_INCOMPLETE: case DevicePolicyManager.STATE_USER_SETUP_COMPLETE: // Can only move to finalized from these states. - if (newState == DevicePolicyManager.STATE_USER_SETUP_FINALIZED) { + if (newState == STATE_USER_SETUP_FINALIZED) { return; } break; @@ -9271,7 +9272,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return; } break; - case DevicePolicyManager.STATE_USER_SETUP_FINALIZED: + case STATE_USER_SETUP_FINALIZED: // Cannot transition out of finalized. break; case DevicePolicyManager.STATE_USER_PROFILE_FINALIZED: @@ -18092,7 +18093,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final CallerIdentity caller = getCallerIdentity(); Preconditions.checkCallAuthorization( - hasCallingOrSelfPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS)); + hasCallingOrSelfPermission(permission.MANAGE_PROFILE_AND_DEVICE_OWNERS) + || (hasCallingOrSelfPermission(permission.PROVISION_DEMO_DEVICE) + && provisioningParams.isDemoDevice())); provisioningParams.logParams(callerPackage); @@ -18129,8 +18132,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } disallowAddUser(); - setAdminCanGrantSensorsPermissionForUserUnchecked(deviceOwnerUserId, - provisioningParams.canDeviceOwnerGrantSensorsPermissions()); + setAdminCanGrantSensorsPermissionForUserUnchecked( + deviceOwnerUserId, provisioningParams.canDeviceOwnerGrantSensorsPermissions()); + setDemoDeviceStateUnchecked(deviceOwnerUserId, provisioningParams.isDemoDevice()); onProvisionFullyManagedDeviceCompleted(provisioningParams); sendProvisioningCompletedBroadcast( deviceOwnerUserId, @@ -18329,6 +18333,19 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + private void setDemoDeviceStateUnchecked(@UserIdInt int userId, boolean isDemoDevice) { + Slogf.d(LOG_TAG, "setDemoDeviceStateUnchecked(%d, %b)", + userId, isDemoDevice); + if (!isDemoDevice) { + return; + } + synchronized (getLockObject()) { + mInjector.settingsGlobalPutStringForUser( + Settings.Global.DEVICE_DEMO_MODE, Integer.toString(/* value= */ 1), userId); + } + setUserProvisioningState(STATE_USER_SETUP_FINALIZED, userId); + } + private void updateAdminCanGrantSensorsPermissionCache(@UserIdInt int userId) { synchronized (getLockObject()) { diff --git a/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java b/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java index b7f8c00896d4..8a9845b1c2d2 100644 --- a/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java +++ b/services/robotests/backup/src/com/android/server/backup/TransportManagerTest.java @@ -16,6 +16,10 @@ package com.android.server.backup; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + import static com.android.server.backup.testing.TransportData.genericTransport; import static com.android.server.backup.testing.TransportTestUtils.mockTransport; import static com.android.server.backup.testing.TransportTestUtils.setUpTransportsForTransportManager; @@ -312,6 +316,86 @@ public class TransportManagerTest { } @Test + public void testOnPackageChanged_whenPackageChanged_packageDisabledUnregistersTransport() + throws Exception { + TransportManager transportManager = + createTransportManagerWithRegisteredTransports(mTransportA1, mTransportB1); + reset(mListener); + + mContext.getPackageManager() + .setApplicationEnabledSetting( + PACKAGE_A, + Integer.valueOf(COMPONENT_ENABLED_STATE_DISABLED), + 0 /*flags*/); + transportManager.onPackageChanged(PACKAGE_A, PACKAGE_A); + + assertRegisteredTransports(transportManager, singletonList(mTransportB1)); + verify(mListener, never()).onTransportRegistered(any(), any()); + } + + @Test + public void testOnPackageChanged_whenPackageChanged_packageEnabledRegistersTransport() + throws Exception { + TransportManager transportManager = + createTransportManagerWithRegisteredTransports(mTransportA1, mTransportB1); + reset(mListener); + + mContext.getPackageManager() + .setApplicationEnabledSetting( + PACKAGE_A, + Integer.valueOf(COMPONENT_ENABLED_STATE_DISABLED), + 0 /*flags*/); + transportManager.onPackageChanged(PACKAGE_A, PACKAGE_A); + + assertRegisteredTransports(transportManager, singletonList(mTransportB1)); + verify(mListener, never()).onTransportRegistered(any(), any()); + + mContext.getPackageManager() + .setApplicationEnabledSetting( + PACKAGE_A, + Integer.valueOf(COMPONENT_ENABLED_STATE_ENABLED), + 0 /*flags*/); + transportManager.onPackageChanged(PACKAGE_A, PACKAGE_A); + + assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportB1)); + verify(mListener) + .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName); + } + + @Test + public void testOnPackageChanged_whenPackageChanged_unknownComponentStateIsIgnored() + throws Exception { + TransportManager transportManager = + createTransportManagerWithRegisteredTransports(mTransportA1, mTransportB1); + reset(mListener); + + mContext.getPackageManager() + .setApplicationEnabledSetting( + PACKAGE_A, + Integer.valueOf(COMPONENT_ENABLED_STATE_DEFAULT), + 0 /*flags*/); + transportManager.onPackageChanged(PACKAGE_A, PACKAGE_A); + + assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportB1)); + verify(mListener, never()).onTransportRegistered(any(), any()); + } + + @Test + public void testOnPackageChanged_whenPackageChanged_unknownPackageExceptionIsIgnored() + throws Exception { + TransportManager transportManager = + createTransportManagerWithRegisteredTransports(mTransportA1, mTransportB1); + reset(mListener); + + // empty packageName triggers Robolectric ApplicationPackageManager to throw + // exception as if package does not exist. + transportManager.onPackageChanged("", ""); + + assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportB1)); + verify(mListener, never()).onTransportRegistered(any(), any()); + } + + @Test public void testRegisterAndSelectTransport_whenTransportRegistered() throws Exception { TransportManager transportManager = createTransportManagerWithRegisteredTransports(null, mTransportA1); diff --git a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java index aea36e555ad7..4a9948668bc4 100644 --- a/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java +++ b/services/robotests/src/com/android/server/testing/shadows/ShadowApplicationPackageManager.java @@ -16,6 +16,7 @@ package com.android.server.testing.shadows; +import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.NameNotFoundException; import android.app.ApplicationPackageManager; @@ -44,6 +45,7 @@ public class ShadowApplicationPackageManager private static final List<PackageInfo> sInstalledPackages = new ArrayList<>(); private static final Map<String, Integer> sPackageUids = new ArrayMap<>(); private static final Map<Integer, Map<String, Integer>> sUserPackageUids = new ArrayMap<>(); + private static final Map<String, Integer> sPackageAppEnabledStates = new ArrayMap<>(); /** * Registers the package {@code packageName} to be returned when invoking {@link @@ -53,6 +55,7 @@ public class ShadowApplicationPackageManager public static void addInstalledPackage(String packageName, PackageInfo packageInfo) { sPackageInfos.put(packageName, packageInfo); sInstalledPackages.add(packageInfo); + sPackageAppEnabledStates.put(packageName, Integer.valueOf(COMPONENT_ENABLED_STATE_DEFAULT)); } /** @@ -77,6 +80,22 @@ public class ShadowApplicationPackageManager } @Override + protected int getApplicationEnabledSetting(String packageName) { + if (packageName.isEmpty()) { + throw new IllegalArgumentException("Robo: Package '' does not exist."); + } + if (!sPackageAppEnabledStates.containsKey(packageName)) { + return COMPONENT_ENABLED_STATE_DEFAULT; + } + return sPackageAppEnabledStates.get(packageName); + } + + @Override + protected void setApplicationEnabledSetting(String packageName, int newState, int flags) { + sPackageAppEnabledStates.put(packageName, Integer.valueOf(newState)); // flags unused here. + } + + @Override protected PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId) throws NameNotFoundException { if (!sPackageInfos.containsKey(packageName)) { @@ -115,6 +134,7 @@ public class ShadowApplicationPackageManager public static void reset() { sPackageInfos.clear(); sInstalledPackages.clear(); + sPackageAppEnabledStates.clear(); org.robolectric.shadows.ShadowApplicationPackageManager.reset(); } } diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index a9812ab9bb03..d104871f488a 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -89,6 +89,7 @@ public class HdmiCecLocalDeviceTvTest { private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>(); private int mTvPhysicalAddress; private int mTvLogicalAddress; + private boolean mWokenUp; @Mock private AudioManager mAudioManager; @@ -104,6 +105,11 @@ public class HdmiCecLocalDeviceTvTest { new HdmiControlService(InstrumentationRegistry.getTargetContext(), Collections.emptyList()) { @Override + void wakeUp() { + mWokenUp = true; + super.wakeUp(); + } + @Override boolean isControlEnabled() { return true; } @@ -308,6 +314,22 @@ public class HdmiCecLocalDeviceTvTest { } @Test + public void handleTextViewOn_Dreaming() { + mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().setIntValue( + HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY, + HdmiControlManager.TV_WAKE_ON_ONE_TOUCH_PLAY_ENABLED); + mTestLooper.dispatchAll(); + mPowerManager.setInteractive(true); + mWokenUp = false; + HdmiCecMessage textViewOn = HdmiCecMessageBuilder.buildTextViewOn(ADDR_PLAYBACK_1, + mTvLogicalAddress); + assertThat(mHdmiCecLocalDeviceTv.dispatchMessage(textViewOn)).isEqualTo(Constants.HANDLED); + mTestLooper.dispatchAll(); + assertThat(mPowerManager.isInteractive()).isTrue(); + assertThat(mWokenUp).isTrue(); + } + + @Test public void tvSendStandbyOnSleep_Enabled() { mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().setIntValue( HdmiControlManager.CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP, diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java index 3cda2a554e48..ec16188bfc1d 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java @@ -288,7 +288,7 @@ public class VibrationSettingsTest { } @Test - public void shouldIgnoreVibration_withRingerModeSilent_ignoresRingtoneOnly() { + public void shouldIgnoreVibration_withRingerModeSilent_ignoresRingtoneAndNotification() { // Vibrating settings on are overruled by ringer mode. setUserSetting(Settings.System.HAPTIC_FEEDBACK_ENABLED, 1); setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1); @@ -296,7 +296,7 @@ public class VibrationSettingsTest { setRingerMode(AudioManager.RINGER_MODE_SILENT); for (int usage : ALL_USAGES) { - if (usage == USAGE_RINGTONE) { + if (usage == USAGE_RINGTONE || usage == USAGE_NOTIFICATION) { assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_FOR_RINGER_MODE); } else { assertVibrationNotIgnoredForUsage(usage); @@ -305,6 +305,16 @@ public class VibrationSettingsTest { } @Test + public void shouldIgnoreVibration_withRingerModeSilentAndBypassFlag_allowsAllVibrations() { + setRingerMode(AudioManager.RINGER_MODE_SILENT); + + for (int usage : ALL_USAGES) { + assertVibrationNotIgnoredForUsageAndFlags(usage, + VibrationAttributes.FLAG_BYPASS_INTERRUPTION_POLICY); + } + } + + @Test public void shouldIgnoreVibration_withRingerModeVibrate_allowsAllVibrations() { setRingerMode(AudioManager.RINGER_MODE_VIBRATE); diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java index e46e28199d5a..92736c517782 100644 --- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -1136,19 +1136,23 @@ public class VibratorManagerServiceTest { } @Test - public void onExternalVibration_setsExternalControl() { + public void onExternalVibration_setsExternalControl() throws Exception { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); createSystemReadyService(); + IBinder binderToken = mock(IBinder.class); ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ATTRS, - mock(IExternalVibrationController.class)); + mock(IExternalVibrationController.class), binderToken); int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration); mExternalVibratorService.onExternalVibrationStop(externalVibration); assertNotEquals(IExternalVibratorService.SCALE_MUTE, scale); assertEquals(Arrays.asList(false, true, false), mVibratorProviders.get(1).getExternalControlStates()); + + verify(binderToken).linkToDeath(any(), eq(0)); + verify(binderToken).unlinkToDeath(any(), eq(0)); } @Test @@ -1160,17 +1164,19 @@ public class VibratorManagerServiceTest { setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1); createSystemReadyService(); + IBinder firstToken = mock(IBinder.class); + IBinder secondToken = mock(IBinder.class); IExternalVibrationController firstController = mock(IExternalVibrationController.class); IExternalVibrationController secondController = mock(IExternalVibrationController.class); ExternalVibration firstVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ATTRS, - firstController); + firstController, firstToken); int firstScale = mExternalVibratorService.onExternalVibrationStart(firstVibration); AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build(); ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME, - ringtoneAudioAttrs, secondController); + ringtoneAudioAttrs, secondController, secondToken); int secondScale = mExternalVibratorService.onExternalVibrationStart(secondVibration); assertNotEquals(IExternalVibratorService.SCALE_MUTE, firstScale); @@ -1180,6 +1186,17 @@ public class VibratorManagerServiceTest { // Set external control called only once. assertEquals(Arrays.asList(false, true), mVibratorProviders.get(1).getExternalControlStates()); + + mExternalVibratorService.onExternalVibrationStop(secondVibration); + mExternalVibratorService.onExternalVibrationStop(firstVibration); + assertEquals(Arrays.asList(false, true, false), + mVibratorProviders.get(1).getExternalControlStates()); + + verify(firstToken).linkToDeath(any(), eq(0)); + verify(firstToken).unlinkToDeath(any(), eq(0)); + + verify(secondToken).linkToDeath(any(), eq(0)); + verify(secondToken).unlinkToDeath(any(), eq(0)); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 71f8b8de032b..d752322f567a 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -18,6 +18,7 @@ package com.android.server.notification; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.Notification.FLAG_AUTO_CANCEL; @@ -432,8 +433,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mUgmInternal.newUriPermissionOwner(anyString())).thenReturn(mPermOwner); when(mPackageManager.getPackagesForUid(mUid)).thenReturn(new String[]{PKG}); when(mPackageManagerClient.getPackagesForUid(anyInt())).thenReturn(new String[]{PKG}); - when(mPermissionPolicyInternal.canShowPermissionPromptForTask( - any(ActivityManager.RecentTaskInfo.class))).thenReturn(false); + when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt())) + .thenReturn(INVALID_TASK_ID); mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class)); when(mUm.getProfileIds(0, false)).thenReturn(new int[]{0}); @@ -971,8 +972,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCreateNotificationChannels_FirstChannelWithFgndTaskStartsPermDialog() throws Exception { - when(mPermissionPolicyInternal.canShowPermissionPromptForTask(any( - ActivityManager.RecentTaskInfo.class))).thenReturn(true); + when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt())).thenReturn(TEST_TASK_ID); final NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); mBinderService.createNotificationChannels(PKG_NO_CHANNELS, @@ -985,8 +985,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testCreateNotificationChannels_SecondChannelWithFgndTaskDoesntStartPermDialog() throws Exception { - when(mPermissionPolicyInternal.canShowPermissionPromptForTask(any( - ActivityManager.RecentTaskInfo.class))).thenReturn(true); + when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt())).thenReturn(TEST_TASK_ID); assertTrue(mBinderService.getNumNotificationChannelsForPackage(PKG, mUid, true) > 0); final NotificationChannel channel = @@ -1001,8 +1000,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testCreateNotificationChannels_FirstChannelWithBgndTaskDoesntStartPermDialog() throws Exception { reset(mPermissionPolicyInternal); - when(mPermissionPolicyInternal.canShowPermissionPromptForTask(any( - ActivityManager.RecentTaskInfo.class))).thenReturn(false); + when(mAtm.getTaskToShowPermissionDialogOn(anyString(), anyInt())).thenReturn(TEST_TASK_ID); final NotificationChannel channel = new NotificationChannel("id", "name", IMPORTANCE_DEFAULT); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 8f1eed89647c..7d5a0d0bf84d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -1870,46 +1870,46 @@ public class PreferencesHelperTest extends UiServiceTestCase { @Test public void testGetChannelsBypassingDndCount_noChannelsBypassing() throws Exception { assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - USER.getIdentifier()).getList().size()); + UID_N_MR1).getList().size()); } @Test - public void testGetChannelsBypassingDnd_noChannelsForUserIdBypassing() + public void testGetChannelsBypassingDnd_noChannelsForUidBypassing() throws Exception { - int user = 9; + int uid = 222; NotificationChannel channel = new NotificationChannel("id", "name", NotificationManager.IMPORTANCE_MAX); channel.setBypassDnd(true); mHelper.createNotificationChannel(PKG_N_MR1, 111, channel, true, true); assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - user).getList().size()); + uid).getList().size()); } @Test public void testGetChannelsBypassingDndCount_oneChannelBypassing_groupBlocked() { - int user = USER.getIdentifier(); + int uid = UID_N_MR1; NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_MAX); channel1.setBypassDnd(true); channel1.setGroup(ncg.getId()); - mHelper.createNotificationChannelGroup(PKG_N_MR1, user, ncg, /* fromTargetApp */ true); - mHelper.createNotificationChannel(PKG_N_MR1, user, channel1, true, /*has DND access*/ true); + mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, ncg, /* fromTargetApp */ true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel1, true, /*has DND access*/ true); assertEquals(1, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - user).getList().size()); + uid).getList().size()); // disable group ncg.setBlocked(true); - mHelper.createNotificationChannelGroup(PKG_N_MR1, user, ncg, /* fromTargetApp */ false); + mHelper.createNotificationChannelGroup(PKG_N_MR1, uid, ncg, /* fromTargetApp */ false); assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - user).getList().size()); + uid).getList().size()); } @Test public void testGetChannelsBypassingDndCount_multipleChannelsBypassing() { - int user = USER.getIdentifier(); + int uid = UID_N_MR1; NotificationChannel channel1 = new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_MAX); NotificationChannel channel2 = new NotificationChannel("id2", "name2", @@ -1920,22 +1920,22 @@ public class PreferencesHelperTest extends UiServiceTestCase { channel2.setBypassDnd(true); channel3.setBypassDnd(true); // has DND access, so can set bypassDnd attribute - mHelper.createNotificationChannel(PKG_N_MR1, user, channel1, true, /*has DND access*/ true); - mHelper.createNotificationChannel(PKG_N_MR1, user, channel2, true, true); - mHelper.createNotificationChannel(PKG_N_MR1, user, channel3, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel1, true, /*has DND access*/ true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel2, true, true); + mHelper.createNotificationChannel(PKG_N_MR1, uid, channel3, true, true); assertEquals(3, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - user).getList().size()); + uid).getList().size()); // setBypassDnd false for some channels channel1.setBypassDnd(false); channel2.setBypassDnd(false); assertEquals(1, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - user).getList().size()); + uid).getList().size()); // setBypassDnd false for rest of the channels channel3.setBypassDnd(false); assertEquals(0, mHelper.getNotificationChannelsBypassingDnd(PKG_N_MR1, - user).getList().size()); + uid).getList().size()); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java index abcc8c1e99cb..8ac729e29424 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java @@ -121,7 +121,7 @@ public class ZenModeFilteringTest extends UiServiceTestCase { // Create a notification record with the people String array as the // bundled extras, and the numbers ArraySet as additional phone numbers. - private NotificationRecord getRecordWithPeopleInfo(String[] people, + private NotificationRecord getCallRecordWithPeopleInfo(String[] people, ArraySet<String> numbers) { // set up notification record NotificationRecord r = mock(NotificationRecord.class); @@ -131,6 +131,8 @@ public class ZenModeFilteringTest extends UiServiceTestCase { when(sbn.getNotification()).thenReturn(notification); when(r.getSbn()).thenReturn(sbn); when(r.getPhoneNumbers()).thenReturn(numbers); + when(r.getCriticality()).thenReturn(CriticalNotificationExtractor.NORMAL); + when(r.isCategory(CATEGORY_CALL)).thenReturn(true); return r; } @@ -350,11 +352,41 @@ public class ZenModeFilteringTest extends UiServiceTestCase { } @Test + public void testRepeatCallers_checksPhoneNumbers() { + // set up telephony manager behavior + when(mTelephonyManager.getNetworkCountryIso()).thenReturn("us"); + + // first, record a phone call from a telephone number + String[] callNumber = new String[]{"tel:12345678910"}; + mZenModeFiltering.recordCall(getCallRecordWithPeopleInfo(callNumber, null)); + + // set up policy to only allow repeat callers + Policy policy = new Policy( + PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0, 0, CONVERSATION_SENDERS_NONE); + + // make sure that a record with the phone number in extras is correctly allowed through + NotificationRecord r = getCallRecordWithPeopleInfo(callNumber, null); + assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r)); + + // make sure that a record with the phone number in the phone numbers array is also + // allowed through + NotificationRecord r2 = getCallRecordWithPeopleInfo(new String[]{"some_contact_uri"}, + new ArraySet<>(new String[]{"12345678910"})); + assertFalse(mZenModeFiltering.shouldIntercept( + ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r2)); + + // A record with the phone number in neither of the above should be intercepted + NotificationRecord r3 = getCallRecordWithPeopleInfo(new String[]{"tel:10987654321"}, + new ArraySet<>(new String[]{"15555555555"})); + assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r3)); + } + + @Test public void testMatchesCallFilter_repeatCallers_directMatch() { // after calls given an email with an exact string match, make sure that // matchesCallFilter returns the right thing String[] mailSource = new String[]{"mailto:hello.world"}; - mZenModeFiltering.recordCall(getRecordWithPeopleInfo(mailSource, null)); + mZenModeFiltering.recordCall(getCallRecordWithPeopleInfo(mailSource, null)); // set up policy to only allow repeat callers Policy policy = new Policy( @@ -377,7 +409,7 @@ public class ZenModeFilteringTest extends UiServiceTestCase { when(mTelephonyManager.getNetworkCountryIso()).thenReturn("us"); String[] telSource = new String[]{"tel:+1-617-555-1212"}; - mZenModeFiltering.recordCall(getRecordWithPeopleInfo(telSource, null)); + mZenModeFiltering.recordCall(getCallRecordWithPeopleInfo(telSource, null)); // set up policy to only allow repeat callers Policy policy = new Policy( @@ -421,7 +453,7 @@ public class ZenModeFilteringTest extends UiServiceTestCase { when(mTelephonyManager.getNetworkCountryIso()).thenReturn("us"); String[] telSource = new String[]{"tel:%2B16175551212"}; - mZenModeFiltering.recordCall(getRecordWithPeopleInfo(telSource, null)); + mZenModeFiltering.recordCall(getCallRecordWithPeopleInfo(telSource, null)); // set up policy to only allow repeat callers Policy policy = new Policy( @@ -468,7 +500,7 @@ public class ZenModeFilteringTest extends UiServiceTestCase { String[] contactSource = new String[]{"content://contacts/lookup/uri-here"}; ArraySet<String> contactNumbers = new ArraySet<>( new String[]{"1-617-555-1212", "1-617-555-3434"}); - NotificationRecord record = getRecordWithPeopleInfo(contactSource, contactNumbers); + NotificationRecord record = getCallRecordWithPeopleInfo(contactSource, contactNumbers); record.mergePhoneNumbers(contactNumbers); mZenModeFiltering.recordCall(record); diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java index 75479de26b1d..cf571815b4cb 100644 --- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationTests.java @@ -36,6 +36,9 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + /** * Test class for {@link KeyCombinationManager}. * @@ -47,16 +50,18 @@ import org.junit.Test; public class KeyCombinationTests { private KeyCombinationManager mKeyCombinationManager; - private boolean mAction1Triggered = false; - private boolean mAction2Triggered = false; - private boolean mAction3Triggered = false; + private final CountDownLatch mAction1Triggered = new CountDownLatch(1); + private final CountDownLatch mAction2Triggered = new CountDownLatch(1); + private final CountDownLatch mAction3Triggered = new CountDownLatch(1); private boolean mPreCondition = true; private static final long SCHEDULE_TIME = 300; + private Handler mHandler; @Before public void setUp() { - mKeyCombinationManager = new KeyCombinationManager(); + mHandler = new Handler(Looper.getMainLooper()); + mKeyCombinationManager = new KeyCombinationManager(mHandler); initKeyCombinationRules(); } @@ -67,7 +72,7 @@ public class KeyCombinationTests { KEYCODE_POWER) { @Override void execute() { - mAction1Triggered = true; + mAction1Triggered.countDown(); } @Override @@ -86,12 +91,17 @@ public class KeyCombinationTests { @Override void execute() { - mAction2Triggered = true; + mAction2Triggered.countDown(); } @Override void cancel() { } + + @Override + long getKeyInterceptDelayMs() { + return 0; + } }); // Rule 3 : power + volume_up schedule and trigger action after timeout. @@ -100,10 +110,9 @@ public class KeyCombinationTests { final Runnable mAction = new Runnable() { @Override public void run() { - mAction3Triggered = true; + mAction3Triggered.countDown(); } }; - final Handler mHandler = new Handler(Looper.getMainLooper()); @Override void execute() { @@ -149,60 +158,74 @@ public class KeyCombinationTests { } @Test - public void testTriggerRule() { + public void testTriggerRule() throws InterruptedException { final long eventTime = SystemClock.uptimeMillis(); pressKeys(eventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_DOWN); - assertTrue(mAction1Triggered); + assertTrue(mAction1Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); pressKeys(eventTime, KEYCODE_VOLUME_UP, eventTime, KEYCODE_VOLUME_DOWN); - assertTrue(mAction2Triggered); + assertTrue(mAction2Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); pressKeys(eventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_UP, SCHEDULE_TIME + 50); - assertTrue(mAction3Triggered); + assertTrue(mAction3Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); } /** * Nothing should happen if there is no definition. */ @Test - public void testNotTrigger_NoRule() { + public void testNotTrigger_NoRule() throws InterruptedException { final long eventTime = SystemClock.uptimeMillis(); pressKeys(eventTime, KEYCODE_BACK, eventTime, KEYCODE_VOLUME_DOWN); - assertFalse(mAction1Triggered); - assertFalse(mAction2Triggered); - assertFalse(mAction3Triggered); + assertFalse(mAction1Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); + assertFalse(mAction2Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); + assertFalse(mAction3Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); } /** * Nothing should happen if the interval of press time is too long. */ @Test - public void testNotTrigger_Interval() { + public void testNotTrigger_Interval() throws InterruptedException { final long eventTime = SystemClock.uptimeMillis(); final long earlyEventTime = eventTime - 200; // COMBINE_KEY_DELAY_MILLIS = 150; pressKeys(earlyEventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_DOWN); - assertFalse(mAction1Triggered); + assertFalse(mAction1Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); } /** * Nothing should happen if the condition is false. */ @Test - public void testNotTrigger_Condition() { + public void testNotTrigger_Condition() throws InterruptedException { final long eventTime = SystemClock.uptimeMillis(); // we won't trigger action 2 because the condition is false. mPreCondition = false; pressKeys(eventTime, KEYCODE_VOLUME_UP, eventTime, KEYCODE_VOLUME_DOWN); - assertFalse(mAction2Triggered); + assertFalse(mAction2Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); } /** * Nothing should happen if the keys released too early. */ @Test - public void testNotTrigger_EarlyRelease() { + public void testNotTrigger_EarlyRelease() throws InterruptedException { final long eventTime = SystemClock.uptimeMillis(); pressKeys(eventTime, KEYCODE_POWER, eventTime, KEYCODE_VOLUME_UP); - assertFalse(mAction3Triggered); + assertFalse(mAction3Triggered.await(SCHEDULE_TIME, TimeUnit.MILLISECONDS)); + } + + /** + * The KeyInterceptTimeout should return the max timeout value. + */ + @Test + public void testKeyInterceptTimeout() { + final long eventTime = SystemClock.uptimeMillis(); + final KeyEvent firstKeyDown = new KeyEvent(eventTime, eventTime, ACTION_DOWN, + KEYCODE_VOLUME_UP, 0 /* repeat */, 0 /* metaState */); + // Press KEYCODE_VOLUME_UP would activate rule2 and rule3, + // and rule2's intercept delay is 0. + mKeyCombinationManager.interceptKey(firstKeyDown, true); + assertTrue(mKeyCombinationManager.getKeyInterceptTimeout(KEYCODE_VOLUME_UP) > eventTime); } -} +}
\ No newline at end of file diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 12f987dfcd8d..3d95ec599b1b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1370,7 +1370,9 @@ public class DisplayContentTests extends WindowTestsBase { ROTATION_0 /* oldRotation */, ROTATION_90 /* newRotation */, false /* forceUpdate */)); - assertNotNull(mDisplayContent.getAsyncRotationController()); + final AsyncRotationController asyncRotationController = + mDisplayContent.getAsyncRotationController(); + assertNotNull(asyncRotationController); assertTrue(mStatusBarWindow.isAnimating(PARENTS, ANIMATION_TYPE_FIXED_TRANSFORM)); assertTrue(mNavBarWindow.isAnimating(PARENTS, ANIMATION_TYPE_FIXED_TRANSFORM)); // Notification shade may have its own view animation in real case so do not fade out it. @@ -1443,6 +1445,7 @@ public class DisplayContentTests extends WindowTestsBase { mDisplayContent.setImeLayeringTarget(mAppWindow); LocalServices.getService(WindowManagerInternal.class).onToggleImeRequested(true /* show */, app.token, app.token, mDisplayContent.mDisplayId); + assertTrue(asyncRotationController.isTargetToken(mImeWindow.mToken)); assertTrue(mImeWindow.mToken.hasFixedRotationTransform()); assertTrue(mImeWindow.isAnimating(PARENTS, ANIMATION_TYPE_FIXED_TRANSFORM)); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 8474c3829827..c4547f654c33 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -32,6 +32,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static android.window.TransitionInfo.isIndependent; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -328,6 +329,7 @@ public class TransitionTests extends WindowTestsBase { final ActivityRecord act = createActivityRecord(tasks[i]); // alternate so that the transition doesn't get promoted to the display area act.mVisibleRequested = (i % 2) == 0; // starts invisible + act.visibleIgnoringKeyguard = (i % 2) == 0; if (i == showWallpaperTask) { doReturn(true).when(act).showWallpaper(); } @@ -490,6 +492,86 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testOpenOpaqueTask() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task newTask = createTask(mDisplayContent); + doReturn(false).when(newTask).isTranslucent(any()); + final Task oldTask = createTask(mDisplayContent); + doReturn(false).when(oldTask).isTranslucent(any()); + + final ActivityRecord closing = createActivityRecord(oldTask); + closing.setOccludesParent(true); + final ActivityRecord opening = createActivityRecord(newTask); + opening.setOccludesParent(false); + // Start states. + changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); + changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); + changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + fillChangeMap(changes, newTask); + // End states. + closing.mVisibleRequested = true; + opening.mVisibleRequested = true; + + final int transit = transition.mType; + int flags = 0; + + // Check basic both tasks participating + participants.add(oldTask); + participants.add(newTask); + ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes); + TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes); + assertEquals(2, info.getChanges().size()); + assertEquals(transit, info.getType()); + + assertTrue((info.getChanges().get(0).getFlags() & FLAG_TRANSLUCENT) == 0); + assertTrue((info.getChanges().get(1).getFlags() & FLAG_TRANSLUCENT) == 0); + } + + @Test + public void testOpenTranslucentTask() { + final Transition transition = createTestTransition(TRANSIT_OPEN); + ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges; + ArraySet<WindowContainer> participants = transition.mParticipants; + + final Task newTask = createTask(mDisplayContent); + doReturn(true).when(newTask).isTranslucent(any()); + final Task oldTask = createTask(mDisplayContent); + doReturn(false).when(oldTask).isTranslucent(any()); + + final ActivityRecord closing = createActivityRecord(oldTask); + closing.setOccludesParent(true); + final ActivityRecord opening = createActivityRecord(newTask); + opening.setOccludesParent(false); + // Start states. + changes.put(newTask, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); + changes.put(oldTask, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + changes.put(opening, new Transition.ChangeInfo(false /* vis */, true /* exChg */)); + changes.put(closing, new Transition.ChangeInfo(true /* vis */, false /* exChg */)); + fillChangeMap(changes, newTask); + // End states. + closing.mVisibleRequested = true; + opening.mVisibleRequested = true; + + final int transit = transition.mType; + int flags = 0; + + // Check basic both tasks participating + participants.add(oldTask); + participants.add(newTask); + ArrayList<WindowContainer> targets = Transition.calculateTargets(participants, changes); + TransitionInfo info = Transition.calculateTransitionInfo(transit, flags, targets, changes); + assertEquals(2, info.getChanges().size()); + assertEquals(transit, info.getType()); + + assertTrue((info.getChanges().get(0).getFlags() & FLAG_TRANSLUCENT) != 0); + assertTrue((info.getChanges().get(1).getFlags() & FLAG_TRANSLUCENT) == 0); + } + + @Test public void testTimeout() { final TransitionController controller = new TransitionController(mAtm, mock(TaskSnapshotController.class)); diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 1999cfc706b4..b290669e103e 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -640,7 +640,7 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser Slog.e(TAG, "unknown state " + state); return; } - removeMessages(MSG_UPDATE_STATE); + if (configured == 0) removeMessages(MSG_UPDATE_STATE); if (connected == 1) removeMessages(MSG_FUNCTION_SWITCH_TIMEOUT); Message msg = Message.obtain(this, MSG_UPDATE_STATE); msg.arg1 = connected; diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 84bd98347dab..a597fc69a340 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -97,6 +97,7 @@ final class HotwordDetectionConnection { private static final Duration MAX_UPDATE_TIMEOUT_DURATION = Duration.ofMillis(MAX_UPDATE_TIMEOUT_MILLIS); private static final long RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS = 60 * 60 * 1000; // 1 hour + private static final int MAX_ISOLATED_PROCESS_NUMBER = 10; private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool(); // TODO: This may need to be a Handler(looper) @@ -772,7 +773,8 @@ final class HotwordDetectionConnection { ServiceConnection createLocked() { ServiceConnection connection = new ServiceConnection(mContext, mIntent, mBindingFlags, mUser, - IHotwordDetectionService.Stub::asInterface, ++mRestartCount); + IHotwordDetectionService.Stub::asInterface, + mRestartCount++ % MAX_ISOLATED_PROCESS_NUMBER); connection.connect(); updateAudioFlinger(connection, mAudioFlinger); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 0aaafef492bf..b6cacaf9f289 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1976,7 +1976,7 @@ public class SubscriptionManager { */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public void addSubscriptionInfoRecord(@NonNull String uniqueId, @Nullable String displayName, - int slotIndex, int subscriptionType) { + int slotIndex, @SubscriptionType int subscriptionType) { if (VDBG) { logd("[addSubscriptionInfoRecord]+ uniqueId:" + uniqueId + ", displayName:" + displayName + ", slotIndex:" + slotIndex @@ -2012,7 +2012,8 @@ public class SubscriptionManager { * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) - public void removeSubscriptionInfoRecord(@NonNull String uniqueId, int subscriptionType) { + public void removeSubscriptionInfoRecord(@NonNull String uniqueId, + @SubscriptionType int subscriptionType) { if (VDBG) { logd("[removeSubscriptionInfoRecord]+ uniqueId:" + uniqueId + ", subscriptionType: " + subscriptionType); |