diff options
130 files changed, 3260 insertions, 1136 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/test-current.txt b/core/api/test-current.txt index 5c6f5f6bafb6..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); 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 9bfdbd459574..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 = 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/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..0f9075b498ae 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -16,7 +16,6 @@ package android.inputmethodservice; -import static android.content.Intent.ACTION_OVERLAY_CHANGED; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import android.animation.ValueAnimator; @@ -24,18 +23,12 @@ import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.StatusBarManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; import android.inputmethodservice.navigationbar.NavigationBarFrame; import android.inputmethodservice.navigationbar.NavigationBarView; -import android.os.PatternMatcher; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -49,6 +42,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 +72,7 @@ final class NavigationBarController { default void onDestroy() { } - default void setShouldShowImeSwitcherWhenImeIsShown( - boolean shouldShowImeSwitcherWhenImeIsShown) { + default void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { } default String toDebugString() { @@ -117,8 +111,8 @@ final class NavigationBarController { mImpl.onDestroy(); } - void setShouldShowImeSwitcherWhenImeIsShown(boolean shouldShowImeSwitcherWhenImeIsShown) { - mImpl.setShouldShowImeSwitcherWhenImeIsShown(shouldShowImeSwitcherWhenImeIsShown); + void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { + mImpl.onNavButtonFlagsChanged(navButtonFlags); } String toDebugString() { @@ -144,9 +138,6 @@ final class NavigationBarController { @Nullable Insets mLastInsets; - @Nullable - private BroadcastReceiver mSystemOverlayChangedReceiver; - private boolean mShouldShowImeSwitcherWhenImeIsShown; @Appearance @@ -359,14 +350,6 @@ final class NavigationBarController { }); } - private boolean imeDrawsImeNavBar() { - final Resources resources = mService.getResources(); - if (resources == null) { - return false; - } - return resources.getBoolean(com.android.internal.R.bool.config_imeDrawsImeNavBar); - } - @Override public void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { final Window window = softInputWindow.getWindow(); @@ -379,27 +362,6 @@ final class NavigationBarController { if (mDestroyed) { return; } - mImeDrawsImeNavBar = imeDrawsImeNavBar(); - if (mSystemOverlayChangedReceiver == null) { - final IntentFilter intentFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); - intentFilter.addDataScheme(IntentFilter.SCHEME_PACKAGE); - intentFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); - mSystemOverlayChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (mDestroyed) { - return; - } - mImeDrawsImeNavBar = imeDrawsImeNavBar(); - if (mImeDrawsImeNavBar) { - installNavigationBarFrameIfNecessary(); - } else { - uninstallNavigationBarFrameIfNecessary(); - } - } - }; - mService.registerReceiver(mSystemOverlayChangedReceiver, intentFilter); - } installNavigationBarFrameIfNecessary(); } @@ -412,10 +374,6 @@ final class NavigationBarController { mTintAnimator.cancel(); mTintAnimator = null; } - if (mSystemOverlayChangedReceiver != null) { - mService.unregisterReceiver(mSystemOverlayChangedReceiver); - mSystemOverlayChangedReceiver = null; - } mDestroyed = true; } @@ -448,28 +406,43 @@ final class NavigationBarController { } @Override - public void setShouldShowImeSwitcherWhenImeIsShown( - boolean shouldShowImeSwitcherWhenImeIsShown) { + public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { if (mDestroyed) { return; } - if (mShouldShowImeSwitcherWhenImeIsShown == shouldShowImeSwitcherWhenImeIsShown) { - return; - } + + final boolean imeDrawsImeNavBar = + (navButtonFlags & InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR) != 0; + final boolean shouldShowImeSwitcherWhenImeIsShown = + (navButtonFlags & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) + != 0; + + mImeDrawsImeNavBar = imeDrawsImeNavBar; + final boolean prevShouldShowImeSwitcherWhenImeIsShown = + mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; - if (mNavigationBarFrame == null) { - return; - } - final NavigationBarView navigationBarView = - mNavigationBarFrame.findViewByPredicate(NavigationBarView.class::isInstance); - if (navigationBarView == null) { - return; + if (imeDrawsImeNavBar) { + installNavigationBarFrameIfNecessary(); + if (mNavigationBarFrame == null) { + return; + } + if (mShouldShowImeSwitcherWhenImeIsShown + == prevShouldShowImeSwitcherWhenImeIsShown) { + return; + } + final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( + NavigationBarView.class::isInstance); + if (navigationBarView == null) { + return; + } + final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT + | (shouldShowImeSwitcherWhenImeIsShown + ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); + navigationBarView.setNavigationIconHints(hints); + } else { + uninstallNavigationBarFrameIfNecessary(); } - final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT - | (shouldShowImeSwitcherWhenImeIsShown - ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); - navigationBarView.setNavigationIconHints(hints); } @Override 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/service/dreams/OWNERS b/core/java/android/service/dreams/OWNERS index f8318054ddfc..489a5f62b49d 100644 --- a/core/java/android/service/dreams/OWNERS +++ b/core/java/android/service/dreams/OWNERS @@ -1,3 +1,8 @@ # Bug component: 78010 +brycelee@google.com dsandler@google.com +galinap@google.com +jjaggi@google.com +michaelwr@google.com +santoscordon@google.com 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/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..728a42907cc4 --- /dev/null +++ b/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java @@ -0,0 +1,49 @@ +/* + * 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.IME_DRAWS_IME_NAV_BAR, + InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN, +}) +public @interface InputMethodNavButtonFlags { + /** + * When set, the IME process needs to render and handle the navigation bar buttons such as the + * back button and the IME switcher button. + */ + int IME_DRAWS_IME_NAV_BAR = 1; + /** + * When set, the IME switcher icon needs to be shown on the navigation bar. + */ + int SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN = 2; +} 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 a7f749c6bfbf..fa9eba34d30b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3718,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/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/graphics/java/android/graphics/RuntimeShader.java b/graphics/java/android/graphics/RuntimeShader.java index 2ff888b06dd8..6abe34b1d675 100644 --- a/graphics/java/android/graphics/RuntimeShader.java +++ b/graphics/java/android/graphics/RuntimeShader.java @@ -19,12 +19,228 @@ package android.graphics; import android.annotation.ColorInt; import android.annotation.ColorLong; import android.annotation.NonNull; +import android.view.Window; import libcore.util.NativeAllocationRegistry; /** - * Shader that calculates per-pixel color via a user defined Android Graphics Shading Language - * (AGSL) function. + * <p>A {@link RuntimeShader} calculates a per-pixel color based on the output of a user defined + * Android Graphics Shading Language (AGSL) function.</p> + * + * <h3>Android Graphics Shading Language</h3> + * <p>The AGSL syntax is very similar to OpenGL ES Shading Language, but there are some important + * differences that are highlighted here. Most of these differences are summed up in one basic fact: + * <b>With GPU shading languages, you are programming a stage of the GPU pipeline. With AGSL, you + * are programming a stage of the {@link Canvas} or {@link RenderNode} drawing pipeline.</b></p> + * + * <p>In particular, a GLSL fragment shader controls the entire behavior of the GPU between the + * rasterizer and the blending hardware. That shader does all of the work to compute a color, and + * the color it generates is exactly what is fed to the blending stage of the pipeline.</p> + * + * <p>In contrast, AGSL functions exist as part of a larger pipeline. When you issue a + * {@link Canvas} drawing operation, Android (generally) assembles a single GPU fragment shader to + * do all of the required work. This shader typically includes several pieces. For example, it might + * include:</p> + * <ul> + * <li>Evaluating whether a pixel falls inside or outside of the shape being drawn (or on the + * border, where it might apply antialiasing).</li> + * <li>Evaluating whether a pixel falls inside or outside of the clipping region (again, with + * possible antialiasing logic for border pixels).</li> + * <li>Logic for the {@link Shader}, {@link ColorFilter}, and {@link BlendMode} on the + * {@link Paint}.</li> + * <li>Color space conversion code, as part of Android’s color management.</li> + * </ul> + * + * <p>A {@link RuntimeShader}, like other {@link Shader} types, effectively contributes a function + * to the GPU’s fragment shader.</p> + * + * <h3>AGSL Shader Execution</h3> + * <p>Just like a GLSL shader, an AGSL shader begins execution in a main function. Unlike GLSL, the + * function receives as an input parameter the position of the pixel within the {@link Canvas} or + * {@link RenderNode} coordinate space (similar to gl_fragCoord) and returns the color to be shaded + * as a vec4 (similar to out vec4 color or gl_FragColor in GLSL).</p> + * + * <pre class="prettyprint"> + * vec4 main(vec2 canvas_coordinates); + * </pre> + * + * <p>AGSL and GLSL use different coordinate spaces by default. In GLSL, the fragment coordinate + * (fragCoord) is relative to the lower left. AGSL matches the screen coordinate system of the + * Android {@link Canvas} which has its origin as the upper left corner. This means that the + * coordinates provided as a parameter in the main function are local to the canvas with the + * exception of any {@link Shader#getLocalMatrix(Matrix)} transformations applied to this shader. + * Additionally, if the shader is invoked by another using {@link #setInputShader(String, Shader)}, + * then that parent shader may modify the input coordinates arbitrarily.</p> + * + * <h3>AGSL and Color Spaces</h3> + * <p>Android Graphics and by extension {@link RuntimeShader} are color managed. The working + * {@link ColorSpace} for an AGSL shader is defined to be the color space of the destination, which + * in most cases is determined by {@link Window#setColorMode(int)}.</p> + * + * <p>When authoring an AGSL shader, you won’t know what the working color space is. For many + * effects, this is fine because by default color inputs are automatically converted into the + * working color space. For certain effects, it may be important to do some math in a fixed, known + * color space. A common example is lighting – to get physically accurate lighting, math should be + * done in a linear color space. To help with this, AGSL provides two intrinsic functions that + * convert colors between the working color space and the + * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB} color space: + * + * <pre class="prettyprint"> + * vec3 toLinearSrgb(vec3 color); + * vec3 fromLinearSrgb(vec3 color);</pre> + * + * <h3>AGSL and Premultiplied Alpha</h3> + * <p>When dealing with transparent colors, there are two (common) possible representations: + * straight (unassociated) alpha and premultiplied (associated) alpha. In ASGL the color returned + * by the main function is expected to be premultiplied. AGSL’s use of premultiplied alpha + * implies: + * </p> + * + * <ul> + * <li>If your AGSL shader will return transparent colors, be sure to multiply the RGB by A. The + * resulting color should be [R*A, G*A, B*A, A], not [R, G, B, A].</li> + * <li>For more complex shaders, you must understand which of your colors are premultiplied vs. + * straight. Many operations don’t make sense if you mix both kinds of color together.</li> + * </ul> + * + * <h3>Uniforms</h3> + * <p>AGSL, like GLSL, exposes the concept of uniforms. An AGSL uniform is defined as a read-only, + * global variable that is accessible by the AGSL code and is initialized by a number of setter + * methods on {@link RuntimeShader}. AGSL exposes two primitive uniform data types (float, int) and + * two specialized types (colors, shaders) that are outlined below.</p> + * + * <h4>Primitive Uniforms</h4> + * <p>There are two primitive uniform types supported by AGSL, float and int. For these types and + * uniforms representing a grouping of these types, like arrays and matrices, there are + * corresponding {@link RuntimeShader} methods to initialize them. + * <table border="2" width="85%" align="center" cellpadding="5"> + * <thead> + * <tr><th>Java Type</th> <th>AGSL Type</th> <th>Method</th> </tr> + * </thead> + * + * <tbody> + * <tr> + * <td rowspan="4">Floats</td> + * <td>float</td> + * <td>{@link RuntimeShader#setFloatUniform(String, float)}</td> + * </tr> + * <tr> + * <td>vec2</td> + * <td>{@link RuntimeShader#setFloatUniform(String, float, float)}</td> + * </tr> + * <tr> + * <td>vec3</td> + * <td>{@link RuntimeShader#setFloatUniform(String, float, float, float)}</td> + * </tr> + * <tr> + * <td>vec4</td> + * <td>{@link RuntimeShader#setFloatUniform(String, float, float, float, float)}</td> + * </tr> + * <tr> + * <td rowspan="4">Integers</td> + * <td>int</td> + * <td>{@link RuntimeShader#setIntUniform(String, int)}</td> + * </tr> + * <tr> + * <td>ivec2</td> + * <td>{@link RuntimeShader#setIntUniform(String, int, int)}</td> + * </tr> + * <tr> + * <td>ivec3</td> + * <td>{@link RuntimeShader#setIntUniform(String, int, int, int)}</td> + * </tr> + * <tr> + * <td>ivec4</td> + * <td>{@link RuntimeShader#setIntUniform(String, int, int, int, int)}</td> + * </tr> + * <tr> + * <td rowspan="2">Matrices and Arrays</td> + * <td>mat2, mat3, and mat4, and float[]</td> + * <td>{@link RuntimeShader#setFloatUniform(String, float[])}</td> + * </tr> + * <tr> + * <td>int[]</td> + * <td>{@link RuntimeShader#setIntUniform(String, int[])}</td> + * </tr> + * </tbody> + * </table> + * + * For example, a simple AGSL shader making use of a float uniform to modulate the transparency + * of the output color would look like:</p> + * + * <pre class="prettyprint"> + * uniform float alpha; + * vec4 main(vec2 canvas_coordinates) { + * vec3 red = vec3(1.0, 0.0, 0.0); + * return vec4(red * alpha, alpha); + * }</pre> + * + * <p>After creating a {@link RuntimeShader} with that program the uniform can then be initialized + * and updated per frame by calling {@link RuntimeShader#setFloatUniform(String, float)} with the + * value of alpha. The value of a primitive uniform defaults to 0 if it is declared in the AGSL + * shader but not initialized.</p> + * + * <h4>Color Uniforms</h4> + * <p>AGSL doesn't know if uniform variables contain colors, it won't automatically convert them to + * the working colorspace of the shader at runtime. However, you can label your vec4 uniform with + * the "layout(color)" qualifier which lets Android know that the uniform will be used as a color. + * Doing so allows AGSL to transform the uniform value to the working color space. In AGSL, declare + * the uniform like this: + * + * <pre class="prettyprint"> + * layout(color) uniform vec4 inputColorA; + * layout(color) uniform vec4 inputColorB; + * vec4 main(vec2 canvas_coordinates) { + * // blend the two colors together and return the resulting color + * return mix(inputColorA, inputColorB, 0.5); + * }</pre> + * + * <p>After creating a {@link RuntimeShader} with that program the uniforms can + * then be initialized and updated per frame by calling + * {@link RuntimeShader#setColorUniform(String, int)}, + * {@link RuntimeShader#setColorUniform(String, long)}, or + * {@link RuntimeShader#setColorUniform(String, Color)} with the desired colors. The value of a + * color uniform is undefined if it is declared in the AGSL shader but not initialized.</p> + * + * <h4>Shader Uniforms</h4> + * In GLSL, a fragment shader can sample a texture. For AGSL instead of sampling textures you can + * sample from any {@link Shader}, which includes but is not limited to {@link BitmapShader}. To + * make it clear that you are operating on an {@link Shader} object there is no "sample" + * method. Instead, the shader uniform has an "eval()" method. This distinction enables AGSL shaders + * to sample from existing bitmap and gradient shaders as well as other {@link RuntimeShader} + * objects. In AGSL, declare the uniform like this: + * + * <pre class="prettyprint"> + * uniform shader myShader; + * vec4 main(vec2 canvas_coordinates) { + * // swap the red and blue color channels when sampling from myShader + * return myShader.sample(canvas_coordinates).bgra; + * }</pre> + * + * <p>After creating a {@link RuntimeShader} with that program the shader uniform can + * then be initialized and updated per frame by calling + * {@link RuntimeShader#setInputShader(String, Shader)} with the desired shader. The value of a + * shader uniform is undefined if it is declared in the AGSL shader but not initialized.</p> + * + * <p>Although most {@link BitmapShader}s contain colors that should be color managed, some contain + * data that isn’t actually colors. This includes bitmaps storing normals, material properties + * (e.g. roughness), heightmaps, or any other purely mathematical data that happens to be stored in + * a bitmap. When using these kinds of shaders in AGSL, you probably want to initialize them with + * {@link #setInputBuffer(String, BitmapShader)}. Shaders initialized this way work much like + * a regular {@link BitmapShader} (including filtering and tiling), with a few major differences: + * <ul> + * <li>No color space transformation is applied (the color space of the bitmap is ignored).</li> + * <li>Bitmaps that return false for {@link Bitmap#isPremultiplied()} are not automatically + * premultiplied.</li> + * </ul> + * + * <p>In addition, when sampling from a {@link BitmapShader} be aware that the shader does not use + * normalized coordinates (like a texture in GLSL). It uses (0, 0) in the upper-left corner, and + * (width, height) in the bottom-right corner. Normally, this is exactly what you want. If you’re + * evaluating the shader with coordinates based on the ones passed to your AGSL program, the scale + * is correct. However, if you want to adjust those coordinates (to do some kind of re-mapping of + * the bitmap), remember that the coordinates are local to the canvas.</p> + * */ public class RuntimeShader extends Shader { diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java index 358104fffbf6..e5d127609b2e 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -83,16 +83,10 @@ public class AndroidKeyStoreProvider extends Provider { // java.security.KeyPairGenerator put("KeyPairGenerator.EC", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$EC"); put("KeyPairGenerator.RSA", PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); - put("KeyPairGenerator." + X25519_ALIAS, - PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); - put("KeyPairGenerator." + ED25519_OID, - PACKAGE_NAME + ".AndroidKeyStoreKeyPairGeneratorSpi$RSA"); // java.security.KeyFactory putKeyFactoryImpl("EC"); putKeyFactoryImpl("RSA"); - putKeyFactoryImpl(X25519_ALIAS); - putKeyFactoryImpl(ED25519_OID); // javax.crypto.KeyGenerator put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES"); 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/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index 79e624212f4b..3876533a922e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -173,8 +173,8 @@ public class BadgedImageView extends ConstraintLayout { } @Override - public void onDraw(Canvas canvas) { - super.onDraw(canvas); + public void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); if (!shouldDrawDot()) { return; 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/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/packages/CompanionDeviceManager/res/drawable/ic_apps.xml b/packages/CompanionDeviceManager/res/drawable/ic_apps.xml new file mode 100644 index 000000000000..f839b80d2e0c --- /dev/null +++ b/packages/CompanionDeviceManager/res/drawable/ic_apps.xml @@ -0,0 +1,34 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <group android:scaleX="5.142857" + android:scaleY="5.142857" + android:translateY="2.5714285"> + <path + android:pathData="M13.1235,1.01L5.1235,1C4.0235,1 3.1235,1.9 3.1235,3V17C3.1235,18.1 4.0235,19 5.1235,19H13.1235C14.2235,19 15.1235,18.1 15.1235,17V12.9765H13.1235V14H5.1235V6H13.1235V6.0034H15.1235V3C15.1235,1.9 14.2235,1.01 13.1235,1.01ZM13.1235,17H5.1235V16H13.1235V17ZM5.1235,4V3H13.1235V4H5.1235Z" + android:fillColor="#202124" + android:fillType="evenOdd"/> + <path + android:pathData="M10.6984,6.4016V13.3136L12.3596,12.3628H17.8731V6.4016H10.6984ZM16.3731,7.9016H12.1984V10.8628H16.3731V7.9016Z" + android:fillColor="#202124" + android:fillType="evenOdd"/> + </group> +</vector>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/drawable/ic_notifications.xml b/packages/CompanionDeviceManager/res/drawable/ic_notifications.xml new file mode 100644 index 000000000000..4ac4d04b184e --- /dev/null +++ b/packages/CompanionDeviceManager/res/drawable/ic_notifications.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/white" + android:pathData="M4,19V17H6V10Q6,7.925 7.25,6.312Q8.5,4.7 10.5,4.2V3.5Q10.5,2.875 10.938,2.438Q11.375,2 12,2Q12.625,2 13.062,2.438Q13.5,2.875 13.5,3.5V4.2Q15.5,4.7 16.75,6.312Q18,7.925 18,10V17H20V19ZM12,11.5Q12,11.5 12,11.5Q12,11.5 12,11.5Q12,11.5 12,11.5Q12,11.5 12,11.5ZM12,22Q11.175,22 10.588,21.413Q10,20.825 10,20H14Q14,20.825 13.413,21.413Q12.825,22 12,22ZM8,17H16V10Q16,8.35 14.825,7.175Q13.65,6 12,6Q10.35,6 9.175,7.175Q8,8.35 8,10Z"/> +</vector>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/drawable/ic_storage.xml b/packages/CompanionDeviceManager/res/drawable/ic_storage.xml new file mode 100644 index 000000000000..d8b7f59185c8 --- /dev/null +++ b/packages/CompanionDeviceManager/res/drawable/ic_storage.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path android:fillColor="@android:color/white" + android:pathData="M6,17H18L14.25,12L11.25,16L9,13ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19ZM5,5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Q5,19 5,19Q5,19 5,19V5Q5,5 5,5Q5,5 5,5Z"/> +</vector>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml index f30dadffa788..82fe72bee4e4 100644 --- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml +++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml @@ -51,6 +51,11 @@ android:scrollbars="vertical" android:layout_height="200dp" /> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/permission_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </RelativeLayout> <LinearLayout diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml new file mode 100644 index 000000000000..b8a0f7938f0b --- /dev/null +++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml @@ -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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/list_item_permission" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="5dp"> + + <ImageView + android:id="@+id/permission_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginTop="7dp" + android:layout_marginEnd="12dp" + android:contentDescription="Permission Icon"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center_vertical" + android:padding="6dp"> + + <TextView + android:id="@+id/permission_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="16sp" + android:textAppearance="?android:attr/textAppearanceListItemSmall"/> + + <TextView + android:id="@+id/permission_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="14sp" + android:textColor="?android:attr/textColorSecondary"/> + + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml index 9626751679e1..c4ad432b092e 100644 --- a/packages/CompanionDeviceManager/res/values/strings.xml +++ b/packages/CompanionDeviceManager/res/values/strings.xml @@ -38,6 +38,12 @@ <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= --> + <!-- Apps permission will be granted of APP_STREAMING profile [CHAR LIMIT=30] --> + <string name="permission_apps">Apps</string> + + <!-- Description of apps permission of APP_STREAMING profile [CHAR LIMIT=NONE] --> + <string name="permission_apps_summary">Stream your phone\u2019s apps</string> + <!-- Confirmation for associating an application with a companion device of APP_STREAMING profile (type) [CHAR LIMIT=NONE] --> <string name="title_app_streaming">Allow <strong><xliff:g id="app_name" example="Exo">%1$s</xliff:g></strong> to access this information for your phone</string> @@ -72,6 +78,18 @@ <!-- Description of the privileges the application will get if associated with the companion device of COMPUTER profile (type) [CHAR LIMIT=NONE] --> <string name="summary_computer"></string> + <!-- Notification permission will be granted of COMPUTER profile [CHAR LIMIT=30] --> + <string name="permission_notification">Notifications</string> + + <!-- Description of notification permission of COMPUTER profile [CHAR LIMIT=NONE] --> + <string name="permission_notification_summary">Can read all notifications, including information like contracts, messages, and photos</string> + + <!-- Storage permission will be granted of COMPUTER profile [CHAR LIMIT=30] --> + <string name="permission_storage">Photos and media</string> + + <!-- Description of storage permission of COMPUTER profile [CHAR LIMIT=NONE] --> + <string name="permission_storage_summary"></string> + <!-- Title of the helper dialog for COMPUTER profile [CHAR LIMIT=30]. --> <string name="helper_title_computer">Google Play services</string> diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java index 0fba250b200a..1b626911fdde 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java @@ -24,6 +24,9 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState; import static com.android.companiondevicemanager.CompanionDeviceDiscoveryService.DiscoveryState.FINISHED_TIMEOUT; +import static com.android.companiondevicemanager.PermissionListAdapter.TYPE_APPS; +import static com.android.companiondevicemanager.PermissionListAdapter.TYPE_NOTIFICATION; +import static com.android.companiondevicemanager.PermissionListAdapter.TYPE_STORAGE; import static com.android.companiondevicemanager.Utils.getApplicationLabel; import static com.android.companiondevicemanager.Utils.getHtmlFromResources; import static com.android.companiondevicemanager.Utils.getVendorHeaderIcon; @@ -61,6 +64,7 @@ import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; import java.util.List; /** @@ -122,8 +126,12 @@ public class CompanionDeviceActivity extends FragmentActivity implements // The recycler view is only shown for multiple-device regular association request, after // at least one matching device is found. - private @Nullable RecyclerView mRecyclerView; - private @Nullable DeviceListAdapter mAdapter; + private @Nullable RecyclerView mDeviceListRecyclerView; + private @Nullable DeviceListAdapter mDeviceAdapter; + + // The recycler view is only shown for selfManaged association request. + private @Nullable RecyclerView mPermissionListRecyclerView; + private @Nullable PermissionListAdapter mPermissionListAdapter; // The flag used to prevent double taps, that may lead to sending several requests for creating // an association to CDM. @@ -133,6 +141,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements // onActivityResult() after the association is created. private @Nullable DeviceFilterPair<?> mSelectedDevice; + private @Nullable List<Integer> mPermissionTypes; + @Override public void onCreate(Bundle savedInstanceState) { if (DEBUG) Log.d(TAG, "onCreate()"); @@ -242,8 +252,11 @@ public class CompanionDeviceActivity extends FragmentActivity implements mVendorHeaderName = findViewById(R.id.vendor_header_name); mVendorHeaderButton = findViewById(R.id.vendor_header_button); - mRecyclerView = findViewById(R.id.device_list); - mAdapter = new DeviceListAdapter(this, this::onListItemClick); + mDeviceListRecyclerView = findViewById(R.id.device_list); + mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick); + + mPermissionListRecyclerView = findViewById(R.id.permission_list); + mPermissionListAdapter = new PermissionListAdapter(this); mButtonAllow = findViewById(R.id.btn_positive); mButtonNotAllow = findViewById(R.id.btn_negative); @@ -359,7 +372,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements final Drawable vendorIcon; final CharSequence vendorName; final Spanned title; - final Spanned summary; + + mPermissionTypes = new ArrayList<>(); try { vendorIcon = getVendorHeaderIcon(this, packageName, userId); @@ -372,33 +386,36 @@ public class CompanionDeviceActivity extends FragmentActivity implements switch (deviceProfile) { case DEVICE_PROFILE_APP_STREAMING: - title = getHtmlFromResources(this, R.string.title_app_streaming, appLabel); - summary = getHtmlFromResources( - this, R.string.summary_app_streaming, appLabel, deviceName); + title = getHtmlFromResources(this, R.string.title_app_streaming, deviceName); + mPermissionTypes.add(TYPE_APPS); break; case DEVICE_PROFILE_AUTOMOTIVE_PROJECTION: - title = getHtmlFromResources(this, R.string.title_automotive_projection, appLabel); - summary = getHtmlFromResources( - this, R.string.summary_automotive_projection, appLabel, deviceName); + title = getHtmlFromResources( + this, R.string.title_automotive_projection, deviceName); break; case DEVICE_PROFILE_COMPUTER: - title = getHtmlFromResources(this, R.string.title_computer, appLabel); - summary = getHtmlFromResources( - this, R.string.summary_computer, appLabel, deviceName); + title = getHtmlFromResources(this, R.string.title_computer, deviceName); + mPermissionTypes.add(TYPE_NOTIFICATION); + mPermissionTypes.add(TYPE_STORAGE); break; default: throw new RuntimeException("Unsupported profile " + deviceProfile); } + mSummary.setVisibility(View.GONE); + + mPermissionListAdapter.setPermissionType(mPermissionTypes); + mPermissionListRecyclerView.setAdapter(mPermissionListAdapter); + mPermissionListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + mTitle.setText(title); - mSummary.setText(summary); mVendorHeaderImage.setImageDrawable(vendorIcon); mVendorHeaderName.setText(vendorName); - mRecyclerView.setVisibility(View.GONE); + mDeviceListRecyclerView.setVisibility(View.GONE); mVendorHeader.setVisibility(View.VISIBLE); } @@ -411,7 +428,8 @@ public class CompanionDeviceActivity extends FragmentActivity implements deviceFilterPairs -> updateSingleDeviceUi( deviceFilterPairs, deviceProfile, appLabel)); - mRecyclerView.setVisibility(View.GONE); + mPermissionListRecyclerView.setVisibility(View.GONE); + mDeviceListRecyclerView.setVisibility(View.GONE); } private void updateSingleDeviceUi(List<DeviceFilterPair<?>> deviceFilterPairs, @@ -460,15 +478,15 @@ public class CompanionDeviceActivity extends FragmentActivity implements mTitle.setText(title); mSummary.setText(summary); - mAdapter = new DeviceListAdapter(this, this::onListItemClick); + mDeviceAdapter = new DeviceListAdapter(this, this::onListItemClick); // TODO: hide the list and show a spinner until a first device matching device is found. - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); + mDeviceListRecyclerView.setAdapter(mDeviceAdapter); + mDeviceListRecyclerView.setLayoutManager(new LinearLayoutManager(this)); CompanionDeviceDiscoveryService.getScanResult().observe( /* lifecycleOwner */ this, - /* observer */ mAdapter); + /* observer */ mDeviceAdapter); // "Remove" consent button: users would need to click on the list item. mButtonAllow.setVisibility(View.GONE); @@ -477,14 +495,14 @@ public class CompanionDeviceActivity extends FragmentActivity implements private void onListItemClick(int position) { if (DEBUG) Log.d(TAG, "onListItemClick() " + position); - final DeviceFilterPair<?> selectedDevice = mAdapter.getItem(position); + final DeviceFilterPair<?> selectedDevice = mDeviceAdapter.getItem(position); if (mSelectedDevice != null) { if (DEBUG) Log.w(TAG, "Already selected."); return; } // Notify the adapter to highlight the selected item. - mAdapter.setSelectedPosition(position); + mDeviceAdapter.setSelectedPosition(position); mSelectedDevice = requireNonNull(selectedDevice); diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java index e5513b074865..8babd3ade1eb 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceListAdapter.java @@ -15,9 +15,10 @@ */ package com.android.companiondevicemanager; + +import static com.android.companiondevicemanager.Utils.getIcon; + import android.content.Context; -import android.graphics.Color; -import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -60,11 +61,11 @@ class DeviceListAdapter extends RecyclerView.Adapter<DeviceListAdapter.ViewHolde R.layout.list_item_device, parent, false); ViewHolder viewHolder = new ViewHolder(view); if (viewType == TYPE_WIFI) { - viewHolder.mImageView.setImageDrawable(getIcon( - com.android.internal.R.drawable.ic_wifi_signal_3)); + viewHolder.mImageView.setImageDrawable( + getIcon(mContext, com.android.internal.R.drawable.ic_wifi_signal_3)); } else { - viewHolder.mImageView.setImageDrawable(getIcon( - android.R.drawable.stat_sys_data_bluetooth)); + viewHolder.mImageView.setImageDrawable( + getIcon(mContext, android.R.drawable.stat_sys_data_bluetooth)); } return viewHolder; } @@ -115,12 +116,6 @@ class DeviceListAdapter extends RecyclerView.Adapter<DeviceListAdapter.ViewHolde return mDevices.get(position).getDevice() instanceof android.net.wifi.ScanResult; } - private Drawable getIcon(int resId) { - Drawable icon = mContext.getResources().getDrawable(resId, null); - icon.setTint(Color.DKGRAY); - return icon; - } - public interface OnItemClickListener { void onItemClick(int position); } diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java new file mode 100644 index 000000000000..895b729ea8c7 --- /dev/null +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java @@ -0,0 +1,128 @@ +/* + * 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.companiondevicemanager; + +import static com.android.companiondevicemanager.Utils.getHtmlFromResources; +import static com.android.companiondevicemanager.Utils.getIcon; + +import static java.util.Collections.unmodifiableMap; + +import android.content.Context; +import android.text.Spanned; +import android.util.ArrayMap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; +import java.util.Map; + +class PermissionListAdapter extends RecyclerView.Adapter<PermissionListAdapter.ViewHolder> { + private final Context mContext; + + private List<Integer> mPermissions; + + static final int TYPE_NOTIFICATION = 0; + static final int TYPE_STORAGE = 1; + static final int TYPE_APPS = 2; + + private static final Map<Integer, Integer> sTitleMap; + static { + final Map<Integer, Integer> map = new ArrayMap<>(); + map.put(TYPE_NOTIFICATION, R.string.permission_notification); + map.put(TYPE_STORAGE, R.string.permission_storage); + map.put(TYPE_APPS, R.string.permission_apps); + sTitleMap = unmodifiableMap(map); + } + + private static final Map<Integer, Integer> sSummaryMap; + static { + final Map<Integer, Integer> map = new ArrayMap<>(); + map.put(TYPE_NOTIFICATION, R.string.permission_notification_summary); + map.put(TYPE_STORAGE, R.string.permission_storage_summary); + map.put(TYPE_APPS, R.string.permission_apps_summary); + sSummaryMap = unmodifiableMap(map); + } + + private static final Map<Integer, Integer> sIconMap; + static { + final Map<Integer, Integer> map = new ArrayMap<>(); + map.put(TYPE_NOTIFICATION, R.drawable.ic_notifications); + map.put(TYPE_STORAGE, R.drawable.ic_storage); + map.put(TYPE_APPS, R.drawable.ic_apps); + sIconMap = unmodifiableMap(map); + } + + PermissionListAdapter(Context context) { + mContext = context; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate( + R.layout.list_item_permission, parent, false); + ViewHolder viewHolder = new ViewHolder(view); + viewHolder.mPermissionIcon.setImageDrawable(getIcon(mContext, sIconMap.get(viewType))); + + return viewHolder; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + int type = getItemViewType(position); + final Spanned title = getHtmlFromResources(mContext, sTitleMap.get(type)); + final Spanned summary = getHtmlFromResources(mContext, sSummaryMap.get(type)); + + holder.mPermissionName.setText(title); + holder.mPermissionSummary.setText(summary); + } + + @Override + public int getItemViewType(int position) { + return mPermissions.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return mPermissions != null ? mPermissions.size() : 0; + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView mPermissionName; + private final TextView mPermissionSummary; + private final ImageView mPermissionIcon; + ViewHolder(View itemView) { + super(itemView); + mPermissionName = itemView.findViewById(R.id.permission_name); + mPermissionSummary = itemView.findViewById(R.id.permission_summary); + mPermissionIcon = itemView.findViewById(R.id.permission_icon); + } + } + + void setPermissionType(List<Integer> permissions) { + mPermissions = permissions; + } +} diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java index 76bbcfb79155..d5b2f0a748f1 100644 --- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java +++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/Utils.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; @@ -121,6 +122,12 @@ class Utils { return appInfo; } + static @NonNull Drawable getIcon(@NonNull Context context, int resId) { + Drawable icon = context.getResources().getDrawable(resId, null); + icon.setTint(Color.DKGRAY); + return icon; + } + static void runOnMainThread(Runnable runnable) { if (Thread.currentThread() == Looper.getMainLooper().getThread()) { runnable.run(); 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-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/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/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index ebc766635733..dfbb0c7c1624 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -65,6 +65,9 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // A reference to the {@link Window} used to hold the dream overlay. private Window mWindow; + // True if the service has been destroyed. + private boolean mDestroyed; + private final Complication.Host mHost = new Complication.Host() { @Override public void requestExitDream() { @@ -134,6 +137,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mPreviewComplication.setDreamLabel(null); mStateController.removeComplication(mPreviewComplication); mStateController.setPreviewMode(false); + mDestroyed = true; super.onDestroy(); } @@ -141,6 +145,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) { setCurrentState(Lifecycle.State.STARTED); mExecutor.execute(() -> { + if (mDestroyed) { + // The task could still be executed after the service has been destroyed. Bail if + // that is the case. + return; + } mStateController.setShouldShowComplications(shouldShowComplications()); mStateController.setPreviewMode(isPreviewMode()); if (isPreviewMode()) { 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 81934a6b6a73..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 @@ -22,6 +22,8 @@ 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 @@ -53,6 +55,7 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( 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. */ @@ -95,6 +98,12 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>( 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. 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 3b94a1d9597e..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 @@ -22,6 +22,7 @@ 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 @@ -52,6 +53,7 @@ class MediaTttChipControllerReceiver @Inject constructor( viewUtil: ViewUtil, mainExecutor: DelayableExecutor, tapGestureDetector: TapGestureDetector, + powerManager: PowerManager, @Main private val mainHandler: Handler, private val uiEventLogger: MediaTttReceiverUiEventLogger, ) : MediaTttChipControllerCommon<ChipReceiverInfo>( @@ -61,6 +63,7 @@ class MediaTttChipControllerReceiver @Inject constructor( viewUtil, mainExecutor, tapGestureDetector, + powerManager, R.layout.media_ttt_chip_receiver ) { private val commandQueueCallbacks = object : CommandQueue.Callbacks { 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 3e4cf9921bad..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 @@ -51,6 +52,7 @@ class MediaTttChipControllerSender @Inject constructor( viewUtil: ViewUtil, @Main mainExecutor: DelayableExecutor, tapGestureDetector: TapGestureDetector, + powerManager: PowerManager, private val uiEventLogger: MediaTttSenderUiEventLogger ) : MediaTttChipControllerCommon<ChipSenderInfo>( context, @@ -59,6 +61,7 @@ class MediaTttChipControllerSender @Inject constructor( viewUtil, mainExecutor, tapGestureDetector, + powerManager, R.layout.media_ttt_chip ) { private var currentlyDisplayedChipState: ChipStateSender? = null 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/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 5d9361d201c1..e0d158cd7db6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -18,7 +18,10 @@ package com.android.systemui.qs import android.app.IActivityManager import android.app.IForegroundServiceObserver +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.os.IBinder @@ -42,6 +45,7 @@ import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.TASK_MANAGER_ import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -67,6 +71,7 @@ class FgsManagerController @Inject constructor( private val packageManager: PackageManager, private val deviceConfigProxy: DeviceConfigProxy, private val dialogLaunchAnimator: DialogLaunchAnimator, + private val broadcastDispatcher: BroadcastDispatcher, private val dumpManager: DumpManager ) : IForegroundServiceObserver.Stub(), Dumpable { @@ -125,6 +130,18 @@ class FgsManagerController @Inject constructor( dumpManager.registerDumpable(this) + broadcastDispatcher.registerReceiver( + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER) { + showDialog(null) + } + } + }, + IntentFilter(Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER), + executor = mainExecutor, + flags = Context.RECEIVER_NOT_EXPORTED) + initialized = true } } 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/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 41b070635d4f..0df2162d3338 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -72,9 +72,9 @@ class HeadsUpCoordinator @Inject constructor( private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null private lateinit var mNotifPipeline: NotifPipeline private var mNow: Long = -1 - // notifs we've extended the lifetime for private val mNotifsExtendingLifetime = ArraySet<NotificationEntry>() + private val mPostedEntries = LinkedHashMap<String, PostedEntry>() override fun attach(pipeline: NotifPipeline) { mNotifPipeline = pipeline @@ -101,10 +101,12 @@ class HeadsUpCoordinator @Inject constructor( return } // Process all non-group adds/updates - mPostedEntries.values.toList().forEach { posted -> - if (!posted.entry.sbn.isGroup) { - handlePostedEntry(posted, "non-group") - mPostedEntries.remove(posted.key) + mHeadsUpManager.modifyHuns { hunMutator -> + mPostedEntries.values.toList().forEach { posted -> + if (!posted.entry.sbn.isGroup) { + handlePostedEntry(posted, hunMutator, "non-group") + mPostedEntries.remove(posted.key) + } } } } @@ -114,10 +116,10 @@ class HeadsUpCoordinator @Inject constructor( * we know that stability and [NotifPromoter]s have been applied, so we can use the location of * notifications in this list to determine what kind of group alert behavior should happen. */ - fun onBeforeFinalizeFilter(list: List<ListEntry>) { + fun onBeforeFinalizeFilter(list: List<ListEntry>) = mHeadsUpManager.modifyHuns { hunMutator -> // Nothing to do if there are no other adds/updates if (mPostedEntries.isEmpty()) { - return + return@modifyHuns } // Calculate a bunch of information about the logical group and the locations of group // entries in the nearly-finalized shade list. These may be used in the per-group loop. @@ -138,13 +140,17 @@ class HeadsUpCoordinator @Inject constructor( // If there is no logical summary, then there is no alert to transfer if (logicalSummary == null) { - postedEntries.forEach { handlePostedEntry(it, "logical-summary-missing") } + postedEntries.forEach { + handlePostedEntry(it, hunMutator, scenario = "logical-summary-missing") + } return@forEach } // If summary isn't wanted to be heads up, then there is no alert to transfer if (!isGoingToShowHunStrict(logicalSummary)) { - postedEntries.forEach { handlePostedEntry(it, "logical-summary-not-alerting") } + postedEntries.forEach { + handlePostedEntry(it, hunMutator, scenario = "logical-summary-not-alerting") + } return@forEach } @@ -177,7 +183,9 @@ class HeadsUpCoordinator @Inject constructor( // If there is no child to receive the parent alert, then just handle the posted entries // and return. if (childToReceiveParentAlert == null) { - postedEntries.forEach { handlePostedEntry(it, "no-transfer-target") } + postedEntries.forEach { + handlePostedEntry(it, hunMutator, scenario = "no-transfer-target") + } return@forEach } @@ -189,51 +197,66 @@ class HeadsUpCoordinator @Inject constructor( if (!isSummaryAttached) { val summaryUpdateForRemoval = summaryUpdate?.also { it.shouldHeadsUpEver = false - } ?: PostedEntry(logicalSummary, - wasAdded = false, - wasUpdated = false, - shouldHeadsUpEver = false, - shouldHeadsUpAgain = false, - isAlerting = mHeadsUpManager.isAlerting(logicalSummary.key), - isBinding = isEntryBinding(logicalSummary), + } ?: PostedEntry( + logicalSummary, + wasAdded = false, + wasUpdated = false, + shouldHeadsUpEver = false, + shouldHeadsUpAgain = false, + isAlerting = mHeadsUpManager.isAlerting(logicalSummary.key), + isBinding = isEntryBinding(logicalSummary), ) // If we transfer the alert and the summary isn't even attached, that means we // should ensure the summary is no longer alerting, so we remove it here. - handlePostedEntry(summaryUpdateForRemoval, "detached-summary-remove-alert") - } else if (summaryUpdate!=null) { - mLogger.logPostedEntryWillNotEvaluate(summaryUpdate, "attached-summary-transferred") + handlePostedEntry( + summaryUpdateForRemoval, + hunMutator, + scenario = "detached-summary-remove-alert") + } else if (summaryUpdate != null) { + mLogger.logPostedEntryWillNotEvaluate( + summaryUpdate, + reason = "attached-summary-transferred") } // Handle all posted entries -- if the child receiving the parent's alert is in the // list, then set its flags to ensure it alerts. var didAlertChildToReceiveParentAlert = false postedEntries.asSequence() - .filter { it.key != logicalSummary.key } - .forEach { postedEntry -> - if (childToReceiveParentAlert.key == postedEntry.key) { - // Update the child's posted update so that it - postedEntry.shouldHeadsUpEver = true - postedEntry.shouldHeadsUpAgain = true - handlePostedEntry(postedEntry, "child-alert-transfer-target-$targetType") - didAlertChildToReceiveParentAlert = true - } else { - handlePostedEntry(postedEntry, "child-alert-non-target") + .filter { it.key != logicalSummary.key } + .forEach { postedEntry -> + if (childToReceiveParentAlert.key == postedEntry.key) { + // Update the child's posted update so that it + postedEntry.shouldHeadsUpEver = true + postedEntry.shouldHeadsUpAgain = true + handlePostedEntry( + postedEntry, + hunMutator, + scenario = "child-alert-transfer-target-$targetType") + didAlertChildToReceiveParentAlert = true + } else { + handlePostedEntry( + postedEntry, + hunMutator, + scenario = "child-alert-non-target") + } } - } // If the child receiving the alert was not updated on this tick (which can happen in a // standard alert transfer scenario), then construct an update so that we can apply it. if (!didAlertChildToReceiveParentAlert) { val posted = PostedEntry( - childToReceiveParentAlert, - wasAdded = false, - wasUpdated = false, - shouldHeadsUpEver = true, - shouldHeadsUpAgain = true, - isAlerting = mHeadsUpManager.isAlerting(childToReceiveParentAlert.key), - isBinding = isEntryBinding(childToReceiveParentAlert), + childToReceiveParentAlert, + wasAdded = false, + wasUpdated = false, + shouldHeadsUpEver = true, + shouldHeadsUpAgain = true, + isAlerting = mHeadsUpManager.isAlerting(childToReceiveParentAlert.key), + isBinding = isEntryBinding(childToReceiveParentAlert), ) - handlePostedEntry(posted, "non-posted-child-alert-transfer-target-$targetType") + handlePostedEntry( + posted, + hunMutator, + scenario = "non-posted-child-alert-transfer-target-$targetType") } } // After this method runs, all posted entries should have been handled (or skipped). @@ -292,9 +315,7 @@ class HeadsUpCoordinator @Inject constructor( } } - private val mPostedEntries = LinkedHashMap<String, PostedEntry>() - - fun handlePostedEntry(posted: PostedEntry, scenario: String) { + private fun handlePostedEntry(posted: PostedEntry, hunMutator: HunMutator, scenario: String) { mLogger.logPostedEntryWillEvaluate(posted, scenario) if (posted.wasAdded) { if (posted.shouldHeadsUpEver) { @@ -308,12 +329,12 @@ class HeadsUpCoordinator @Inject constructor( // If alerting, we need to post an update. Otherwise we're still binding, // and we can just let that finish. if (posted.isAlerting) { - mHeadsUpManager.updateNotification(posted.key, posted.shouldHeadsUpAgain) + hunMutator.updateNotification(posted.key, posted.shouldHeadsUpAgain) } } else { if (posted.isAlerting) { // We don't want this to be interrupting anymore, let's remove it - mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/) + hunMutator.removeNotification(posted.key, false /*removeImmediately*/) } else { // Don't let the bind finish cancelHeadsUpBind(posted.entry) @@ -366,7 +387,7 @@ class HeadsUpCoordinator @Inject constructor( val shouldHeadsUpAgain = shouldHunAgain(entry) val isAlerting = mHeadsUpManager.isAlerting(entry.key) val isBinding = isEntryBinding(entry) - mPostedEntries.compute(entry.key) { _, value -> + val posted = mPostedEntries.compute(entry.key) { _, value -> value?.also { update -> update.wasUpdated = true update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver @@ -383,6 +404,18 @@ class HeadsUpCoordinator @Inject constructor( isBinding = isBinding, ) } + // Handle cancelling alerts here, rather than in the OnBeforeFinalizeFilter, so that + // work can be done before the ShadeListBuilder is run. This prevents re-entrant + // behavior between this Coordinator, HeadsUpManager, and VisualStabilityManager. + if (posted?.shouldHeadsUpEver == false) { + if (posted.isAlerting) { + // We don't want this to be interrupting anymore, let's remove it + mHeadsUpManager.removeNotification(posted.key, false /*removeImmediately*/) + } else if (posted.isBinding) { + // Don't let the bind finish + cancelHeadsUpBind(posted.entry) + } + } } /** @@ -543,3 +576,43 @@ private enum class GroupLocation { Detached, Isolated, Summary, Child } private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation = getOrDefault(key, GroupLocation.Detached) + +/** + * Invokes the given block with a [HunMutator] that defers all HUN removals. This ensures that the + * HeadsUpManager is notified of additions before removals, which prevents a glitch where the + * HeadsUpManager temporarily believes that nothing is alerting, causing bad re-entrant behavior. + */ +private fun <R> HeadsUpManager.modifyHuns(block: (HunMutator) -> R): R { + val mutator = HunMutatorImpl(this) + return block(mutator).also { mutator.commitModifications() } +} + +/** Mutates the HeadsUp state of notifications. */ +private interface HunMutator { + fun updateNotification(key: String, alert: Boolean) + fun removeNotification(key: String, releaseImmediately: Boolean) +} + +/** + * [HunMutator] implementation that defers removing notifications from the HeadsUpManager until + * after additions/updates. + */ +private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator { + private val deferred = mutableListOf<Pair<String, Boolean>>() + + override fun updateNotification(key: String, alert: Boolean) { + headsUpManager.updateNotification(key, alert) + } + + override fun removeNotification(key: String, releaseImmediately: Boolean) { + val args = Pair(key, releaseImmediately) + deferred.add(args) + } + + fun commitModifications() { + deferred.forEach { (key, releaseImmediately) -> + headsUpManager.removeNotification(key, releaseImmediately) + } + deferred.clear() + } +} 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/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 35fda1392512..7d7ccb462b3e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -19,6 +19,7 @@ package com.android.systemui.dreams; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -222,4 +223,25 @@ public class DreamOverlayServiceTest extends SysuiTestCase { verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED); verify(mStateController).setOverlayActive(false); } + + @Test + public void testDecorViewNotAddedToWindowAfterDestroy() throws Exception { + when(mDreamOverlayContainerView.getParent()) + .thenReturn(mDreamOverlayContainerViewParent) + .thenReturn(null); + + final IBinder proxy = mService.onBind(new Intent()); + final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy); + + // Inform the overlay service of dream starting. + overlay.startDream(mWindowParams, mDreamOverlayCallback); + + // Destroy the service. + mService.onDestroy(); + + // Run executor tasks. + mMainExecutor.runAllReady(); + + verify(mWindowManager, never()).addView(any(), any()); + } } 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 f99efe25c6eb..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 @@ -20,7 +20,7 @@ import android.content.Context 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.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -30,7 +30,6 @@ import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.media.taptotransfer.receiver.ChipReceiverInfo import com.android.systemui.statusbar.gesture.TapGestureDetector import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.FakeExecutor @@ -71,6 +70,8 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { private lateinit var viewUtil: ViewUtil @Mock private lateinit var tapGestureDetector: TapGestureDetector + @Mock + private lateinit var powerManager: PowerManager @Before fun setUp() { @@ -88,16 +89,17 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { 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 @@ -281,6 +283,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { viewUtil: ViewUtil, @Main mainExecutor: DelayableExecutor, tapGestureDetector: TapGestureDetector, + powerManager: PowerManager ) : MediaTttChipControllerCommon<ChipInfo>( context, logger, @@ -288,6 +291,7 @@ class MediaTttChipControllerCommonTest : SysuiTestCase() { viewUtil, mainExecutor, tapGestureDetector, + powerManager, R.layout.media_ttt_chip ) { override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) { 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 36b5761ca690..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 @@ -64,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 @@ -97,6 +100,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { viewUtil, FakeExecutor(FakeSystemClock()), TapGestureDetector(context), + powerManager, Handler.getMain(), receiverUiEventLogger, ) 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 4ac7709071c8..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 @@ -66,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 @@ -103,6 +106,7 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { viewUtil, fakeExecutor, TapGestureDetector(context), + powerManager, senderUiEventLogger ) 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/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 47f9fd535801..7054a8171572 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -16999,6 +16999,15 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public void addPendingTopUid(int uid, int pid) { mPendingStartActivityUids.add(uid, pid); + + // If the next top activity is in cached and frozen mode, WM should raise its priority + // to unfreeze it. This is done by calling AMS.updateOomAdj that will lower its oom adj. + // However, WM cannot hold the AMS clock here so the updateOomAdj operation is performed + // in a separate thread asynchronously. Therefore WM can't guarantee AMS will unfreeze + // next top activity on time. This race will fail the following binder transactions WM + // sends to the activity. After this race issue between WM/ATMS and AMS is solved, this + // workaround can be removed. (b/213288355) + mOomAdjuster.mCachedAppOptimizer.unfreezeProcess(pid); } @Override diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index b18d390476f0..0c3b984aed73 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -183,6 +183,8 @@ public final class CachedAppOptimizer { private final ActivityManagerGlobalLock mProcLock; + private final Object mFreezerLock = new Object(); + private final OnPropertiesChangedListener mOnFlagsChangedListener = new OnPropertiesChangedListener() { @Override @@ -949,8 +951,8 @@ public final class CachedAppOptimizer { } } - @GuardedBy({"mAm", "mProcLock"}) - void unfreezeAppLSP(ProcessRecord app) { + @GuardedBy({"mAm", "mProcLock", "mFreezerLock"}) + void unfreezeAppInternalLSP(ProcessRecord app) { final int pid = app.getPid(); final ProcessCachedOptimizerRecord opt = app.mOptRecord; if (opt.isPendingFreeze()) { @@ -1035,6 +1037,42 @@ public final class CachedAppOptimizer { } } + @GuardedBy({"mAm", "mProcLock"}) + void unfreezeAppLSP(ProcessRecord app) { + synchronized (mFreezerLock) { + unfreezeAppInternalLSP(app); + } + } + + /** + * This quick function works around the race condition between WM/ATMS and AMS, allowing + * the former to directly unfreeze a frozen process before the latter runs updateOomAdj. + * After the race issue is solved, this workaround can be removed. (b/213288355) + * @param pid pid of the process to be unfrozen + */ + @GuardedBy({"mFreezerLock"}) + void unfreezeProcess(int pid) { + synchronized (mFreezerLock) { + ProcessRecord app = mFrozenProcesses.get(pid); + if (app == null) { + return; + } + Slog.i(TAG_AM, "quick sync unfreeze " + pid); + try { + freezeBinder(pid, false); + } catch (RuntimeException e) { + Slog.e(TAG_AM, "Unable to quick unfreeze binder for " + pid); + return; + } + + try { + Process.setProcessFrozen(pid, app.uid, false); + } catch (Exception e) { + Slog.e(TAG_AM, "Unable to quick unfreeze " + pid); + } + } + } + /** * To be called when the given app is killed. */ @@ -1464,8 +1502,6 @@ public final class CachedAppOptimizer { return; } - Slog.d(TAG_AM, "froze " + pid + " " + name); - EventLog.writeEvent(EventLogTags.AM_FREEZE, pid, name); // See above for why we're not taking mPhenotypeFlagLock here 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/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..1f9d08063714 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; @@ -335,6 +336,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private final HandwritingModeController mHwController; + @GuardedBy("ImfLock.class") + @Nullable + private OverlayableSystemBooleanResourceWrapper mImeDrawsImeNavBarRes; + static class SessionState { final ClientState client; final IInputMethodInvoker method; @@ -1723,11 +1728,43 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") + private void recreateImeDrawsImeNavBarResIfNecessary(@UserIdInt int targetUserId) { + // Currently, com.android.internal.R.bool.config_imeDrawsImeNavBar is overlaid only for the + // profile parent user. + // TODO(b/221443458): See if we can make OverlayManager be aware of profile groups. + final int profileParentUserId = mUserManagerInternal.getProfileParentId(targetUserId); + if (mImeDrawsImeNavBarRes != null + && mImeDrawsImeNavBarRes.getUserId() != profileParentUserId) { + mImeDrawsImeNavBarRes.close(); + mImeDrawsImeNavBarRes = null; + } + if (mImeDrawsImeNavBarRes == null) { + final Context userContext; + if (mContext.getUserId() == profileParentUserId) { + userContext = mContext; + } else { + userContext = mContext.createContextAsUser(UserHandle.of(profileParentUserId), + 0 /* flags */); + } + mImeDrawsImeNavBarRes = OverlayableSystemBooleanResourceWrapper.create(userContext, + com.android.internal.R.bool.config_imeDrawsImeNavBar, mHandler, resource -> { + synchronized (ImfLock.class) { + if (resource == mImeDrawsImeNavBarRes) { + sendOnNavButtonFlagsChangedLocked(); + } + } + }); + } + } + + @GuardedBy("ImfLock.class") private void switchUserOnHandlerLocked(@UserIdInt int newUserId, IInputMethodClient clientToBeReset) { if (DEBUG) Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId + " currentUserId=" + mSettings.getCurrentUserId()); + recreateImeDrawsImeNavBarResIfNecessary(newUserId); + // ContentObserver should be registered again when the user is changed mSettingsObserver.registerContentObserverLocked(newUserId); @@ -1832,6 +1869,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub }); } + recreateImeDrawsImeNavBarResIfNecessary(currentUserId); + mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true); mSettingsObserver.registerContentObserverLocked(currentUserId); @@ -2412,12 +2451,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 +2720,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 +2977,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - boolean shouldShowImeSwitcherWhenImeIsShownLocked() { - return shouldShowImeSwitcherLocked( + @InputMethodNavButtonFlags + private int getInputMethodNavButtonFlagsLocked() { + final boolean canImeDrawsImeNavBar = + mImeDrawsImeNavBarRes != null && mImeDrawsImeNavBarRes.get(); + final boolean shouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherLocked( InputMethodService.IME_ACTIVE | InputMethodService.IME_VISIBLE); + return (canImeDrawsImeNavBar ? InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR : 0) + | (shouldShowImeSwitcherWhenImeIsShown + ? InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0); } @GuardedBy("ImfLock.class") @@ -3203,7 +3248,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the same enabled IMEs list. mSwitchingController.resetCircularListLocked(mContext); - sendShouldShowImeSwitcherWhenImeIsShownLocked(); + sendOnNavButtonFlagsChangedLocked(); } @GuardedBy("ImfLock.class") @@ -4680,7 +4725,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 +4995,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 +5004,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/inputmethod/OverlayableSystemBooleanResourceWrapper.java b/services/core/java/com/android/server/inputmethod/OverlayableSystemBooleanResourceWrapper.java new file mode 100644 index 000000000000..33e7a7621340 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/OverlayableSystemBooleanResourceWrapper.java @@ -0,0 +1,159 @@ +/* + * 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.inputmethod; + +import static android.content.Intent.ACTION_OVERLAY_CHANGED; + +import android.annotation.AnyThread; +import android.annotation.BoolRes; +import android.annotation.NonNull; +import android.annotation.UserHandleAware; +import android.annotation.UserIdInt; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.PatternMatcher; +import android.util.Slog; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * A wrapper object for any boolean resource defined in {@code "android"} package, in a way that is + * aware of per-user Runtime Resource Overlay (RRO). + */ +final class OverlayableSystemBooleanResourceWrapper implements AutoCloseable { + private static final String TAG = "OverlayableSystemBooleanResourceWrapper"; + + private static final String SYSTEM_PACKAGE_NAME = "android"; + + @UserIdInt + private final int mUserId; + @NonNull + private final AtomicBoolean mValueRef; + @NonNull + private final AtomicReference<Runnable> mCleanerRef; + + /** + * Creates {@link OverlayableSystemBooleanResourceWrapper} for the given boolean resource ID + * with a value change callback for the user associated with the {@link Context}. + * + * @param userContext The {@link Context} to be used to access the resource. This needs to be + * associated with the right user because the Runtime Resource Overlay (RRO) + * is per-user configuration. + * @param boolResId The resource ID to be queried. + * @param handler {@link Handler} to be used to dispatch {@code callback}. + * @param callback The callback to be notified when the specified value might be updated. + * The callback needs to take care of spurious wakeup. The value returned from + * {@link #get()} may look to be exactly the same as the previously read value + * e.g. when the value is changed from {@code false} to {@code true} to + * {@code false} in a very short period of time, because {@link #get()} always + * does volatile-read. + * @return New {@link OverlayableSystemBooleanResourceWrapper}. + */ + @NonNull + @UserHandleAware + static OverlayableSystemBooleanResourceWrapper create(@NonNull Context userContext, + @BoolRes int boolResId, @NonNull Handler handler, + @NonNull Consumer<OverlayableSystemBooleanResourceWrapper> callback) { + + // Note that we cannot fully trust this initial value due to the dead time between obtaining + // the value here and setting up a broadcast receiver for change callback below. + // We will refresh the value again later after setting up the change callback anyway. + final AtomicBoolean valueRef = new AtomicBoolean(evaluate(userContext, boolResId)); + + final AtomicReference<Runnable> cleanerRef = new AtomicReference<>(); + + final OverlayableSystemBooleanResourceWrapper object = + new OverlayableSystemBooleanResourceWrapper(userContext.getUserId(), valueRef, + cleanerRef); + + final IntentFilter intentFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); + intentFilter.addDataScheme(IntentFilter.SCHEME_PACKAGE); + intentFilter.addDataSchemeSpecificPart(SYSTEM_PACKAGE_NAME, PatternMatcher.PATTERN_LITERAL); + + final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final boolean newValue = evaluate(userContext, boolResId); + if (newValue != valueRef.getAndSet(newValue)) { + callback.accept(object); + } + } + }; + userContext.registerReceiver(broadcastReceiver, intentFilter, + null /* broadcastPermission */, handler, + Context.RECEIVER_NOT_EXPORTED); + cleanerRef.set(() -> userContext.unregisterReceiver(broadcastReceiver)); + + // Make sure that the initial observable value is obtained after the change callback is set. + valueRef.set(evaluate(userContext, boolResId)); + return object; + } + + private OverlayableSystemBooleanResourceWrapper(@UserIdInt int userId, + @NonNull AtomicBoolean valueRef, @NonNull AtomicReference<Runnable> cleanerRef) { + mUserId = userId; + mValueRef = valueRef; + mCleanerRef = cleanerRef; + } + + /** + * @return The boolean resource value. + */ + @AnyThread + boolean get() { + return mValueRef.get(); + } + + /** + * @return The user ID associated with this resource reader. + */ + @AnyThread + @UserIdInt + int getUserId() { + return mUserId; + } + + @AnyThread + private static boolean evaluate(@NonNull Context context, @BoolRes int boolResId) { + try { + return context.getPackageManager() + .getResourcesForApplication(SYSTEM_PACKAGE_NAME) + .getBoolean(boolResId); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "getResourcesForApplication(\"" + SYSTEM_PACKAGE_NAME + "\") failed", e); + return false; + } + } + + /** + * Cleans up the callback. + */ + @AnyThread + @Override + public void close() { + final Runnable cleaner = mCleanerRef.getAndSet(null); + if (cleaner != null) { + cleaner.run(); + } + } +} 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/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java index 6b9e374771ff..e551f1056b24 100644 --- a/services/core/java/com/android/server/notification/PermissionHelper.java +++ b/services/core/java/com/android/server/notification/PermissionHelper.java @@ -35,6 +35,7 @@ import android.util.ArrayMap; import android.util.Pair; import android.util.Slog; +import com.android.internal.util.ArrayUtils; import com.android.server.pm.permission.PermissionManagerServiceInternal; import java.util.Collections; @@ -178,13 +179,16 @@ public final class PermissionHelper { boolean userSet, boolean reviewRequired) { assertFlag(); final long callingId = Binder.clearCallingIdentity(); - // Do not change fixed permissions, and do not change non-user set permissions that are - // granted by default, or granted by role. - if (isPermissionFixed(packageName, userId) - || (isPermissionGrantedByDefaultOrRole(packageName, userId) && !userSet)) { - return; - } try { + // Do not change the permission if the package doesn't request it, do not change fixed + // permissions, and do not change non-user set permissions that are granted by default, + // or granted by role. + if (!packageRequestsNotificationPermission(packageName, userId) + || isPermissionFixed(packageName, userId) + || (isPermissionGrantedByDefaultOrRole(packageName, userId) && !userSet)) { + return; + } + boolean currentlyGranted = mPmi.checkPermission(packageName, NOTIFICATION_PERMISSION, userId) != PackageManager.PERMISSION_DENIED; if (grant && !reviewRequired && !currentlyGranted) { @@ -278,6 +282,19 @@ public final class PermissionHelper { } } + private boolean packageRequestsNotificationPermission(String packageName, + @UserIdInt int userId) { + assertFlag(); + try { + String[] permissions = mPackageManager.getPackageInfo(packageName, GET_PERMISSIONS, + userId).requestedPermissions; + return ArrayUtils.contains(permissions, NOTIFICATION_PERMISSION); + } catch (RemoteException e) { + Slog.e(TAG, "Could not reach system server", e); + } + return false; + } + private void assertFlag() { if (!mMigrationEnabled) { throw new IllegalStateException("Method called without checking flag value"); 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/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/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 9961ae51b92d..90842619c31e 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -1403,7 +1403,7 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } } else { if (ps.getUserStateOrDefault(userId).isInstantApp() && !bp.isInstant()) { - throw new SecurityException("Cannot grant non-ephemeral permission" + permName + throw new SecurityException("Cannot grant non-ephemeral permission " + permName + " for package " + packageName); } @@ -3532,8 +3532,9 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt final Boolean granted = SystemConfig.getInstance().getOemPermissions(pkg.getPackageName()).get(permission); if (granted == null) { - throw new IllegalStateException("OEM permission" + permission + " requested by package " - + pkg.getPackageName() + " must be explicitly declared granted or not"); + throw new IllegalStateException("OEM permission " + permission + + " requested by package " + pkg.getPackageName() + + " must be explicitly declared granted or not"); } return Boolean.TRUE == granted; } 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..70ef3d364277 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; @@ -54,6 +56,8 @@ import android.content.pm.PackageManagerInternal.PackageListObserver; import android.content.pm.PermissionInfo; import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -109,6 +113,7 @@ public final class PermissionPolicyService extends SystemService { private static final String SYSTEM_PKG = "android"; private static final boolean DEBUG = false; private static final long USER_SENSITIVE_UPDATE_DELAY_MS = 60000; + private static final long ACTIVITY_START_DELAY_MS = 200; private final Object mLock = new Object(); @@ -149,6 +154,7 @@ public final class PermissionPolicyService extends SystemService { private List<String> mAppOpPermissions; private Context mContext; + private Handler mHandler; private PackageManagerInternal mPackageManagerInternal; private NotificationManagerInternal mNotificationManager; private final KeyguardManager mKeyguardManager; @@ -158,6 +164,7 @@ public final class PermissionPolicyService extends SystemService { super(context); mContext = context; + mHandler = new Handler(Looper.getMainLooper()); mPackageManager = context.getPackageManager(); mKeyguardManager = context.getSystemService(KeyguardManager.class); LocalServices.addService(PermissionPolicyInternal.class, new Internal()); @@ -1016,7 +1023,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 +1040,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 +1059,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 +1092,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 +1111,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,14 +1181,15 @@ 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()); options.setTaskOverlay(true, false); options.setLaunchTaskId(taskId); try { - mContext.startActivityAsUser(grantPermission, options.toBundle(), user); + mHandler.postDelayed(() -> mContext.startActivityAsUser( + grantPermission, options.toBundle(), user), ACTIVITY_START_DELAY_MS); } catch (Exception e) { Log.e(LOG_TAG, "couldn't start grant permission dialog" + "for other package " + pkgName, e); @@ -1170,12 +1208,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/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/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/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/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 f65b17db1e10..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; @@ -3332,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/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/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/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/PermissionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java index a24ba0d1e1c2..50151bfb7191 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java @@ -89,6 +89,10 @@ public class PermissionHelperTest extends UiServiceTestCase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, true); + PackageInfo testPkgInfo = new PackageInfo(); + testPkgInfo.requestedPermissions = new String[]{ Manifest.permission.POST_NOTIFICATIONS }; + when(mPackageManager.getPackageInfo(anyString(), anyLong(), anyInt())) + .thenReturn(testPkgInfo); } // TODO (b/194833441): Remove when the migration is enabled @@ -384,6 +388,22 @@ public class PermissionHelperTest extends UiServiceTestCase { } @Test + public void testSetNotificationPermission_doesntRequestNotChanged() throws Exception { + when(mPmi.checkPermission(anyString(), anyString(), anyInt())) + .thenReturn(PERMISSION_GRANTED); + PackageInfo testPkgInfo = new PackageInfo(); + testPkgInfo.requestedPermissions = new String[]{ Manifest.permission.RECORD_AUDIO }; + when(mPackageManager.getPackageInfo(anyString(), anyLong(), anyInt())) + .thenReturn(testPkgInfo); + mPermissionHelper.setNotificationPermission("pkg", 10, false, false); + + verify(mPmi, never()).checkPermission( + eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10)); + verify(mPermManager, never()).revokeRuntimePermission( + eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString()); + } + + @Test public void testIsPermissionFixed() throws Exception { when(mPermManager.getPermissionFlags(anyString(), eq(Manifest.permission.POST_NOTIFICATIONS), 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/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/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); diff --git a/telephony/java/android/telephony/data/DataServiceCallback.java b/telephony/java/android/telephony/data/DataServiceCallback.java index ec734716f6e4..b8c5b4e3628a 100644 --- a/telephony/java/android/telephony/data/DataServiceCallback.java +++ b/telephony/java/android/telephony/data/DataServiceCallback.java @@ -50,12 +50,13 @@ public class DataServiceCallback { */ @Retention(RetentionPolicy.SOURCE) @IntDef({RESULT_SUCCESS, RESULT_ERROR_UNSUPPORTED, RESULT_ERROR_INVALID_ARG, RESULT_ERROR_BUSY, - RESULT_ERROR_ILLEGAL_STATE}) + RESULT_ERROR_ILLEGAL_STATE, RESULT_ERROR_TEMPORARILY_UNAVAILABLE, + RESULT_ERROR_RADIO_NOT_AVAILABLE}) public @interface ResultCode {} /** Request is completed successfully */ public static final int RESULT_SUCCESS = 0; - /** Request is not support */ + /** Request is not supported */ public static final int RESULT_ERROR_UNSUPPORTED = 1; /** Request contains invalid arguments */ public static final int RESULT_ERROR_INVALID_ARG = 2; @@ -68,6 +69,11 @@ public class DataServiceCallback { * @hide */ public static final int RESULT_ERROR_TEMPORARILY_UNAVAILABLE = 5; + /** + * Request failed to complete due to radio not available. + * @hide + */ + public static final int RESULT_ERROR_RADIO_NOT_AVAILABLE = 6; private final IDataServiceCallback mCallback; @@ -255,6 +261,8 @@ public class DataServiceCallback { return "RESULT_ERROR_ILLEGAL_STATE"; case RESULT_ERROR_TEMPORARILY_UNAVAILABLE: return "RESULT_ERROR_TEMPORARILY_UNAVAILABLE"; + case RESULT_ERROR_RADIO_NOT_AVAILABLE: + return "RESULT_ERROR_RADIO_NOT_AVAILABLE"; default: return "Unknown(" + resultCode + ")"; } |