diff options
198 files changed, 8530 insertions, 2463 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 997a3c1089cd..eef2324c7a6c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9268,6 +9268,7 @@ package android.content { field public static final int CLASSIFICATION_NOT_COMPLETE = 1; // 0x1 field public static final int CLASSIFICATION_NOT_PERFORMED = 2; // 0x2 field @NonNull public static final android.os.Parcelable.Creator<android.content.ClipDescription> CREATOR; + field public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE"; field public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE"; field public static final String MIMETYPE_TEXT_HTML = "text/html"; field public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent"; @@ -19496,7 +19497,7 @@ package android.location { method public boolean hasSatelliteBlocklist(); method public boolean hasSatellitePvt(); method public boolean hasScheduling(); - method public boolean hasSingleShot(); + method public boolean hasSingleShotFix(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssCapabilities> CREATOR; } @@ -19528,7 +19529,7 @@ package android.location { method @NonNull public android.location.GnssCapabilities.Builder setHasSatelliteBlocklist(boolean); method @NonNull public android.location.GnssCapabilities.Builder setHasSatellitePvt(boolean); method @NonNull public android.location.GnssCapabilities.Builder setHasScheduling(boolean); - method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShot(boolean); + method @NonNull public android.location.GnssCapabilities.Builder setHasSingleShotFix(boolean); } public final class GnssClock implements android.os.Parcelable { @@ -19726,7 +19727,7 @@ package android.location { method public int getConstellationType(@IntRange(from=0) int); method @FloatRange(from=0xffffffa6, to=90) public float getElevationDegrees(@IntRange(from=0) int); method @IntRange(from=0) public int getSatelliteCount(); - method @IntRange(from=1, to=200) public int getSvid(@IntRange(from=0) int); + method @IntRange(from=1, to=206) public int getSvid(@IntRange(from=0) int); method public boolean hasAlmanacData(@IntRange(from=0) int); method public boolean hasBasebandCn0DbHz(@IntRange(from=0) int); method public boolean hasCarrierFrequencyHz(@IntRange(from=0) int); @@ -47430,6 +47431,7 @@ package android.util { field public static final int DENSITY_420 = 420; // 0x1a4 field public static final int DENSITY_440 = 440; // 0x1b8 field public static final int DENSITY_450 = 450; // 0x1c2 + field public static final int DENSITY_520 = 520; // 0x208 field public static final int DENSITY_560 = 560; // 0x230 field public static final int DENSITY_600 = 600; // 0x258 field public static final int DENSITY_DEFAULT = 160; // 0xa0 diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index e890005c0479..88efcced78fb 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -301,10 +301,6 @@ package android.os { method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK}) public void reportNetworkInterfaceForTransports(@NonNull String, @NonNull int[]) throws java.lang.RuntimeException; } - public class Binder implements android.os.IBinder { - method public final void markVintfStability(); - } - public class BluetoothServiceManager { method @NonNull public android.os.BluetoothServiceManager.ServiceRegisterer getBluetoothManagerServiceRegisterer(); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 7a22e373045d..a382ecfc99d3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -775,11 +775,14 @@ package android.app { } public class BroadcastOptions { + method public void clearDeliveryGroupPolicy(); method public void clearRequireCompatChange(); + method public int getDeliveryGroupPolicy(); method public boolean isPendingIntentBackgroundActivityLaunchAllowed(); method public static android.app.BroadcastOptions makeBasic(); method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from=0) long); method @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean); + method public void setDeliveryGroupPolicy(int); method public void setDontSendToRestrictedApps(boolean); method public void setPendingIntentBackgroundActivityLaunchAllowed(boolean); method public void setRequireAllOfPermissions(@Nullable String[]); @@ -788,6 +791,8 @@ package android.app { method @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppAllowlist(long, int, int, @Nullable String); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST, android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND, android.Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND}) public void setTemporaryAppWhitelistDuration(long); method public android.os.Bundle toBundle(); + field public static final int DELIVERY_GROUP_POLICY_ALL = 0; // 0x0 + field public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; // 0x1 } public class DownloadManager { @@ -9346,6 +9351,7 @@ package android.os { public class Binder implements android.os.IBinder { method public int handleShellCommand(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull android.os.ParcelFileDescriptor, @NonNull String[]); + method public final void markVintfStability(); method public static void setProxyTransactListener(@Nullable android.os.Binder.ProxyTransactListener); } @@ -13392,7 +13398,7 @@ package android.telephony { method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int[] getCompleteActiveSubscriptionIdList(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int); method @NonNull public static android.content.res.Resources getResourcesForSubId(@NonNull android.content.Context, int); - method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getUserHandle(int); + method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public android.os.UserHandle getSubscriptionUserHandle(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isSubscriptionEnabled(int); method public void requestEmbeddedSubscriptionInfoListRefresh(); method public void requestEmbeddedSubscriptionInfoListRefresh(int); @@ -13402,8 +13408,8 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDefaultVoiceSubscriptionId(int); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setPreferredDataSubscriptionId(int, boolean, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int, boolean); + method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setSubscriptionUserHandle(int, @Nullable android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setUiccApplicationsEnabled(int, boolean); - method @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) public void setUserHandle(int, @Nullable android.os.UserHandle); field @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS) public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED"; field @NonNull public static final android.net.Uri ADVANCED_CALLING_ENABLED_CONTENT_URI; field @NonNull public static final android.net.Uri CROSS_SIM_ENABLED_CONTENT_URI; diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index cc4650a7df71..48638d1fdff4 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -31,6 +31,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; +import android.os.BundleMerger; import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerExemptionManager.TempAllowListType; @@ -67,6 +68,7 @@ public class BroadcastOptions extends ComponentOptions { private @Nullable IntentFilter mRemoveMatchingFilter; private @DeliveryGroupPolicy int mDeliveryGroupPolicy; private @Nullable String mDeliveryGroupKey; + private @Nullable BundleMerger mDeliveryGroupExtrasMerger; /** * Change ID which is invalid. @@ -218,6 +220,12 @@ public class BroadcastOptions extends ComponentOptions { "android:broadcast.deliveryGroupKey"; /** + * Corresponds to {@link #setDeliveryGroupExtrasMerger(BundleMerger)}. + */ + private static final String KEY_DELIVERY_GROUP_EXTRAS_MERGER = + "android:broadcast.deliveryGroupExtrasMerger"; + + /** * The list of delivery group policies which specify how multiple broadcasts belonging to * the same delivery group has to be handled. * @hide @@ -225,6 +233,7 @@ public class BroadcastOptions extends ComponentOptions { @IntDef(flag = true, prefix = { "DELIVERY_GROUP_POLICY_" }, value = { DELIVERY_GROUP_POLICY_ALL, DELIVERY_GROUP_POLICY_MOST_RECENT, + DELIVERY_GROUP_POLICY_MERGED, }) @Retention(RetentionPolicy.SOURCE) public @interface DeliveryGroupPolicy {} @@ -235,6 +244,7 @@ public class BroadcastOptions extends ComponentOptions { * * @hide */ + @SystemApi public static final int DELIVERY_GROUP_POLICY_ALL = 0; /** @@ -243,8 +253,17 @@ public class BroadcastOptions extends ComponentOptions { * * @hide */ + @SystemApi public static final int DELIVERY_GROUP_POLICY_MOST_RECENT = 1; + /** + * Delivery group policy that indicates that the extras data from the broadcasts in the + * delivery group need to be merged into a single broadcast and the rest can be dropped. + * + * @hide + */ + public static final int DELIVERY_GROUP_POLICY_MERGED = 2; + public static BroadcastOptions makeBasic() { BroadcastOptions opts = new BroadcastOptions(); return opts; @@ -295,6 +314,8 @@ public class BroadcastOptions extends ComponentOptions { mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY, DELIVERY_GROUP_POLICY_ALL); mDeliveryGroupKey = opts.getString(KEY_DELIVERY_GROUP_KEY); + mDeliveryGroupExtrasMerger = opts.getParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, + BundleMerger.class); } /** @@ -724,16 +745,35 @@ public class BroadcastOptions extends ComponentOptions { * * @hide */ + @SystemApi public void setDeliveryGroupPolicy(@DeliveryGroupPolicy int policy) { mDeliveryGroupPolicy = policy; } - /** @hide */ + /** + * Get the delivery group policy for this broadcast that specifies how multiple broadcasts + * belonging to the same delivery group has to be handled. + * + * @hide + */ + @SystemApi public @DeliveryGroupPolicy int getDeliveryGroupPolicy() { return mDeliveryGroupPolicy; } /** + * Clears any previously set delivery group policies using + * {@link #setDeliveryGroupKey(String, String)} and resets the delivery group policy to + * the default value ({@link #DELIVERY_GROUP_POLICY_ALL}). + * + * @hide + */ + @SystemApi + public void clearDeliveryGroupPolicy() { + mDeliveryGroupPolicy = DELIVERY_GROUP_POLICY_ALL; + } + + /** * Set namespace and key to identify the delivery group that this broadcast belongs to. * If no namespace and key is set, then by default {@link Intent#filterEquals(Intent)} will be * used to identify the delivery group. @@ -754,12 +794,35 @@ public class BroadcastOptions extends ComponentOptions { } /** + * Set the {@link BundleMerger} that specifies how to merge the extras data from + * broadcasts in a delivery group. + * + * <p>Note that this value will be ignored if the delivery group policy is not set as + * {@link #DELIVERY_GROUP_POLICY_MERGED}. + * + * @hide + */ + public void setDeliveryGroupExtrasMerger(@NonNull BundleMerger extrasMerger) { + Preconditions.checkNotNull(extrasMerger); + mDeliveryGroupExtrasMerger = extrasMerger; + } + + /** @hide */ + public @Nullable BundleMerger getDeliveryGroupExtrasMerger() { + return mDeliveryGroupExtrasMerger; + } + + /** * Returns the created options as a Bundle, which can be passed to * {@link android.content.Context#sendBroadcast(android.content.Intent) * Context.sendBroadcast(Intent)} and related methods. * Note that the returned Bundle is still owned by the BroadcastOptions * object; you must not modify it, but can supply it to the sendBroadcast * methods that take an options Bundle. + * + * @throws IllegalStateException if the broadcast option values are inconsistent. For example, + * if the delivery group policy is specified as "MERGED" but no + * extras merger is supplied. */ @Override public Bundle toBundle() { @@ -810,6 +873,15 @@ public class BroadcastOptions extends ComponentOptions { if (mDeliveryGroupKey != null) { b.putString(KEY_DELIVERY_GROUP_KEY, mDeliveryGroupKey); } + if (mDeliveryGroupPolicy == DELIVERY_GROUP_POLICY_MERGED) { + if (mDeliveryGroupExtrasMerger != null) { + b.putParcelable(KEY_DELIVERY_GROUP_EXTRAS_MERGER, + mDeliveryGroupExtrasMerger); + } else { + throw new IllegalStateException("Extras merger cannot be empty " + + "when delivery group policy is 'MERGED'"); + } + } return b.isEmpty() ? null : b; } } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 08a6b8c4e135..aaa3d21a0b25 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -173,6 +173,7 @@ import android.os.IThermalService; import android.os.IUserManager; import android.os.IncidentManager; import android.os.PerformanceHintManager; +import android.os.PermissionEnforcer; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.ServiceManager; @@ -1366,6 +1367,14 @@ public final class SystemServiceRegistry { return new PermissionCheckerManager(ctx.getOuterContext()); }}); + registerService(Context.PERMISSION_ENFORCER_SERVICE, PermissionEnforcer.class, + new CachedServiceFetcher<PermissionEnforcer>() { + @Override + public PermissionEnforcer createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new PermissionEnforcer(ctx.getOuterContext()); + }}); + registerService(Context.DYNAMIC_SYSTEM_SERVICE, DynamicSystemManager.class, new CachedServiceFetcher<DynamicSystemManager>() { @Override diff --git a/core/java/android/content/ClipDescription.java b/core/java/android/content/ClipDescription.java index bf466116009b..de2ba44ca393 100644 --- a/core/java/android/content/ClipDescription.java +++ b/core/java/android/content/ClipDescription.java @@ -139,21 +139,28 @@ public class ClipDescription implements Parcelable { * password or credit card number. * <p> * Type: boolean - * </p> * <p> * This extra can be used to indicate that a ClipData contains sensitive information that * should be redacted or hidden from view until a user takes explicit action to reveal it * (e.g., by pasting). - * </p> * <p> * Adding this extra does not change clipboard behavior or add additional security to * the ClipData. Its purpose is essentially a rendering hint from the source application, * asking that the data within be obfuscated or redacted, unless the user has taken action * to make it visible. - * </p> */ public static final String EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE"; + /** Indicates that a ClipData's source is a remote device. + * <p> + * Type: boolean + * <p> + * This extra can be used to indicate that a ClipData comes from a separate device rather + * than being local. It is a rendering hint that can be used to take different behavior + * based on the source device of copied data. + */ + public static final String EXTRA_IS_REMOTE_DEVICE = "android.content.extra.IS_REMOTE_DEVICE"; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(value = diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java index 5f859846a5c1..f12e971afb1f 100644 --- a/core/java/android/content/ComponentName.java +++ b/core/java/android/content/ComponentName.java @@ -314,17 +314,14 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co */ @Override public boolean equals(@Nullable Object obj) { - try { - if (obj != null) { - ComponentName other = (ComponentName)obj; - // Note: no null checks, because mPackage and mClass can - // never be null. - return mPackage.equals(other.mPackage) - && mClass.equals(other.mClass); - } - } catch (ClassCastException e) { + if (obj instanceof ComponentName) { + ComponentName other = (ComponentName) obj; + // mPackage and mClass can never be null. + return mPackage.equals(other.mPackage) + && mClass.equals(other.mClass); + } else { + return false; } - return false; } @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index d65210b8a0bc..cbc1789b56fc 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5142,6 +5142,14 @@ public abstract class Context { public static final String PERMISSION_CHECKER_SERVICE = "permission_checker"; /** + * Official published name of the (internal) permission enforcer service. + * + * @see #getSystemService(String) + * @hide + */ + public static final String PERMISSION_ENFORCER_SERVICE = "permission_enforcer"; + + /** * Use with {@link #getSystemService(String) to retrieve an * {@link android.apphibernation.AppHibernationManager}} for * communicating with the hibernation service. diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 43fa61782bf6..f2ebec6306f8 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -49,6 +49,7 @@ import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.BundleMerger; import android.os.IBinder; import android.os.IncidentManager; import android.os.Parcel; @@ -11072,6 +11073,20 @@ public class Intent implements Parcelable, Cloneable { } /** + * Merge the extras data in this intent with that of other supplied intent using the + * strategy specified using {@code extrasMerger}. + * + * <p> Note the extras data in this intent is treated as the {@code first} param + * and the extras data in {@code other} intent is treated as the {@code last} param + * when using the passed in {@link BundleMerger} object. + * + * @hide + */ + public void mergeExtras(@NonNull Intent other, @NonNull BundleMerger extrasMerger) { + mExtras = extrasMerger.merge(mExtras, other.mExtras); + } + + /** * Wrapper class holding an Intent and implementing comparisons on it for * the purpose of filtering. The class implements its * {@link #equals equals()} and {@link #hashCode hashCode()} methods as diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index d3a6323230a5..26435586f2d7 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -562,7 +562,7 @@ public class Binder implements IBinder { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public final native void markVintfStability(); /** diff --git a/core/java/android/os/BundleMerger.java b/core/java/android/os/BundleMerger.java new file mode 100644 index 000000000000..51bd4ea75005 --- /dev/null +++ b/core/java/android/os/BundleMerger.java @@ -0,0 +1,379 @@ +/* + * 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.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Objects; +import java.util.function.BinaryOperator; + +/** + * Configured rules for merging two {@link Bundle} instances. + * <p> + * By default, values from both {@link Bundle} instances are blended together on + * a key-wise basis, and conflicting value definitions for a key are dropped. + * <p> + * Nuanced strategies for handling conflicting value definitions can be applied + * using {@link #setMergeStrategy(String, int)} and + * {@link #setDefaultMergeStrategy(int)}. + * <p> + * When conflicting values have <em>inconsistent</em> data types (such as trying + * to merge a {@link String} and a {@link Integer}), both conflicting values are + * rejected and the key becomes undefined, regardless of the requested strategy. + * + * @hide + */ +public class BundleMerger implements Parcelable { + private static final String TAG = "BundleMerger"; + + private @Strategy int mDefaultStrategy = STRATEGY_REJECT; + + private final ArrayMap<String, Integer> mStrategies = new ArrayMap<>(); + + /** + * Merge strategy that rejects both conflicting values. + */ + public static final int STRATEGY_REJECT = 0; + + /** + * Merge strategy that selects the first of conflicting values. + */ + public static final int STRATEGY_FIRST = 1; + + /** + * Merge strategy that selects the last of conflicting values. + */ + public static final int STRATEGY_LAST = 2; + + /** + * Merge strategy that selects the "minimum" of conflicting values which are + * {@link Comparable} with each other. + */ + public static final int STRATEGY_COMPARABLE_MIN = 3; + + /** + * Merge strategy that selects the "maximum" of conflicting values which are + * {@link Comparable} with each other. + */ + public static final int STRATEGY_COMPARABLE_MAX = 4; + + /** + * Merge strategy that numerically adds both conflicting values. + */ + public static final int STRATEGY_NUMBER_ADD = 5; + + /** + * Merge strategy that numerically increments the first conflicting value by + * {@code 1} and ignores the last conflicting value. + */ + public static final int STRATEGY_NUMBER_INCREMENT_FIRST = 6; + + /** + * Merge strategy that combines conflicting values using a boolean "and" + * operation. + */ + public static final int STRATEGY_BOOLEAN_AND = 7; + + /** + * Merge strategy that combines conflicting values using a boolean "or" + * operation. + */ + public static final int STRATEGY_BOOLEAN_OR = 8; + + /** + * Merge strategy that combines two conflicting array values by appending + * the last array after the first array. + */ + public static final int STRATEGY_ARRAY_APPEND = 9; + + /** + * Merge strategy that combines two conflicting {@link ArrayList} values by + * appending the last {@link ArrayList} after the first {@link ArrayList}. + */ + public static final int STRATEGY_ARRAY_LIST_APPEND = 10; + + @IntDef(flag = false, prefix = { "STRATEGY_" }, value = { + STRATEGY_REJECT, + STRATEGY_FIRST, + STRATEGY_LAST, + STRATEGY_COMPARABLE_MIN, + STRATEGY_COMPARABLE_MAX, + STRATEGY_NUMBER_ADD, + STRATEGY_NUMBER_INCREMENT_FIRST, + STRATEGY_BOOLEAN_AND, + STRATEGY_BOOLEAN_OR, + STRATEGY_ARRAY_APPEND, + STRATEGY_ARRAY_LIST_APPEND, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Strategy {} + + /** + * Create a empty set of rules for merging two {@link Bundle} instances. + */ + public BundleMerger() { + } + + private BundleMerger(@NonNull Parcel in) { + mDefaultStrategy = in.readInt(); + final int N = in.readInt(); + for (int i = 0; i < N; i++) { + mStrategies.put(in.readString(), in.readInt()); + } + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeInt(mDefaultStrategy); + final int N = mStrategies.size(); + out.writeInt(N); + for (int i = 0; i < N; i++) { + out.writeString(mStrategies.keyAt(i)); + out.writeInt(mStrategies.valueAt(i)); + } + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Configure the default merge strategy to be used when there isn't a + * more-specific strategy defined for a particular key via + * {@link #setMergeStrategy(String, int)}. + */ + public void setDefaultMergeStrategy(@Strategy int strategy) { + mDefaultStrategy = strategy; + } + + /** + * Configure the merge strategy to be used for the given key. + * <p> + * Subsequent calls for the same key will overwrite any previously + * configured strategy. + */ + public void setMergeStrategy(@NonNull String key, @Strategy int strategy) { + mStrategies.put(key, strategy); + } + + /** + * Return the merge strategy to be used for the given key, as defined by + * {@link #setMergeStrategy(String, int)}. + * <p> + * If no specific strategy has been configured for the given key, this + * returns {@link #setDefaultMergeStrategy(int)}. + */ + public @Strategy int getMergeStrategy(@NonNull String key) { + return (int) mStrategies.getOrDefault(key, mDefaultStrategy); + } + + /** + * Return a {@link BinaryOperator} which applies the strategies configured + * in this object to merge the two given {@link Bundle} arguments. + */ + public BinaryOperator<Bundle> asBinaryOperator() { + return this::merge; + } + + /** + * Apply the strategies configured in this object to merge the two given + * {@link Bundle} arguments. + * + * @return the merged {@link Bundle} result. If one argument is {@code null} + * it will return the other argument. If both arguments are null it + * will return {@code null}. + */ + @SuppressWarnings("deprecation") + public @Nullable Bundle merge(@Nullable Bundle first, @Nullable Bundle last) { + if (first == null && last == null) { + return null; + } + if (first == null) { + first = Bundle.EMPTY; + } + if (last == null) { + last = Bundle.EMPTY; + } + + // Start by bulk-copying all values without attempting to unpack any + // custom parcelables; we'll circle back to handle conflicts below + final Bundle res = new Bundle(); + res.putAll(first); + res.putAll(last); + + final ArraySet<String> conflictingKeys = new ArraySet<>(); + conflictingKeys.addAll(first.keySet()); + conflictingKeys.retainAll(last.keySet()); + for (int i = 0; i < conflictingKeys.size(); i++) { + final String key = conflictingKeys.valueAt(i); + final int strategy = getMergeStrategy(key); + final Object firstValue = first.get(key); + final Object lastValue = last.get(key); + try { + res.putObject(key, merge(strategy, firstValue, lastValue)); + } catch (Exception e) { + Log.w(TAG, "Failed to merge key " + key + " with " + firstValue + " and " + + lastValue + " using strategy " + strategy, e); + } + } + return res; + } + + /** + * Merge the two given values. If only one of the values is defined, it + * always wins, otherwise the given strategy is applied. + * + * @hide + */ + @VisibleForTesting + public static @Nullable Object merge(@Strategy int strategy, + @Nullable Object first, @Nullable Object last) { + if (first == null) return last; + if (last == null) return first; + + if (first.getClass() != last.getClass()) { + throw new IllegalArgumentException("Merging requires consistent classes; first " + + first.getClass() + " last " + last.getClass()); + } + + switch (strategy) { + case STRATEGY_REJECT: + // Only actually reject when the values are different + if (Objects.deepEquals(first, last)) { + return first; + } else { + return null; + } + case STRATEGY_FIRST: + return first; + case STRATEGY_LAST: + return last; + case STRATEGY_COMPARABLE_MIN: + return comparableMin(first, last); + case STRATEGY_COMPARABLE_MAX: + return comparableMax(first, last); + case STRATEGY_NUMBER_ADD: + return numberAdd(first, last); + case STRATEGY_NUMBER_INCREMENT_FIRST: + return numberIncrementFirst(first, last); + case STRATEGY_BOOLEAN_AND: + return booleanAnd(first, last); + case STRATEGY_BOOLEAN_OR: + return booleanOr(first, last); + case STRATEGY_ARRAY_APPEND: + return arrayAppend(first, last); + case STRATEGY_ARRAY_LIST_APPEND: + return arrayListAppend(first, last); + default: + throw new UnsupportedOperationException(); + } + } + + @SuppressWarnings("unchecked") + private static @NonNull Object comparableMin(@NonNull Object first, @NonNull Object last) { + return ((Comparable<Object>) first).compareTo(last) < 0 ? first : last; + } + + @SuppressWarnings("unchecked") + private static @NonNull Object comparableMax(@NonNull Object first, @NonNull Object last) { + return ((Comparable<Object>) first).compareTo(last) >= 0 ? first : last; + } + + private static @NonNull Object numberAdd(@NonNull Object first, @NonNull Object last) { + if (first instanceof Integer) { + return ((Integer) first) + ((Integer) last); + } else if (first instanceof Long) { + return ((Long) first) + ((Long) last); + } else if (first instanceof Float) { + return ((Float) first) + ((Float) last); + } else if (first instanceof Double) { + return ((Double) first) + ((Double) last); + } else { + throw new IllegalArgumentException("Unable to add " + first.getClass()); + } + } + + private static @NonNull Number numberIncrementFirst(@NonNull Object first, + @NonNull Object last) { + if (first instanceof Integer) { + return ((Integer) first) + 1; + } else if (first instanceof Long) { + return ((Long) first) + 1L; + } else { + throw new IllegalArgumentException("Unable to add " + first.getClass()); + } + } + + private static @NonNull Object booleanAnd(@NonNull Object first, @NonNull Object last) { + return ((Boolean) first) && ((Boolean) last); + } + + private static @NonNull Object booleanOr(@NonNull Object first, @NonNull Object last) { + return ((Boolean) first) || ((Boolean) last); + } + + private static @NonNull Object arrayAppend(@NonNull Object first, @NonNull Object last) { + if (!first.getClass().isArray()) { + throw new IllegalArgumentException("Unable to append " + first.getClass()); + } + final Class<?> clazz = first.getClass().getComponentType(); + final int firstLength = Array.getLength(first); + final int lastLength = Array.getLength(last); + final Object res = Array.newInstance(clazz, firstLength + lastLength); + System.arraycopy(first, 0, res, 0, firstLength); + System.arraycopy(last, 0, res, firstLength, lastLength); + return res; + } + + @SuppressWarnings("unchecked") + private static @NonNull Object arrayListAppend(@NonNull Object first, @NonNull Object last) { + if (!(first instanceof ArrayList)) { + throw new IllegalArgumentException("Unable to append " + first.getClass()); + } + final ArrayList<Object> firstList = (ArrayList<Object>) first; + final ArrayList<Object> lastList = (ArrayList<Object>) last; + final ArrayList<Object> res = new ArrayList<>(firstList.size() + lastList.size()); + res.addAll(firstList); + res.addAll(lastList); + return res; + } + + public static final @android.annotation.NonNull Parcelable.Creator<BundleMerger> CREATOR = + new Parcelable.Creator<BundleMerger>() { + @Override + public BundleMerger createFromParcel(Parcel in) { + return new BundleMerger(in); + } + + @Override + public BundleMerger[] newArray(int size) { + return new BundleMerger[size]; + } + }; +} diff --git a/core/java/android/os/PermissionEnforcer.java b/core/java/android/os/PermissionEnforcer.java new file mode 100644 index 000000000000..221e89a6a76f --- /dev/null +++ b/core/java/android/os/PermissionEnforcer.java @@ -0,0 +1,101 @@ +/* + * 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.annotation.NonNull; +import android.annotation.SystemService; +import android.content.AttributionSource; +import android.content.Context; +import android.content.PermissionChecker; +import android.permission.PermissionCheckerManager; + +/** + * PermissionEnforcer check permissions for AIDL-generated services which use + * the @EnforcePermission annotation. + * + * <p>AIDL services may be annotated with @EnforcePermission which will trigger + * the generation of permission check code. This generated code relies on + * PermissionEnforcer to validate the permissions. The methods available are + * purposely similar to the AIDL annotation syntax. + * + * @see android.permission.PermissionManager + * + * @hide + */ +@SystemService(Context.PERMISSION_ENFORCER_SERVICE) +public class PermissionEnforcer { + + private final Context mContext; + + /** Protected constructor. Allows subclasses to instantiate an object + * without using a Context. + */ + protected PermissionEnforcer() { + mContext = null; + } + + /** Constructor, prefer using the fromContext static method when possible */ + public PermissionEnforcer(@NonNull Context context) { + mContext = context; + } + + @PermissionCheckerManager.PermissionResult + protected int checkPermission(@NonNull String permission, @NonNull AttributionSource source) { + return PermissionChecker.checkPermissionForDataDelivery( + mContext, permission, PermissionChecker.PID_UNKNOWN, source, "" /* message */); + } + + public void enforcePermission(@NonNull String permission, @NonNull + AttributionSource source) throws SecurityException { + int result = checkPermission(permission, source); + if (result != PermissionCheckerManager.PERMISSION_GRANTED) { + throw new SecurityException("Access denied, requires: " + permission); + } + } + + public void enforcePermissionAllOf(@NonNull String[] permissions, + @NonNull AttributionSource source) throws SecurityException { + for (String permission : permissions) { + int result = checkPermission(permission, source); + if (result != PermissionCheckerManager.PERMISSION_GRANTED) { + throw new SecurityException("Access denied, requires: allOf={" + + String.join(", ", permissions) + "}"); + } + } + } + + public void enforcePermissionAnyOf(@NonNull String[] permissions, + @NonNull AttributionSource source) throws SecurityException { + for (String permission : permissions) { + int result = checkPermission(permission, source); + if (result == PermissionCheckerManager.PERMISSION_GRANTED) { + return; + } + } + throw new SecurityException("Access denied, requires: anyOf={" + + String.join(", ", permissions) + "}"); + } + + /** + * Returns a new PermissionEnforcer based on a Context. + * + * @hide + */ + public static PermissionEnforcer fromContext(@NonNull Context context) { + return context.getSystemService(PermissionEnforcer.class); + } +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index cd2bbebf3d4d..4502eec9fe4f 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7135,7 +7135,7 @@ public final class Settings { * Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0" * where imeId is ComponentName and subtype is int32. */ - @Readable + @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU) public static final String ENABLED_INPUT_METHODS = "enabled_input_methods"; /** @@ -7144,7 +7144,7 @@ public final class Settings { * by ':'. * @hide */ - @Readable + @Readable(maxTargetSdk = Build.VERSION_CODES.TIRAMISU) public static final String DISABLED_SYSTEM_INPUT_METHODS = "disabled_system_input_methods"; /** diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index 0a3e6b1cff38..517d98222093 100755 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -174,6 +174,14 @@ public class DisplayMetrics { * This is not a density that applications should target, instead relying * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them. */ + public static final int DENSITY_520 = 520; + + /** + * Intermediate density for screens that sit somewhere between + * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi). + * This is not a density that applications should target, instead relying + * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them. + */ public static final int DENSITY_560 = 560; /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 5e836ef186d4..ff4588a7bc9b 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4970,7 +4970,7 @@ public final class ViewRootImpl implements ViewParent, } void reportKeepClearAreasChanged() { - if (!mHasPendingKeepClearAreaChange) { + if (!mHasPendingKeepClearAreaChange || mView == null) { return; } mHasPendingKeepClearAreaChange = false; diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto index 5099dd20a6d5..9e4f63cab86c 100644 --- a/core/proto/android/server/activitymanagerservice.proto +++ b/core/proto/android/server/activitymanagerservice.proto @@ -977,6 +977,7 @@ message UserControllerProto { optional int32 profile = 2; } repeated UserProfile user_profile_group_ids = 4; + repeated int32 visible_users_array = 5; } // sync with com.android.server.am.AppTimeTracker.java diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java new file mode 100644 index 000000000000..fe3ab625bacc --- /dev/null +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java @@ -0,0 +1,433 @@ +/* + * 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.hardware.radio.tests.unittests; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Bitmap; +import android.hardware.radio.IRadioService; +import android.hardware.radio.ITuner; +import android.hardware.radio.ITunerCallback; +import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioManager; +import android.hardware.radio.RadioMetadata; +import android.hardware.radio.RadioTuner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@RunWith(MockitoJUnitRunner.class) +public final class TunerAdapterTest { + + private static final int CALLBACK_TIMEOUT_MS = 30_000; + private static final int AM_LOWER_LIMIT_KHZ = 150; + + private static final RadioManager.BandConfig TEST_BAND_CONFIG = createBandConfig(); + + private static final ProgramSelector.Identifier FM_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, + /* value= */ 94300); + private static final ProgramSelector FM_SELECTOR = + new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER, + /* secondaryIds= */ null, /* vendorIds= */ null); + private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo(); + + private RadioTuner mRadioTuner; + private ITunerCallback mTunerCallback; + + @Mock + private IRadioService mRadioServiceMock; + @Mock + private Context mContextMock; + @Mock + private ITuner mTunerMock; + @Mock + private RadioTuner.Callback mCallbackMock; + + @Before + public void setUp() throws Exception { + RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock); + + doAnswer(invocation -> { + mTunerCallback = (ITunerCallback) invocation.getArguments()[3]; + return mTunerMock; + }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any()); + + doAnswer(invocation -> { + ProgramSelector program = (ProgramSelector) invocation.getArguments()[0]; + if (program.getPrimaryId().getType() + != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY) { + throw new IllegalArgumentException(); + } + if (program.getPrimaryId().getValue() < AM_LOWER_LIMIT_KHZ) { + mTunerCallback.onTuneFailed(RadioManager.STATUS_BAD_VALUE, program); + } else { + mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); + } + return RadioManager.STATUS_OK; + }).when(mTunerMock).tune(any()); + + mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, TEST_BAND_CONFIG, + /* withAudio= */ true, mCallbackMock, /* handler= */ null); + } + + @After + public void cleanUp() throws Exception { + mRadioTuner.close(); + } + + @Test + public void close_forTunerAdapter() throws Exception { + mRadioTuner.close(); + + verify(mTunerMock).close(); + } + + @Test + public void setConfiguration_forTunerAdapter() throws Exception { + int status = mRadioTuner.setConfiguration(TEST_BAND_CONFIG); + + verify(mTunerMock).setConfiguration(TEST_BAND_CONFIG); + assertWithMessage("Status for setting configuration") + .that(status).isEqualTo(RadioManager.STATUS_OK); + } + + @Test + public void getConfiguration_forTunerAdapter() throws Exception { + when(mTunerMock.getConfiguration()).thenReturn(TEST_BAND_CONFIG); + RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1]; + + int status = mRadioTuner.getConfiguration(bandConfigs); + + assertWithMessage("Status for getting configuration") + .that(status).isEqualTo(RadioManager.STATUS_OK); + assertWithMessage("Configuration obtained from radio tuner") + .that(bandConfigs[0]).isEqualTo(TEST_BAND_CONFIG); + } + + @Test + public void setMute_forTunerAdapter() { + int status = mRadioTuner.setMute(/* mute= */ true); + + assertWithMessage("Status for setting mute") + .that(status).isEqualTo(RadioManager.STATUS_OK); + } + + @Test + public void getMute_forTunerAdapter() throws Exception { + when(mTunerMock.isMuted()).thenReturn(true); + + boolean muteStatus = mRadioTuner.getMute(); + + assertWithMessage("Mute status").that(muteStatus).isTrue(); + } + + @Test + public void step_forTunerAdapter_succeeds() throws Exception { + doAnswer(invocation -> { + mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); + return RadioManager.STATUS_OK; + }).when(mTunerMock).step(anyBoolean(), anyBoolean()); + + int scanStatus = mRadioTuner.step(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); + + verify(mTunerMock).step(/* skipSubChannel= */ true, /* skipSubChannel= */ false); + assertWithMessage("Status for stepping") + .that(scanStatus).isEqualTo(RadioManager.STATUS_OK); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + @Test + public void seek_forTunerAdapter_succeeds() throws Exception { + doAnswer(invocation -> { + mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); + return RadioManager.STATUS_OK; + }).when(mTunerMock).scan(anyBoolean(), anyBoolean()); + + int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); + + verify(mTunerMock).scan(/* directionDown= */ true, /* skipSubChannel= */ false); + assertWithMessage("Status for seeking") + .that(scanStatus).isEqualTo(RadioManager.STATUS_OK); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + @Test + public void seek_forTunerAdapter_invokesOnErrorWhenTimeout() throws Exception { + doAnswer(invocation -> { + mTunerCallback.onError(RadioTuner.ERROR_SCAN_TIMEOUT); + return RadioManager.STATUS_OK; + }).when(mTunerMock).scan(anyBoolean(), anyBoolean()); + + mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SCAN_TIMEOUT); + } + + @Test + public void tune_withChannelsForTunerAdapter_succeeds() { + int status = mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0); + + assertWithMessage("Status for tuning with channel and sub-channel") + .that(status).isEqualTo(RadioManager.STATUS_OK); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + @Test + public void tune_withValidSelectorForTunerAdapter_succeeds() throws Exception { + mRadioTuner.tune(FM_SELECTOR); + + verify(mTunerMock).tune(FM_SELECTOR); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + } + + + @Test + public void tune_withInvalidSelectorForTunerAdapter_invokesOnTuneFailed() { + ProgramSelector invalidSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 100), + /* secondaryIds= */ null, /* vendorIds= */ null); + + mRadioTuner.tune(invalidSelector); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onTuneFailed(RadioManager.STATUS_BAD_VALUE, invalidSelector); + } + + @Test + public void cancel_forTunerAdapter() throws Exception { + mRadioTuner.tune(FM_SELECTOR); + + mRadioTuner.cancel(); + + verify(mTunerMock).cancel(); + } + + @Test + public void cancelAnnouncement_forTunerAdapter() throws Exception { + mRadioTuner.cancelAnnouncement(); + + verify(mTunerMock).cancelAnnouncement(); + } + + @Test + public void getProgramInfo_beforeProgramInfoSetForTunerAdapter() { + RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1]; + + int status = mRadioTuner.getProgramInformation(programInfoArray); + + assertWithMessage("Status for getting null program info") + .that(status).isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void getProgramInfo_afterTuneForTunerAdapter() { + mRadioTuner.tune(FM_SELECTOR); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO); + RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1]; + + int status = mRadioTuner.getProgramInformation(programInfoArray); + + assertWithMessage("Status for getting program info") + .that(status).isEqualTo(RadioManager.STATUS_OK); + assertWithMessage("Program info obtained from radio tuner") + .that(programInfoArray[0]).isEqualTo(FM_PROGRAM_INFO); + } + + @Test + public void getMetadataImage_forTunerAdapter() throws Exception { + Bitmap bitmapExpected = Mockito.mock(Bitmap.class); + when(mTunerMock.getImage(anyInt())).thenReturn(bitmapExpected); + int imageId = 1; + + Bitmap image = mRadioTuner.getMetadataImage(/* id= */ imageId); + + assertWithMessage("Image obtained from id %s", imageId) + .that(image).isEqualTo(bitmapExpected); + } + + @Test + public void isAnalogForced_forTunerAdapter() throws Exception { + when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true); + + boolean isAnalogForced = mRadioTuner.isAnalogForced(); + + assertWithMessage("Forced analog playback switch") + .that(isAnalogForced).isTrue(); + } + + @Test + public void setAnalogForced_forTunerAdapter() throws Exception { + boolean analogForced = true; + + mRadioTuner.setAnalogForced(analogForced); + + verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, analogForced); + } + + @Test + public void isConfigFlagSupported_forTunerAdapter() throws Exception { + when(mTunerMock.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING)) + .thenReturn(true); + + boolean dabFmSoftLinking = + mRadioTuner.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING); + + assertWithMessage("Support for DAB-DAB linking config flag") + .that(dabFmSoftLinking).isTrue(); + } + + @Test + public void isConfigFlagSet_forTunerAdapter() throws Exception { + when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING)) + .thenReturn(true); + + boolean dabFmSoftLinking = + mRadioTuner.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING); + + assertWithMessage("DAB-FM soft linking config flag") + .that(dabFmSoftLinking).isTrue(); + } + + @Test + public void setConfigFlag_forTunerAdapter() throws Exception { + boolean dabFmLinking = true; + + mRadioTuner.setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking); + + verify(mTunerMock).setConfigFlag(RadioManager.CONFIG_DAB_FM_LINKING, dabFmLinking); + } + + @Test + public void getParameters_forTunerAdapter() throws Exception { + List<String> parameterKeys = Arrays.asList("ParameterKeyMock"); + Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); + when(mTunerMock.getParameters(parameterKeys)).thenReturn(parameters); + + assertWithMessage("Parameters obtained from radio tuner") + .that(mRadioTuner.getParameters(parameterKeys)).isEqualTo(parameters); + } + + @Test + public void setParameters_forTunerAdapter() throws Exception { + Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); + when(mTunerMock.setParameters(parameters)).thenReturn(parameters); + + assertWithMessage("Parameters set for radio tuner") + .that(mRadioTuner.setParameters(parameters)).isEqualTo(parameters); + } + + @Test + public void isAntennaConnected_forTunerAdapter() throws Exception { + mTunerCallback.onAntennaState(/* connected= */ false); + + assertWithMessage("Antenna connection status") + .that(mRadioTuner.isAntennaConnected()).isFalse(); + } + + @Test + public void hasControl_forTunerAdapter() throws Exception { + when(mTunerMock.isClosed()).thenReturn(true); + + assertWithMessage("Control on tuner").that(mRadioTuner.hasControl()).isFalse(); + } + + @Test + public void onConfigurationChanged_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onConfigurationChanged(TEST_BAND_CONFIG); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onConfigurationChanged(TEST_BAND_CONFIG); + } + + @Test + public void onTrafficAnnouncement_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onTrafficAnnouncement(/* active= */ true); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onTrafficAnnouncement(/* active= */ true); + } + + @Test + public void onEmergencyAnnouncement_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onEmergencyAnnouncement(/* active= */ true); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onEmergencyAnnouncement(/* active= */ true); + } + + @Test + public void onBackgroundScanAvailabilityChange_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onBackgroundScanAvailabilityChange(/* isAvailable= */ false); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)) + .onBackgroundScanAvailabilityChange(/* isAvailable= */ false); + } + + @Test + public void onProgramListChanged_forTunerCallbackAdapter() throws Exception { + mTunerCallback.onProgramListChanged(); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramListChanged(); + } + + @Test + public void onParametersUpdated_forTunerCallbackAdapter() throws Exception { + Map<String, String> parametersExpected = Map.of("ParameterKeyMock", "ParameterValueMock"); + + mTunerCallback.onParametersUpdated(parametersExpected); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onParametersUpdated(parametersExpected); + } + + private static RadioManager.ProgramInfo createFmProgramInfo() { + return new RadioManager.ProgramInfo(FM_SELECTOR, FM_IDENTIFIER, FM_IDENTIFIER, + /* relatedContent= */ null, /* infoFlags= */ 0b110001, + /* signalQuality= */ 1, createRadioMetadata(), /* vendorInfo= */ null); + } + + private static RadioManager.FmBandConfig createBandConfig() { + return new RadioManager.FmBandConfig(new RadioManager.FmBandDescriptor( + RadioManager.REGION_ITU_1, RadioManager.BAND_FM, /* lowerLimit= */ 87500, + /* upperLimit= */ 108000, /* spacing= */ 200, /* stereo= */ true, + /* rds= */ false, /* ta= */ false, /* af= */ false, /* es= */ false)); + } + + private static RadioMetadata createRadioMetadata() { + RadioMetadata.Builder metadataBuilder = new RadioMetadata.Builder(); + return metadataBuilder.putString(RadioMetadata.METADATA_KEY_ARTIST, "artistMock").build(); + } +} diff --git a/core/tests/coretests/src/android/app/backup/OWNERS b/core/tests/coretests/src/android/app/backup/OWNERS new file mode 100644 index 000000000000..53b6c78b3895 --- /dev/null +++ b/core/tests/coretests/src/android/app/backup/OWNERS @@ -0,0 +1 @@ +include /services/backup/OWNERS
\ No newline at end of file diff --git a/core/tests/coretests/src/android/os/BundleMergerTest.java b/core/tests/coretests/src/android/os/BundleMergerTest.java new file mode 100644 index 000000000000..b7012ba66124 --- /dev/null +++ b/core/tests/coretests/src/android/os/BundleMergerTest.java @@ -0,0 +1,408 @@ +/* + * 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 android.os.BundleMerger.STRATEGY_ARRAY_APPEND; +import static android.os.BundleMerger.STRATEGY_ARRAY_LIST_APPEND; +import static android.os.BundleMerger.STRATEGY_BOOLEAN_AND; +import static android.os.BundleMerger.STRATEGY_BOOLEAN_OR; +import static android.os.BundleMerger.STRATEGY_COMPARABLE_MAX; +import static android.os.BundleMerger.STRATEGY_COMPARABLE_MIN; +import static android.os.BundleMerger.STRATEGY_FIRST; +import static android.os.BundleMerger.STRATEGY_LAST; +import static android.os.BundleMerger.STRATEGY_NUMBER_ADD; +import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST; +import static android.os.BundleMerger.STRATEGY_REJECT; +import static android.os.BundleMerger.merge; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.content.Intent; +import android.net.Uri; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@SmallTest +@RunWith(JUnit4.class) +public class BundleMergerTest { + /** + * Strategies are only applied when there is an actual conflict; in the + * absence of conflict we pick whichever value is defined. + */ + @Test + public void testNoConflict() throws Exception { + for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) { + assertEquals(null, merge(strategy, null, null)); + assertEquals(10, merge(strategy, 10, null)); + assertEquals(20, merge(strategy, null, 20)); + } + } + + /** + * Strategies are only applied to identical data types; if there are mixed + * types we always reject the two conflicting values. + */ + @Test + public void testMixedTypes() throws Exception { + for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) { + final int finalStrategy = strategy; + assertThrows(Exception.class, () -> { + merge(finalStrategy, 10, "foo"); + }); + assertThrows(Exception.class, () -> { + merge(finalStrategy, List.of("foo"), "bar"); + }); + assertThrows(Exception.class, () -> { + merge(finalStrategy, new String[] { "foo" }, "bar"); + }); + assertThrows(Exception.class, () -> { + merge(finalStrategy, Integer.valueOf(10), Long.valueOf(10)); + }); + } + } + + @Test + public void testStrategyReject() throws Exception { + assertEquals(null, merge(STRATEGY_REJECT, 10, 20)); + + // Identical values aren't technically a conflict, so they're passed + // through without being rejected + assertEquals(10, merge(STRATEGY_REJECT, 10, 10)); + assertArrayEquals(new int[] {10}, + (int[]) merge(STRATEGY_REJECT, new int[] {10}, new int[] {10})); + } + + @Test + public void testStrategyFirst() throws Exception { + assertEquals(10, merge(STRATEGY_FIRST, 10, 20)); + } + + @Test + public void testStrategyLast() throws Exception { + assertEquals(20, merge(STRATEGY_LAST, 10, 20)); + } + + @Test + public void testStrategyComparableMin() throws Exception { + assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 10, 20)); + assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 20, 10)); + assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "a", "z")); + assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "z", "a")); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_COMPARABLE_MIN, new Binder(), new Binder()); + }); + } + + @Test + public void testStrategyComparableMax() throws Exception { + assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 10, 20)); + assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 20, 10)); + assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "a", "z")); + assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "z", "a")); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_COMPARABLE_MAX, new Binder(), new Binder()); + }); + } + + @Test + public void testStrategyNumberAdd() throws Exception { + assertEquals(30, merge(STRATEGY_NUMBER_ADD, 10, 20)); + assertEquals(30, merge(STRATEGY_NUMBER_ADD, 20, 10)); + assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 10L, 20L)); + assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 20L, 10L)); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_NUMBER_ADD, new Binder(), new Binder()); + }); + } + + @Test + public void testStrategyNumberIncrementFirst() throws Exception { + assertEquals(11, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10, 20)); + assertEquals(21, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20, 10)); + assertEquals(11L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10L, 20L)); + assertEquals(21L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20L, 10L)); + } + + @Test + public void testStrategyBooleanAnd() throws Exception { + assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, false)); + assertEquals(false, merge(STRATEGY_BOOLEAN_AND, true, false)); + assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, true)); + assertEquals(true, merge(STRATEGY_BOOLEAN_AND, true, true)); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_BOOLEAN_AND, "True!", "False?"); + }); + } + + @Test + public void testStrategyBooleanOr() throws Exception { + assertEquals(false, merge(STRATEGY_BOOLEAN_OR, false, false)); + assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, false)); + assertEquals(true, merge(STRATEGY_BOOLEAN_OR, false, true)); + assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, true)); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_BOOLEAN_OR, "True!", "False?"); + }); + } + + @Test + public void testStrategyArrayAppend() throws Exception { + assertArrayEquals(new int[] {}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {})); + assertArrayEquals(new int[] {10}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {})); + assertArrayEquals(new int[] {20}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {20})); + assertArrayEquals(new int[] {10, 20}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {20})); + assertArrayEquals(new int[] {10, 30, 20, 40}, + (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10, 30}, new int[] {20, 40})); + assertArrayEquals(new String[] {"a", "b"}, + (String[]) merge(STRATEGY_ARRAY_APPEND, new String[] {"a"}, new String[] {"b"})); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_ARRAY_APPEND, 10, 20); + }); + } + + @Test + public void testStrategyArrayListAppend() throws Exception { + assertEquals(arrayListOf(), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf())); + assertEquals(arrayListOf(10), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf())); + assertEquals(arrayListOf(20), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf(20))); + assertEquals(arrayListOf(10, 20), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf(20))); + assertEquals(arrayListOf(10, 30, 20, 40), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10, 30), arrayListOf(20, 40))); + assertEquals(arrayListOf("a", "b"), + merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf("a"), arrayListOf("b"))); + + assertThrows(Exception.class, () -> { + merge(STRATEGY_ARRAY_LIST_APPEND, 10, 20); + }); + } + + @Test + public void testMerge_Simple() throws Exception { + final BundleMerger merger = new BundleMerger(); + final Bundle probe = new Bundle(); + probe.putInt(Intent.EXTRA_INDEX, 42); + + assertEquals(null, merger.merge(null, null)); + assertEquals(probe.keySet(), merger.merge(probe, null).keySet()); + assertEquals(probe.keySet(), merger.merge(null, probe).keySet()); + assertEquals(probe.keySet(), merger.merge(probe, probe).keySet()); + } + + /** + * Verify that we can merge parcelables present in the base classpath, since + * everyone on the device will be able to unpack them. + */ + @Test + public void testMerge_Parcelable_BCP() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(Intent.EXTRA_STREAM, STRATEGY_COMPARABLE_MIN); + + Bundle a = new Bundle(); + a.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.com")); + a = parcelAndUnparcel(a); + + Bundle b = new Bundle(); + b.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.net")); + b = parcelAndUnparcel(b); + + assertEquals(Uri.parse("http://example.com"), + merger.merge(a, b).getParcelable(Intent.EXTRA_STREAM, Uri.class)); + assertEquals(Uri.parse("http://example.com"), + merger.merge(b, a).getParcelable(Intent.EXTRA_STREAM, Uri.class)); + } + + /** + * Verify that we tiptoe around custom parcelables while still merging other + * known data types. Custom parcelables aren't in the base classpath, so not + * everyone on the device will be able to unpack them. + */ + @Test + public void testMerge_Parcelable_Custom() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(Intent.EXTRA_INDEX, STRATEGY_NUMBER_ADD); + + Bundle a = new Bundle(); + a.putInt(Intent.EXTRA_INDEX, 10); + a.putString(Intent.EXTRA_CC, "foo@bar.com"); + a.putParcelable(Intent.EXTRA_SUBJECT, new ExplodingParcelable()); + a = parcelAndUnparcel(a); + + Bundle b = new Bundle(); + b.putInt(Intent.EXTRA_INDEX, 20); + a.putString(Intent.EXTRA_BCC, "foo@baz.com"); + b.putParcelable(Intent.EXTRA_STREAM, new ExplodingParcelable()); + b = parcelAndUnparcel(b); + + Bundle ab = merger.merge(a, b); + assertEquals(Set.of(Intent.EXTRA_INDEX, Intent.EXTRA_CC, Intent.EXTRA_BCC, + Intent.EXTRA_SUBJECT, Intent.EXTRA_STREAM), ab.keySet()); + assertEquals(30, ab.getInt(Intent.EXTRA_INDEX)); + assertEquals("foo@bar.com", ab.getString(Intent.EXTRA_CC)); + assertEquals("foo@baz.com", ab.getString(Intent.EXTRA_BCC)); + + // And finally, make sure that if we try unpacking one of our custom + // values that we actually explode + assertThrows(BadParcelableException.class, () -> { + ab.getParcelable(Intent.EXTRA_SUBJECT, ExplodingParcelable.class); + }); + assertThrows(BadParcelableException.class, () -> { + ab.getParcelable(Intent.EXTRA_STREAM, ExplodingParcelable.class); + }); + } + + @Test + public void testMerge_PackageChanged() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, STRATEGY_ARRAY_APPEND); + + final Bundle first = new Bundle(); + first.putInt(Intent.EXTRA_UID, 10001); + first.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] { + "com.example.Foo", + }); + + final Bundle second = new Bundle(); + second.putInt(Intent.EXTRA_UID, 10001); + second.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] { + "com.example.Bar", + "com.example.Baz", + }); + + final Bundle res = merger.merge(first, second); + assertEquals(10001, res.getInt(Intent.EXTRA_UID)); + assertArrayEquals(new String[] { + "com.example.Foo", "com.example.Bar", "com.example.Baz", + }, res.getStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST)); + } + + /** + * Each event in isolation reports "zero events dropped", but if we need to + * merge them together, then we start incrementing. + */ + @Test + public void testMerge_DropBox() throws Exception { + final BundleMerger merger = new BundleMerger(); + merger.setMergeStrategy(DropBoxManager.EXTRA_TIME, + STRATEGY_COMPARABLE_MAX); + merger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT, + STRATEGY_NUMBER_INCREMENT_FIRST); + + final long now = System.currentTimeMillis(); + final Bundle a = new Bundle(); + a.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); + a.putLong(DropBoxManager.EXTRA_TIME, now); + a.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); + + final Bundle b = new Bundle(); + b.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); + b.putLong(DropBoxManager.EXTRA_TIME, now + 1000); + b.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); + + final Bundle c = new Bundle(); + c.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); + c.putLong(DropBoxManager.EXTRA_TIME, now + 2000); + c.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); + + final Bundle ab = merger.merge(a, b); + assertEquals("system_server_strictmode", ab.getString(DropBoxManager.EXTRA_TAG)); + assertEquals(now + 1000, ab.getLong(DropBoxManager.EXTRA_TIME)); + assertEquals(1, ab.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); + + final Bundle abc = merger.merge(ab, c); + assertEquals("system_server_strictmode", abc.getString(DropBoxManager.EXTRA_TAG)); + assertEquals(now + 2000, abc.getLong(DropBoxManager.EXTRA_TIME)); + assertEquals(2, abc.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); + } + + private static ArrayList<Object> arrayListOf(Object... values) { + final ArrayList<Object> res = new ArrayList<>(values.length); + for (Object value : values) { + res.add(value); + } + return res; + } + + private static Bundle parcelAndUnparcel(Bundle input) { + final Parcel parcel = Parcel.obtain(); + try { + input.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + return Bundle.CREATOR.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } + + /** + * Object that only offers to parcel itself; if something tries unparceling + * it, it will "explode" by throwing an exception. + * <p> + * Useful for verifying interactions that must leave unknown data in a + * parceled state. + */ + public static class ExplodingParcelable implements Parcelable { + public ExplodingParcelable() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(42); + } + + public static final Creator<ExplodingParcelable> CREATOR = + new Creator<ExplodingParcelable>() { + @Override + public ExplodingParcelable createFromParcel(Parcel in) { + throw new BadParcelableException("exploding!"); + } + + @Override + public ExplodingParcelable[] newArray(int size) { + throw new BadParcelableException("exploding!"); + } + }; + } +} diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 30c3d50ed8ad..df5f921f3a62 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -23,6 +23,10 @@ TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> <bool name="config_registerShellTaskOrganizerOnInit">true</bool> + <!-- Determines whether to register the shell transitions on init. + TODO(b/238217847): This config is temporary until we refactor the base WMComponent. --> + <bool name="config_registerShellTransitionsOnInit">true</bool> + <!-- Animation duration for PIP when entering. --> <integer name="config_pipEnterAnimationDuration">425</integer> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index 48c5f64e0dd4..bd2ea9c1f822 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -41,7 +41,6 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -122,7 +121,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, /** Until all users are converted, we may have mixed-use (eg. Car). */ private boolean isUsingShellTransitions() { - return mTaskViewTransitions != null && Transitions.ENABLE_SHELL_TRANSITIONS; + return mTaskViewTransitions != null && mTaskViewTransitions.isEnabled(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java index 83335ac24799..07d501201105 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java @@ -87,6 +87,10 @@ public class TaskViewTransitions implements Transitions.TransitionHandler { // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell } + boolean isEnabled() { + return mTransitions.isRegistered(); + } + /** * Looks through the pending transitions for one matching `taskView`. * @param taskView the pending transition should be for this. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 725b20525bf7..3972b592c448 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -671,10 +671,18 @@ public class BubbleController implements ConfigurationChangeListener { return; } + mAddedToWindowManager = false; + // Put on background for this binder call, was causing jank + mBackgroundExecutor.execute(() -> { + try { + mContext.unregisterReceiver(mBroadcastReceiver); + } catch (IllegalArgumentException e) { + // Not sure if this happens in production, but was happening in tests + // (b/253647225) + e.printStackTrace(); + } + }); try { - mAddedToWindowManager = false; - // Put on background for this binder call, was causing jank - mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver)); if (mStackView != null) { mWindowManager.removeView(mStackView); mBubbleData.getOverflow().cleanUpExpandedState(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 64dbfbbb738d..8b8e192cf964 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -507,6 +507,10 @@ public abstract class WMShellBaseModule { @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, @ShellAnimationThread ShellExecutor animExecutor) { + if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) { + // TODO(b/238217847): Force override shell init if registration is disabled + shellInit = new ShellInit(mainExecutor); + } return new Transitions(context, shellInit, shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index b9746e338ced..cbed4b5a501f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -115,8 +115,8 @@ public class PipSurfaceTransactionHelper { // coordinates so offset the bounds to 0,0 mTmpDestinationRect.offsetTo(0, 0); mTmpDestinationRect.inset(insets); - // Scale by the shortest edge and offset such that the top/left of the scaled inset source - // rect aligns with the top/left of the destination bounds + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds final float scale; if (isInPipDirection && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { @@ -129,9 +129,8 @@ public class PipSurfaceTransactionHelper { : (float) destinationBounds.height() / sourceBounds.height(); scale = (1 - fraction) * startScale + fraction * endScale; } else { - scale = sourceBounds.width() <= sourceBounds.height() - ? (float) destinationBounds.width() / sourceBounds.width() - : (float) destinationBounds.height() / sourceBounds.height(); + scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); } final float left = destinationBounds.left - insets.left * scale; final float top = destinationBounds.top - insets.top * scale; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index d7ca791e3863..21a13103616c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -108,6 +108,14 @@ class SplitScreenTransitions { private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, @NonNull WindowContainerToken topRoot) { + final TransitSession pendingTransition = getPendingTransition(transition); + if (pendingTransition != null && pendingTransition.mCanceled) { + // The pending transition was canceled, so skip playing animation. + t.apply(); + onFinish(null /* wct */, null /* wctCB */); + return; + } + // Play some place-holder fade animations for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -170,9 +178,7 @@ class SplitScreenTransitions { } boolean isPendingTransition(IBinder transition) { - return isPendingEnter(transition) - || isPendingDismiss(transition) - || isPendingRecent(transition); + return getPendingTransition(transition) != null; } boolean isPendingEnter(IBinder transition) { @@ -187,22 +193,38 @@ class SplitScreenTransitions { return mPendingDismiss != null && mPendingDismiss.mTransition == transition; } + @Nullable + private TransitSession getPendingTransition(IBinder transition) { + if (isPendingEnter(transition)) { + return mPendingEnter; + } else if (isPendingRecent(transition)) { + return mPendingRecent; + } else if (isPendingDismiss(transition)) { + return mPendingDismiss; + } + + return null; + } + /** Starts a transition to enter split with a remote transition animator. */ IBinder startEnterTransition( @WindowManager.TransitionType int transitType, WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, Transitions.TransitionHandler handler, - @Nullable TransitionCallback callback) { + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { final IBinder transition = mTransitions.startTransition(transitType, wct, handler); - setEnterTransition(transition, remoteTransition, callback); + setEnterTransition(transition, remoteTransition, consumedCallback, finishedCallback); return transition; } /** Sets a transition to enter split. */ void setEnterTransition(@NonNull IBinder transition, - @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) { - mPendingEnter = new TransitSession(transition, callback); + @Nullable RemoteTransition remoteTransition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { + mPendingEnter = new TransitSession(transition, consumedCallback, finishedCallback); if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) @@ -237,8 +259,9 @@ class SplitScreenTransitions { } void setRecentTransition(@NonNull IBinder transition, - @Nullable RemoteTransition remoteTransition, @Nullable TransitionCallback callback) { - mPendingRecent = new TransitSession(transition, callback); + @Nullable RemoteTransition remoteTransition, + @Nullable TransitionFinishedCallback finishCallback) { + mPendingRecent = new TransitSession(transition, null /* consumedCb */, finishCallback); if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) @@ -248,7 +271,7 @@ class SplitScreenTransitions { } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " - + " deduced Enter recent panel"); + + " deduced Enter recent panel"); } void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, @@ -256,14 +279,9 @@ class SplitScreenTransitions { if (mergeTarget != mAnimatingTransition) return; if (isPendingEnter(transition) && isPendingRecent(mergeTarget)) { - mPendingRecent.mCallback = new TransitionCallback() { - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - // Since there's an entering transition merged, recent transition no longer - // need to handle entering split screen after the transition finished. - } - }; + // Since there's an entering transition merged, recent transition no longer + // need to handle entering split screen after the transition finished. + mPendingRecent.setFinishedCallback(null); } if (mActiveRemoteHandler != null) { @@ -277,7 +295,7 @@ class SplitScreenTransitions { } boolean end() { - // If its remote, there's nothing we can do right now. + // If It's remote, there's nothing we can do right now. if (mActiveRemoteHandler != null) return false; for (int i = mAnimations.size() - 1; i >= 0; --i) { final Animator anim = mAnimations.get(i); @@ -290,20 +308,20 @@ class SplitScreenTransitions { @Nullable SurfaceControl.Transaction finishT) { if (isPendingEnter(transition)) { if (!aborted) { - // An enter transition got merged, appends the rest operations to finish entering + // An entering transition got merged, appends the rest operations to finish entering // split screen. mStageCoordinator.finishEnterSplitScreen(finishT); mPendingRemoteHandler = null; } - mPendingEnter.mCallback.onTransitionConsumed(aborted); + mPendingEnter.onConsumed(aborted); mPendingEnter = null; mPendingRemoteHandler = null; } else if (isPendingDismiss(transition)) { - mPendingDismiss.mCallback.onTransitionConsumed(aborted); + mPendingDismiss.onConsumed(aborted); mPendingDismiss = null; } else if (isPendingRecent(transition)) { - mPendingRecent.mCallback.onTransitionConsumed(aborted); + mPendingRecent.onConsumed(aborted); mPendingRecent = null; mPendingRemoteHandler = null; } @@ -312,23 +330,16 @@ class SplitScreenTransitions { void onFinish(WindowContainerTransaction wct, WindowContainerTransactionCallback wctCB) { if (!mAnimations.isEmpty()) return; - TransitionCallback callback = null; + if (wct == null) wct = new WindowContainerTransaction(); if (isPendingEnter(mAnimatingTransition)) { - callback = mPendingEnter.mCallback; + mPendingEnter.onFinished(wct, mFinishTransaction); mPendingEnter = null; - } - if (isPendingDismiss(mAnimatingTransition)) { - callback = mPendingDismiss.mCallback; - mPendingDismiss = null; - } - if (isPendingRecent(mAnimatingTransition)) { - callback = mPendingRecent.mCallback; + } else if (isPendingRecent(mAnimatingTransition)) { + mPendingRecent.onFinished(wct, mFinishTransaction); mPendingRecent = null; - } - - if (callback != null) { - if (wct == null) wct = new WindowContainerTransaction(); - callback.onTransitionFinished(wct, mFinishTransaction); + } else if (isPendingDismiss(mAnimatingTransition)) { + mPendingDismiss.onFinished(wct, mFinishTransaction); + mPendingDismiss = null; } mPendingRemoteHandler = null; @@ -363,10 +374,7 @@ class SplitScreenTransitions { onFinish(null /* wct */, null /* wctCB */); }); }; - va.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { } - + va.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finisher.run(); @@ -376,9 +384,6 @@ class SplitScreenTransitions { public void onAnimationCancel(Animator animation) { finisher.run(); } - - @Override - public void onAnimationRepeat(Animator animation) { } }); mAnimations.add(va); mTransitions.getAnimExecutor().execute(va::start); @@ -432,24 +437,66 @@ class SplitScreenTransitions { || info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN; } - /** Clean-up callbacks for transition. */ - interface TransitionCallback { - /** Calls when the transition got consumed. */ - default void onTransitionConsumed(boolean aborted) {} + /** Calls when the transition got consumed. */ + interface TransitionConsumedCallback { + void onConsumed(boolean aborted); + } - /** Calls when the transition finished. */ - default void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) {} + /** Calls when the transition finished. */ + interface TransitionFinishedCallback { + void onFinished(WindowContainerTransaction wct, SurfaceControl.Transaction t); } /** Session for a transition and its clean-up callback. */ static class TransitSession { final IBinder mTransition; - TransitionCallback mCallback; + TransitionConsumedCallback mConsumedCallback; + TransitionFinishedCallback mFinishedCallback; - TransitSession(IBinder transition, @Nullable TransitionCallback callback) { + /** Whether the transition was canceled. */ + boolean mCanceled; + + TransitSession(IBinder transition, + @Nullable TransitionConsumedCallback consumedCallback, + @Nullable TransitionFinishedCallback finishedCallback) { mTransition = transition; - mCallback = callback != null ? callback : new TransitionCallback() {}; + mConsumedCallback = consumedCallback; + mFinishedCallback = finishedCallback; + + } + + /** Sets transition consumed callback. */ + void setConsumedCallback(@Nullable TransitionConsumedCallback callback) { + mConsumedCallback = callback; + } + + /** Sets transition finished callback. */ + void setFinishedCallback(@Nullable TransitionFinishedCallback callback) { + mFinishedCallback = callback; + } + + /** + * Cancels the transition. This should be called before playing animation. A canceled + * transition will skip playing animation. + * + * @param finishedCb new finish callback to override. + */ + void cancel(@Nullable TransitionFinishedCallback finishedCb) { + mCanceled = true; + setFinishedCallback(finishedCb); + } + + void onConsumed(boolean aborted) { + if (mConsumedCallback != null) { + mConsumedCallback.onConsumed(aborted); + } + } + + void onFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + if (mFinishedCallback != null) { + mFinishedCallback.onFinished(finishWct, finishT); + } } } @@ -459,7 +506,7 @@ class SplitScreenTransitions { final @SplitScreen.StageType int mDismissTop; DismissTransition(IBinder transition, int reason, int dismissTop) { - super(transition, null /* callback */); + super(transition, null /* consumedCallback */, null /* finishedCallback */); this.mReason = reason; this.mDismissTop = dismissTop; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index e2ac01f7b003..943419bb8ea2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -226,33 +226,36 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } }; - private final SplitScreenTransitions.TransitionCallback mRecentTransitionCallback = - new SplitScreenTransitions.TransitionCallback() { - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - // Check if the recent transition is finished by returning to the current split, so we - // can restore the divider bar. - for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { - final WindowContainerTransaction.HierarchyOp op = - finishWct.getHierarchyOps().get(i); - final IBinder container = op.getContainer(); - if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() - && (mMainStage.containsContainer(container) - || mSideStage.containsContainer(container))) { - updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); - setDividerVisibility(true, finishT); - return; - } - } + private final SplitScreenTransitions.TransitionFinishedCallback + mRecentTransitionFinishedCallback = + new SplitScreenTransitions.TransitionFinishedCallback() { + @Override + public void onFinished(WindowContainerTransaction finishWct, + SurfaceControl.Transaction finishT) { + // Check if the recent transition is finished by returning to the current + // split, so we + // can restore the divider bar. + for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { + final WindowContainerTransaction.HierarchyOp op = + finishWct.getHierarchyOps().get(i); + final IBinder container = op.getContainer(); + if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() + && (mMainStage.containsContainer(container) + || mSideStage.containsContainer(container))) { + updateSurfaceBounds(mSplitLayout, finishT, + false /* applyResizingOffset */); + setDividerVisibility(true, finishT); + return; + } + } - // Dismiss the split screen if it's not returning to split. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); - setSplitsVisible(false); - setDividerVisibility(false, finishT); - logExit(EXIT_REASON_UNKNOWN); - } - }; + // Dismiss the split screen if it's not returning to split. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, finishWct); + setSplitsVisible(false); + setDividerVisibility(false, finishT); + logExit(EXIT_REASON_UNKNOWN); + } + }; protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, ShellTaskOrganizer taskOrganizer, DisplayController displayController, @@ -389,15 +392,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (ENABLE_SHELL_TRANSITIONS) { prepareEnterSplitScreen(wct); mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, - null, this, new SplitScreenTransitions.TransitionCallback() { - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } + null, this, null /* consumedCallback */, (finishWct, finishT) -> { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); } - }); + } /* finishedCallback */); } else { if (!evictWct.isEmpty()) { wct.merge(evictWct, true /* transfer */); @@ -434,28 +433,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); wct.sendPendingIntent(intent, fillInIntent, options); + + // If split screen is not activated, we're expecting to open a pair of apps to split. + final int transitType = mMainStage.isActive() + ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; prepareEnterSplitScreen(wct, null /* taskInfo */, position); - mSplitTransitions.startEnterTransition(TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, null, this, - new SplitScreenTransitions.TransitionCallback() { - @Override - public void onTransitionConsumed(boolean aborted) { - // Switch the split position if launching as MULTIPLE_TASK failed. - if (aborted - && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { - setSideStagePositionAnimated( - SplitLayout.reversePosition(mSideStagePosition)); - } + mSplitTransitions.startEnterTransition(transitType, wct, null, this, + aborted -> { + // Switch the split position if launching as MULTIPLE_TASK failed. + if (aborted && (fillInIntent.getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK) != 0) { + setSideStagePositionAnimated( + SplitLayout.reversePosition(mSideStagePosition)); } - - @Override - public void onTransitionFinished(WindowContainerTransaction finishWct, - SurfaceControl.Transaction finishT) { - if (!evictWct.isEmpty()) { - finishWct.merge(evictWct, true); - } + } /* consumedCallback */, + (finishWct, finishT) -> { + if (!evictWct.isEmpty()) { + finishWct.merge(evictWct, true); } - }); + } /* finishedCallback */); } /** Launches an activity into split by legacy transition. */ @@ -564,9 +560,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** * Starts with the second task to a split pair in one transition. * - * @param wct transaction to start the first task + * @param wct transaction to start the first task * @param instanceId if {@code null}, will not log. Otherwise it will be used in - * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} + * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} */ private void startWithTask(WindowContainerTransaction wct, int mainTaskId, @Nullable Bundle mainOptions, float splitRatio, @@ -592,7 +588,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.startTask(mainTaskId, mainOptions); mSplitTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this, null, null); setEnterInstanceId(instanceId); } @@ -639,7 +635,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } /** - * @param wct transaction to start the first task + * @param wct transaction to start the first task * @param instanceId if {@code null}, will not log. Otherwise it will be used in * {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)} */ @@ -1082,15 +1078,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, switch (exitReason) { // One of the apps doesn't support MW case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW: - // User has explicitly dragged the divider to dismiss split + // User has explicitly dragged the divider to dismiss split case EXIT_REASON_DRAG_DIVIDER: - // Either of the split apps have finished + // Either of the split apps have finished case EXIT_REASON_APP_FINISHED: - // One of the children enters PiP + // One of the children enters PiP case EXIT_REASON_CHILD_TASK_ENTER_PIP: - // One of the apps occludes lock screen. + // One of the apps occludes lock screen. case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: - // User has unlocked the device after folded + // User has unlocked the device after folded case EXIT_REASON_DEVICE_FOLDED: return true; default: @@ -1839,7 +1835,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, || activityType == ACTIVITY_TYPE_RECENTS) { // Enter overview panel, so start recent transition. mSplitTransitions.setRecentTransition(transition, request.getRemoteTransition(), - mRecentTransitionCallback); + mRecentTransitionFinishedCallback); } else if (mSplitTransitions.mPendingRecent == null) { // If split-task is not controlled by recents animation // and occluded by the other fullscreen task, dismiss both. @@ -1853,8 +1849,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // One task is appearing into split, prepare to enter split screen. out = new WindowContainerTransaction(); prepareEnterSplitScreen(out); - mSplitTransitions.setEnterTransition( - transition, request.getRemoteTransition(), null /* callback */); + mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(), + null /* consumedCallback */, null /* finishedCallback */); } } return out; @@ -1873,7 +1869,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } final @WindowManager.TransitionType int type = request.getType(); if (isSplitActive() && !isOpeningType(type) - && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) { + && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " One of the splits became " + "empty during a mixed transition (one not handled by split)," + " so make sure split-screen state is cleaned-up. " @@ -2031,17 +2027,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - // TODO(b/250853925): fallback logic. Probably start a new transition to exit split before - // applying anything here. Ideally consolidate with transition-merging. if (info.getType() == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) { if (mainChild == null && sideChild == null) { - throw new IllegalStateException("Launched a task in split, but didn't receive any" - + " task in transition."); + Log.w(TAG, "Launched a task in split, but didn't receive any task in transition."); + mSplitTransitions.mPendingEnter.cancel(null /* finishedCb */); + return true; } } else { if (mainChild == null || sideChild == null) { - throw new IllegalStateException("Launched 2 tasks in split, but didn't receive" + Log.w(TAG, "Launched 2 tasks in split, but didn't receive" + " 2 tasks in transition. Possibly one of them failed to launch"); + final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN : + (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED); + mSplitTransitions.mPendingEnter.cancel( + (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct)); + return true; } } @@ -2305,7 +2305,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; final WindowContainerTransaction wct = new WindowContainerTransaction(); prepareExitSplitScreen(stageType, wct); - mSplitTransitions.startDismissTransition(wct,StageCoordinator.this, stageType, + mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType, EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); mSplitUnsupportedToast.show(); } 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 394d6f6bf731..63d31cde4715 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 @@ -122,6 +122,8 @@ public class Transitions implements RemoteCallable<Transitions> { private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); + private boolean mIsRegistered = false; + /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>(); @@ -163,19 +165,18 @@ public class Transitions implements RemoteCallable<Transitions> { displayController, pool, mainExecutor, mainHandler, animExecutor); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellController = shellController; - shellInit.addInitCallback(this::onInit, this); - } - - private void onInit() { - mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, - this::createExternalInterface, this); - // The very last handler (0 in the list) should be the default one. mHandlers.add(mDefaultTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default"); // Next lowest priority is remote transitions. mHandlers.add(mRemoteTransitionHandler); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS, + this::createExternalInterface, this); ContentResolver resolver = mContext.getContentResolver(); mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); @@ -186,13 +187,23 @@ public class Transitions implements RemoteCallable<Transitions> { new SettingsObserver()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mIsRegistered = true; // Register this transition handler with Core - mOrganizer.registerTransitionPlayer(mPlayerImpl); + try { + mOrganizer.registerTransitionPlayer(mPlayerImpl); + } catch (RuntimeException e) { + mIsRegistered = false; + throw e; + } // Pre-load the instance. TransitionMetrics.getInstance(); } } + public boolean isRegistered() { + return mIsRegistered; + } + private float getTransitionAnimationScaleSetting() { return fixScale(Settings.Global.getFloat(mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index ea0033ba4bbb..652f9b38c88f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -181,7 +181,7 @@ public class SplitTransitionTests extends ShellTestCase { IBinder transition = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(testRemote), mStageCoordinator, null); + new RemoteTransition(testRemote), mStageCoordinator, null, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, @@ -421,7 +421,7 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo enterInfo = createEnterPairInfo(); IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null); + new RemoteTransition(new TestRemoteTransition()), mStageCoordinator, null, null); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, diff --git a/location/java/android/location/GnssCapabilities.java b/location/java/android/location/GnssCapabilities.java index 7a412a0ed2de..b38f9ea39136 100644 --- a/location/java/android/location/GnssCapabilities.java +++ b/location/java/android/location/GnssCapabilities.java @@ -207,7 +207,7 @@ public final class GnssCapabilities implements Parcelable { /** * Returns {@code true} if GNSS chipset supports single shot locating, {@code false} otherwise. */ - public boolean hasSingleShot() { + public boolean hasSingleShotFix() { return (mTopFlags & TOP_HAL_CAPABILITY_SINGLE_SHOT) != 0; } @@ -482,7 +482,7 @@ public final class GnssCapabilities implements Parcelable { if (hasMsa()) { builder.append("MSA "); } - if (hasSingleShot()) { + if (hasSingleShotFix()) { builder.append("SINGLE_SHOT "); } if (hasOnDemandTime()) { @@ -602,7 +602,7 @@ public final class GnssCapabilities implements Parcelable { /** * Sets single shot locating capability. */ - public @NonNull Builder setHasSingleShot(boolean capable) { + public @NonNull Builder setHasSingleShotFix(boolean capable) { mTopFlags = setFlag(mTopFlags, TOP_HAL_CAPABILITY_SINGLE_SHOT, capable); return this; } diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java index 23390fce1a5f..09f40e80f885 100644 --- a/location/java/android/location/GnssStatus.java +++ b/location/java/android/location/GnssStatus.java @@ -199,15 +199,15 @@ public final class GnssStatus implements Parcelable { * <li>93-106 as the frequency channel number (FCN) (-7 to +6) plus 100. * i.e. encode FCN of -7 as 93, 0 as 100, and +6 as 106</li> * </ul></li> - * <li>QZSS: 193-200</li> + * <li>QZSS: 183-206</li> * <li>Galileo: 1-36</li> - * <li>Beidou: 1-37</li> + * <li>Beidou: 1-63</li> * <li>IRNSS: 1-14</li> * </ul> * * @param satelliteIndex An index from zero to {@link #getSatelliteCount()} - 1 */ - @IntRange(from = 1, to = 200) + @IntRange(from = 1, to = 206) public int getSvid(@IntRange(from = 0) int satelliteIndex) { return mSvidWithFlags[satelliteIndex] >> SVID_SHIFT_WIDTH; } diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml index 428f2dc2eb35..2000d9675ca4 100644 --- a/packages/CompanionDeviceManager/res/values/styles.xml +++ b/packages/CompanionDeviceManager/res/values/styles.xml @@ -49,7 +49,6 @@ <style name="DescriptionSummary"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> - <item name="android:gravity">center</item> <item name="android:layout_marginTop">18dp</item> <item name="android:layout_marginLeft">18dp</item> <item name="android:layout_marginRight">18dp</item> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt new file mode 100644 index 000000000000..d4341b498fe0 --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/ActionUi.kt @@ -0,0 +1,53 @@ +/* + * 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.credentialmanager.jetpack + +import android.app.slice.Slice +import android.credentials.ui.Entry +import android.graphics.drawable.Icon + +/** + * UI representation for a credential entry used during the get credential flow. + * + * TODO: move to jetpack. + */ +class ActionUi( + val icon: Icon, + val text: CharSequence, + val subtext: CharSequence?, +) { + companion object { + fun fromSlice(slice: Slice): ActionUi { + var icon: Icon? = null + var text: CharSequence? = null + var subtext: CharSequence? = null + + val items = slice.items + items.forEach { + if (it.hasHint(Entry.HINT_ACTION_ICON)) { + icon = it.icon + } else if (it.hasHint(Entry.HINT_ACTION_TITLE)) { + text = it.text + } else if (it.hasHint(Entry.HINT_ACTION_SUBTEXT)) { + subtext = it.text + } + } + // TODO: fail NPE more elegantly. + return ActionUi(icon!!, text!!, subtext) + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index eb53ea1d44f7..950ee21ae7b5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -758,23 +758,16 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public boolean isBusy() { - for (CachedBluetoothDevice memberDevice : getMemberDevice()) { - if (isBusyState(memberDevice)) { - return true; - } - } - return isBusyState(this); - } - - private boolean isBusyState(CachedBluetoothDevice device){ - for (LocalBluetoothProfile profile : device.getProfiles()) { - int status = device.getProfileConnectionState(profile); - if (status == BluetoothProfile.STATE_CONNECTING - || status == BluetoothProfile.STATE_DISCONNECTING) { - return true; + synchronized (mProfileLock) { + for (LocalBluetoothProfile profile : mProfiles) { + int status = getProfileConnectionState(profile); + if (status == BluetoothProfile.STATE_CONNECTING + || status == BluetoothProfile.STATE_DISCONNECTING) { + return true; + } } + return getBondState() == BluetoothDevice.BOND_BONDING; } - return device.getBondState() == BluetoothDevice.BOND_BONDING; } private boolean updateProfiles() { @@ -920,7 +913,14 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> @Override public String toString() { - return mDevice.toString(); + return "CachedBluetoothDevice (" + + "anonymizedAddress=" + + mDevice.getAnonymizedAddress() + + ", name=" + + getName() + + ", groupId=" + + mGroupId + + ")"; } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 8a9f9dd9c3fb..fb861da0a7f0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -231,7 +231,7 @@ public class LocalBluetoothProfileManager { if (DEBUG) { Log.d(TAG, "Adding local Volume Control profile"); } - mVolumeControlProfile = new VolumeControlProfile(); + mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this); // Note: no event handler for VCP, only for being connectable. mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile); } @@ -553,6 +553,10 @@ public class LocalBluetoothProfileManager { return mCsipSetCoordinatorProfile; } + public VolumeControlProfile getVolumeControlProfile() { + return mVolumeControlProfile; + } + /** * Fill in a list of LocalBluetoothProfile objects that are supported by * the local device and the remote device. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java index 511df282ea4b..57867be53bb9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java @@ -16,18 +16,91 @@ package com.android.settingslib.bluetooth; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED; +import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothVolumeControl; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.RequiresApi; /** * VolumeControlProfile handles Bluetooth Volume Control Controller role */ public class VolumeControlProfile implements LocalBluetoothProfile { private static final String TAG = "VolumeControlProfile"; + private static boolean DEBUG = true; static final String NAME = "VCP"; // Order of this profile in device profiles list - private static final int ORDINAL = 23; + private static final int ORDINAL = 1; + + private Context mContext; + private final CachedBluetoothDeviceManager mDeviceManager; + private final LocalBluetoothProfileManager mProfileManager; + + private BluetoothVolumeControl mService; + private boolean mIsProfileReady; + + // These callbacks run on the main thread. + private final class VolumeControlProfileServiceListener + implements BluetoothProfile.ServiceListener { + + @RequiresApi(Build.VERSION_CODES.S) + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service connected"); + } + mService = (BluetoothVolumeControl) proxy; + // We just bound to the service, so refresh the UI for any connected + // VolumeControlProfile devices. + List<BluetoothDevice> deviceList = mService.getConnectedDevices(); + while (!deviceList.isEmpty()) { + BluetoothDevice nextDevice = deviceList.remove(0); + CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice); + // we may add a new device here, but generally this should not happen + if (device == null) { + if (DEBUG) { + Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice); + } + device = mDeviceManager.addDevice(nextDevice); + } + device.onProfileStateChanged(VolumeControlProfile.this, + BluetoothProfile.STATE_CONNECTED); + device.refresh(); + } + + mProfileManager.callServiceConnectedListeners(); + mIsProfileReady = true; + } + + public void onServiceDisconnected(int profile) { + if (DEBUG) { + Log.d(TAG, "Bluetooth service disconnected"); + } + mProfileManager.callServiceDisconnectedListeners(); + mIsProfileReady = false; + } + } + + VolumeControlProfile(Context context, CachedBluetoothDeviceManager deviceManager, + LocalBluetoothProfileManager profileManager) { + mContext = context; + mDeviceManager = deviceManager; + mProfileManager = profileManager; + + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new VolumeControlProfile.VolumeControlProfileServiceListener(), + BluetoothProfile.VOLUME_CONTROL); + } @Override public boolean accessProfileEnabled() { @@ -39,29 +112,70 @@ public class VolumeControlProfile implements LocalBluetoothProfile { return true; } + /** + * Get VolumeControlProfile devices matching connection states{ + * + * @return Matching device list + * @code BluetoothProfile.STATE_CONNECTED, + * @code BluetoothProfile.STATE_CONNECTING, + * @code BluetoothProfile.STATE_DISCONNECTING} + */ + public List<BluetoothDevice> getConnectedDevices() { + if (mService == null) { + return new ArrayList<BluetoothDevice>(0); + } + return mService.getDevicesMatchingConnectionStates( + new int[]{BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_DISCONNECTING}); + } + @Override public int getConnectionStatus(BluetoothDevice device) { - return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle VCP + if (mService == null) { + return BluetoothProfile.STATE_DISCONNECTED; + } + return mService.getConnectionState(device); } @Override public boolean isEnabled(BluetoothDevice device) { - return false; + if (mService == null || device == null) { + return false; + } + return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN; } @Override public int getConnectionPolicy(BluetoothDevice device) { - return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle VCP + if (mService == null || device == null) { + return CONNECTION_POLICY_FORBIDDEN; + } + return mService.getConnectionPolicy(device); } @Override public boolean setEnabled(BluetoothDevice device, boolean enabled) { - return false; + boolean isSuccessful = false; + if (mService == null || device == null) { + return false; + } + if (DEBUG) { + Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled); + } + if (enabled) { + if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) { + isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED); + } + } else { + isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN); + } + + return isSuccessful; } @Override public boolean isProfileReady() { - return true; + return mIsProfileReady; } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index 1606540da3fd..2614644feb20 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -92,7 +92,8 @@ public class DreamBackend { COMPLICATION_TYPE_AIR_QUALITY, COMPLICATION_TYPE_CAST_INFO, COMPLICATION_TYPE_HOME_CONTROLS, - COMPLICATION_TYPE_SMARTSPACE + COMPLICATION_TYPE_SMARTSPACE, + COMPLICATION_TYPE_MEDIA_ENTRY }) @Retention(RetentionPolicy.SOURCE) public @interface ComplicationType { @@ -105,6 +106,7 @@ public class DreamBackend { public static final int COMPLICATION_TYPE_CAST_INFO = 5; public static final int COMPLICATION_TYPE_HOME_CONTROLS = 6; public static final int COMPLICATION_TYPE_SMARTSPACE = 7; + public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8; private final Context mContext; private final IDreamManager mDreamManager; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index 315ab0aac878..79e99387b2fa 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -1069,80 +1069,4 @@ public class CachedBluetoothDeviceTest { assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice); assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); } - - @Test - public void isBusy_mainDeviceIsConnecting_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_mainDeviceIsBonding_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_memberDeviceIsConnecting_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_memberDeviceIsBonding_returnsBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isTrue(); - } - - @Test - public void isBusy_allDevicesAreNotBusy_returnsNotBusy() { - mCachedDevice.addMemberDevice(mSubCachedDevice); - updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); - - assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue(); - assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue(); - assertThat(mCachedDevice.isBusy()).isFalse(); - } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index ccbfac226c46..fa96a2f0ee7f 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -5533,13 +5533,17 @@ public class SettingsProvider extends ContentProvider { } if (currentVersion == 210) { final SettingsState secureSettings = getSecureSettingsLocked(userId); - final int defaultValueVibrateIconEnabled = getContext().getResources() - .getInteger(R.integer.def_statusBarVibrateIconEnabled); - secureSettings.insertSettingOverrideableByRestoreLocked( - Secure.STATUS_BAR_SHOW_VIBRATE_ICON, - String.valueOf(defaultValueVibrateIconEnabled), - null /* tag */, true /* makeDefault */, - SettingsState.SYSTEM_PACKAGE_NAME); + final Setting currentSetting = secureSettings.getSettingLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON); + if (currentSetting.isNull()) { + final int defaultValueVibrateIconEnabled = getContext().getResources() + .getInteger(R.integer.def_statusBarVibrateIconEnabled); + secureSettings.insertSettingOverrideableByRestoreLocked( + Secure.STATUS_BAR_SHOW_VIBRATE_ICON, + String.valueOf(defaultValueVibrateIconEnabled), + null /* tag */, true /* makeDefault */, + SettingsState.SYSTEM_PACKAGE_NAME); + } currentVersion = 211; } // vXXX: Add new settings above this point. diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index b5145f926abd..4267ba2ff0b7 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -289,6 +289,12 @@ <!-- Query all packages on device on R+ --> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <queries> + <intent> + <action android:name="android.intent.action.NOTES" /> + </intent> + </queries> + <!-- Permission to register process observer --> <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/> diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md index 38d636d7ff82..95b986faebb4 100644 --- a/packages/SystemUI/docs/device-entry/quickaffordance.md +++ b/packages/SystemUI/docs/device-entry/quickaffordance.md @@ -8,7 +8,7 @@ credit card, etc. ### Step 1: create a new quick affordance config * Create a new class under the [systemui/keyguard/domain/quickaffordance](../../src/com/android/systemui/keyguard/domain/quickaffordance) directory * Please make sure that the class is injected through the Dagger dependency injection system by using the `@Inject` annotation on its main constructor and the `@SysUISingleton` annotation at class level, to make sure only one instance of the class is ever instantiated -* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes: +* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes: * The `state` Flow property must emit `State.Hidden` when the feature is not enabled! * It is safe to assume that `onQuickAffordanceClicked` will not be invoked if-and-only-if the previous rule is followed * When implementing `onQuickAffordanceClicked`, the implementation can do something or it can ask the framework to start an activity using an `Intent` provided by the implementation diff --git a/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml new file mode 100644 index 000000000000..de83df4e625c --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_floating_message_background.xml @@ -0,0 +1,22 @@ +<!-- + ~ 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="@color/accessibility_floating_menu_message_background"/> + <corners android:radius="28dp"/> +</shape> diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml index 3bcc37a478c9..e2ce34f5008e 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPasswordView +<com.android.systemui.biometrics.ui.CredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -86,4 +86,4 @@ </LinearLayout> -</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index a3dd334bd667..6e0e38b95ee5 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPatternView +<com.android.systemui.biometrics.ui.CredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -83,4 +83,4 @@ </FrameLayout> -</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPatternView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_password_view.xml b/packages/SystemUI/res/layout/auth_credential_password_view.xml index 774b335f913e..021ebe6e7bff 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPasswordView +<com.android.systemui.biometrics.ui.CredentialPasswordView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -83,4 +83,4 @@ </LinearLayout> -</com.android.systemui.biometrics.AuthCredentialPasswordView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPasswordView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 4af997017bba..891c6af4b667 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<com.android.systemui.biometrics.AuthCredentialPatternView +<com.android.systemui.biometrics.ui.CredentialPatternView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" @@ -78,4 +78,4 @@ android:layout_gravity="center_horizontal|bottom"/> </FrameLayout> -</com.android.systemui.biometrics.AuthCredentialPatternView>
\ No newline at end of file +</com.android.systemui.biometrics.ui.CredentialPatternView> diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index dc2bee56373c..16152f80308a 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -99,6 +99,8 @@ <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% --> + <color name="accessibility_floating_menu_message_background">@*android:color/background_material_dark</color> + <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_dark</color> <color name="people_tile_background">@color/material_dynamic_secondary20</color> </resources> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 9e8bef06270b..55b59b63c2f9 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -219,6 +219,8 @@ <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#CCFFFFFF</color> <!-- 80% --> <color name="accessibility_floating_menu_stroke_dark">#26FFFFFF</color> <!-- 15% --> + <color name="accessibility_floating_menu_message_background">@*android:color/background_material_light</color> + <color name="accessibility_floating_menu_message_text">@*android:color/primary_text_default_material_light</color> <!-- Wallet screen --> <color name="wallet_card_border">#33FFFFFF</color> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 9188ce091a3b..93982cb2c5b9 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -643,6 +643,18 @@ <item>26</item> <!-- MOUTH_COVERING_DETECTED --> </integer-array> + <!-- Which device wake-ups will trigger face auth. These values correspond with + PowerManager#WakeReason. --> + <integer-array name="config_face_auth_wake_up_triggers"> + <item>1</item> <!-- WAKE_REASON_POWER_BUTTON --> + <item>4</item> <!-- WAKE_REASON_GESTURE --> + <item>6</item> <!-- WAKE_REASON_WAKE_KEY --> + <item>7</item> <!-- WAKE_REASON_WAKE_MOTION --> + <item>9</item> <!-- WAKE_REASON_LID --> + <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED --> + <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE --> + </integer-array> + <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 66f0e7543469..f02f29a4f741 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1336,6 +1336,14 @@ <dimen name="accessibility_floating_menu_large_single_radius">35dp</dimen> <dimen name="accessibility_floating_menu_large_multiple_radius">35dp</dimen> + <dimen name="accessibility_floating_menu_message_container_horizontal_padding">15dp</dimen> + <dimen name="accessibility_floating_menu_message_text_vertical_padding">8dp</dimen> + <dimen name="accessibility_floating_menu_message_margin">8dp</dimen> + <dimen name="accessibility_floating_menu_message_elevation">5dp</dimen> + <dimen name="accessibility_floating_menu_message_text_size">14sp</dimen> + <dimen name="accessibility_floating_menu_message_min_width">312dp</dimen> + <dimen name="accessibility_floating_menu_message_min_height">48dp</dimen> + <dimen name="accessibility_floating_tooltip_arrow_width">8dp</dimen> <dimen name="accessibility_floating_tooltip_arrow_height">16dp</dimen> <dimen name="accessibility_floating_tooltip_arrow_margin">-2dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 7ca42f7d7015..4fd25a98a71c 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -177,6 +177,7 @@ <item type="id" name="action_move_bottom_right"/> <item type="id" name="action_move_to_edge_and_hide"/> <item type="id" name="action_move_out_edge_and_show"/> + <item type="id" name="action_remove_menu"/> <!-- rounded corner view id --> <item type="id" name="rounded_corner_top_left"/> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 44031bb99751..b325c56adefc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2197,6 +2197,15 @@ <string name="accessibility_floating_button_migration_tooltip">Tap to open accessibility features. Customize or replace this button in Settings.\n\n<annotation id="link">View settings</annotation></string> <!-- Message for the accessibility floating button docking tooltip. It shows when the user first time drag the button. It will tell the user about docking behavior. [CHAR LIMIT=70] --> <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string> + <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_undo">Undo</string> + + <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_undo_message_text">{count, plural, + =1 {{label} shortcut removed} + other {# shortcuts removed} + }</string> + <!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] --> <string name="accessibility_floating_button_action_move_top_left">Move top left</string> <!-- Action in accessibility menu to move the accessibility floating button to the top right of the screen. [CHAR LIMIT=30] --> @@ -2209,6 +2218,8 @@ <string name="accessibility_floating_button_action_move_to_edge_and_hide_to_half">Move to edge and hide</string> <!-- Action in accessibility menu to move the accessibility floating button out the edge and show. [CHAR LIMIT=36]--> <string name="accessibility_floating_button_action_move_out_edge_and_show">Move out edge and show</string> + <!-- Action in accessibility menu to remove the accessibility floating menu view on the screen. [CHAR LIMIT=36]--> + <string name="accessibility_floating_button_action_remove_menu">Remove</string> <!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]--> <string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java index 7e42e1b89b1f..8ac1de898be8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java @@ -85,13 +85,12 @@ public class PipSurfaceTransactionHelper { mTmpSourceRectF.set(sourceBounds); mTmpDestinationRect.set(sourceBounds); mTmpDestinationRect.inset(insets); - // Scale by the shortest edge and offset such that the top/left of the scaled inset - // source rect aligns with the top/left of the destination bounds + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds final float scale; if (sourceRectHint.isEmpty() || sourceRectHint.width() == sourceBounds.width()) { - scale = sourceBounds.width() <= sourceBounds.height() - ? (float) destinationBounds.width() / sourceBounds.width() - : (float) destinationBounds.height() / sourceBounds.height(); + scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); } else { // scale by sourceRectHint if it's not edge-to-edge final float endScale = sourceRectHint.width() <= sourceRectHint.height() diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 386c09575a1a..40a96b060bc0 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -30,6 +30,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.DOZING_MIGRATION_1 import com.android.systemui.flags.Flags.REGION_SAMPLING import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -221,8 +222,11 @@ open class ClockEventController @Inject constructor( disposableHandle = parent.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) - listenForDozeAmount(this) - listenForDozeAmountTransition(this) + if (featureFlags.isEnabled(DOZING_MIGRATION_1)) { + listenForDozeAmountTransition(this) + } else { + listenForDozeAmount(this) + } } } } @@ -265,10 +269,9 @@ open class ClockEventController @Inject constructor( @VisibleForTesting internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { return scope.launch { - keyguardTransitionInteractor.aodToLockscreenTransition.collect { - // Would eventually run this: - // dozeAmount = it.value - // clock?.animations?.doze(dozeAmount) + keyguardTransitionInteractor.dozeAmountTransition.collect { + dozeAmount = it.value + clock?.animations?.doze(dozeAmount) } } } diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index 6fcb6f55dc20..4a41b3fe2589 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -17,6 +17,7 @@ package com.android.keyguard import android.annotation.StringDef +import android.os.PowerManager import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED @@ -122,122 +123,93 @@ private object InternalFaceAuthReasons { "Face auth started/stopped because biometric is enabled on keyguard" } -/** UiEvents that are logged to identify why face auth is being triggered. */ -enum class FaceAuthUiEvent constructor(private val id: Int, val reason: String) : +/** + * UiEvents that are logged to identify why face auth is being triggered. + * @param extraInfo is logged as the position. See [UiEventLogger#logWithInstanceIdAndPosition] + */ +enum class FaceAuthUiEvent +constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : UiEventLogger.UiEventEnum { @UiEvent(doc = OCCLUDING_APP_REQUESTED) FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED(1146, OCCLUDING_APP_REQUESTED), - @UiEvent(doc = UDFPS_POINTER_DOWN) FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN(1147, UDFPS_POINTER_DOWN), - @UiEvent(doc = SWIPE_UP_ON_BOUNCER) FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER(1148, SWIPE_UP_ON_BOUNCER), - @UiEvent(doc = DEVICE_WOKEN_UP_ON_REACH_GESTURE) FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD(1149, DEVICE_WOKEN_UP_ON_REACH_GESTURE), - @UiEvent(doc = FACE_LOCKOUT_RESET) FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET(1150, FACE_LOCKOUT_RESET), - - @UiEvent(doc = QS_EXPANDED) - FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED), - + @UiEvent(doc = QS_EXPANDED) FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED), @UiEvent(doc = NOTIFICATION_PANEL_CLICKED) FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED(1152, NOTIFICATION_PANEL_CLICKED), - @UiEvent(doc = PICK_UP_GESTURE_TRIGGERED) FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED(1153, PICK_UP_GESTURE_TRIGGERED), - @UiEvent(doc = ALTERNATE_BIOMETRIC_BOUNCER_SHOWN) - FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, - ALTERNATE_BIOMETRIC_BOUNCER_SHOWN), - + FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, ALTERNATE_BIOMETRIC_BOUNCER_SHOWN), @UiEvent(doc = PRIMARY_BOUNCER_SHOWN) FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN(1155, PRIMARY_BOUNCER_SHOWN), - @UiEvent(doc = PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN) FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN( 1197, PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN ), - @UiEvent(doc = RETRY_AFTER_HW_UNAVAILABLE) FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE(1156, RETRY_AFTER_HW_UNAVAILABLE), - - @UiEvent(doc = TRUST_DISABLED) - FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED), - - @UiEvent(doc = TRUST_ENABLED) - FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED), - + @UiEvent(doc = TRUST_DISABLED) FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED), + @UiEvent(doc = TRUST_ENABLED) FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED), @UiEvent(doc = KEYGUARD_OCCLUSION_CHANGED) FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED(1159, KEYGUARD_OCCLUSION_CHANGED), - @UiEvent(doc = ASSISTANT_VISIBILITY_CHANGED) FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED(1160, ASSISTANT_VISIBILITY_CHANGED), - @UiEvent(doc = STARTED_WAKING_UP) - FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP), - + FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP) { + override fun extraInfoToString(): String { + return PowerManager.wakeReasonToString(extraInfo) + } + }, + @Deprecated( + "Not a face auth trigger.", + ReplaceWith( + "FACE_AUTH_UPDATED_STARTED_WAKING_UP, " + + "extraInfo=PowerManager.WAKE_REASON_DREAM_FINISHED" + ) + ) @UiEvent(doc = DREAM_STOPPED) FACE_AUTH_TRIGGERED_DREAM_STOPPED(1162, DREAM_STOPPED), - @UiEvent(doc = ALL_AUTHENTICATORS_REGISTERED) FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED(1163, ALL_AUTHENTICATORS_REGISTERED), - @UiEvent(doc = ENROLLMENTS_CHANGED) FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED(1164, ENROLLMENTS_CHANGED), - @UiEvent(doc = KEYGUARD_VISIBILITY_CHANGED) FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED(1165, KEYGUARD_VISIBILITY_CHANGED), - @UiEvent(doc = FACE_CANCEL_NOT_RECEIVED) FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED(1174, FACE_CANCEL_NOT_RECEIVED), - @UiEvent(doc = AUTH_REQUEST_DURING_CANCELLATION) FACE_AUTH_TRIGGERED_DURING_CANCELLATION(1175, AUTH_REQUEST_DURING_CANCELLATION), - - @UiEvent(doc = DREAM_STARTED) - FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED), - - @UiEvent(doc = FP_LOCKED_OUT) - FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT), - + @UiEvent(doc = DREAM_STARTED) FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED), + @UiEvent(doc = FP_LOCKED_OUT) FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT), @UiEvent(doc = FACE_AUTH_STOPPED_ON_USER_INPUT) FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER(1178, FACE_AUTH_STOPPED_ON_USER_INPUT), - @UiEvent(doc = KEYGUARD_GOING_AWAY) FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY(1179, KEYGUARD_GOING_AWAY), - - @UiEvent(doc = CAMERA_LAUNCHED) - FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED), - - @UiEvent(doc = FP_AUTHENTICATED) - FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED), - - @UiEvent(doc = GOING_TO_SLEEP) - FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP), - + @UiEvent(doc = CAMERA_LAUNCHED) FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED), + @UiEvent(doc = FP_AUTHENTICATED) FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED), + @UiEvent(doc = GOING_TO_SLEEP) FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP), @UiEvent(doc = FINISHED_GOING_TO_SLEEP) FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP(1183, FINISHED_GOING_TO_SLEEP), - - @UiEvent(doc = KEYGUARD_INIT) - FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT), - - @UiEvent(doc = KEYGUARD_RESET) - FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET), - - @UiEvent(doc = USER_SWITCHING) - FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING), - + @UiEvent(doc = KEYGUARD_INIT) FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT), + @UiEvent(doc = KEYGUARD_RESET) FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET), + @UiEvent(doc = USER_SWITCHING) FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING), @UiEvent(doc = FACE_AUTHENTICATED) FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED(1187, FACE_AUTHENTICATED), - @UiEvent(doc = BIOMETRIC_ENABLED) FACE_AUTH_UPDATED_BIOMETRIC_ENABLED_ON_KEYGUARD(1188, BIOMETRIC_ENABLED); override fun getId(): Int = this.id + + /** Convert [extraInfo] to a human-readable string. By default, this is empty. */ + open fun extraInfoToString(): String = "" } private val apiRequestReasonToUiEvent = diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt new file mode 100644 index 000000000000..a0c43fba4bc1 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt @@ -0,0 +1,76 @@ +/* + * 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.keyguard + +import android.content.res.Resources +import android.os.Build +import android.os.PowerManager +import com.android.systemui.Dumpable +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.settings.GlobalSettings +import java.io.PrintWriter +import java.util.stream.Collectors +import javax.inject.Inject + +/** Determines which device wake-ups should trigger face authentication. */ +@SysUISingleton +class FaceWakeUpTriggersConfig +@Inject +constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpManager: DumpManager) : + Dumpable { + private val defaultTriggerFaceAuthOnWakeUpFrom: Set<Int> = + resources.getIntArray(R.array.config_face_auth_wake_up_triggers).toSet() + private val triggerFaceAuthOnWakeUpFrom: Set<Int> + + init { + triggerFaceAuthOnWakeUpFrom = + if (Build.IS_DEBUGGABLE) { + // Update face wake triggers via adb on debuggable builds: + // ie: adb shell settings put global face_wake_triggers "1\|4" && + // adb shell am crash com.android.systemui + processStringArray( + globalSettings.getString("face_wake_triggers"), + defaultTriggerFaceAuthOnWakeUpFrom + ) + } else { + defaultTriggerFaceAuthOnWakeUpFrom + } + dumpManager.registerDumpable(this) + } + + fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean { + return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("FaceWakeUpTriggers:") + for (pmWakeReason in triggerFaceAuthOnWakeUpFrom) { + pw.println(" ${PowerManager.wakeReasonToString(pmWakeReason)}") + } + } + + /** Convert a pipe-separated set of integers into a set of ints. */ + private fun processStringArray(stringSetting: String?, default: Set<Int>): Set<Int> { + return stringSetting?.let { + stringSetting.split("|").stream().map(Integer::parseInt).collect(Collectors.toSet()) + } + ?: default + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index aff9dcbc26e3..cc1f2fe5b027 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -44,7 +44,6 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_TRUST_ENABL import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN; -import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DREAM_STOPPED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DURING_CANCELLATION; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET; @@ -287,6 +286,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } }; + private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; HashMap<Integer, SimData> mSimDatas = new HashMap<>(); HashMap<Integer, ServiceState> mServiceStates = new HashMap<>(); @@ -1807,11 +1807,21 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - protected void handleStartedWakingUp() { + protected void handleStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) { Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp"); Assert.isMainThread(); - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_STARTED_WAKING_UP); - requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp"); + + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); + if (mFaceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(pmWakeReason)) { + FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason); + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_STARTED_WAKING_UP); + requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - " + + PowerManager.wakeReasonToString(pmWakeReason)); + } else { + mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason); + } + for (int i = 0; i < mCallbacks.size(); i++) { KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); if (cb != null) { @@ -1863,12 +1873,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onDreamingStateChanged(mIsDreaming); } } + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); if (mIsDreaming) { - updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_DREAM_STARTED); - } else { - updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, - FACE_AUTH_TRIGGERED_DREAM_STOPPED); } } @@ -1948,7 +1955,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab PackageManager packageManager, @Nullable FaceManager faceManager, @Nullable FingerprintManager fingerprintManager, - @Nullable BiometricManager biometricManager) { + @Nullable BiometricManager biometricManager, + FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) { mContext = context; mSubscriptionManager = subscriptionManager; mTelephonyListenerManager = telephonyListenerManager; @@ -1987,6 +1995,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab R.array.config_face_acquire_device_entry_ignorelist)) .boxed() .collect(Collectors.toSet()); + mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig; mHandler = new Handler(mainLooper) { @Override @@ -2036,7 +2045,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab break; case MSG_STARTED_WAKING_UP: Trace.beginSection("KeyguardUpdateMonitor#handler MSG_STARTED_WAKING_UP"); - handleStartedWakingUp(); + handleStartedWakingUp(msg.arg1); Trace.endSection(); break; case MSG_SIM_SUBSCRIPTION_INFO_CHANGED: @@ -2227,8 +2236,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void updateFaceEnrolled(int userId) { mIsFaceEnrolled = whitelistIpcs( () -> mFaceManager != null && mFaceManager.isHardwareDetected() - && mFaceManager.hasEnrolledTemplates(userId) - && mBiometricEnabledForUser.get(userId)); + && mBiometricEnabledForUser.get(userId)) + && mAuthController.isFaceAuthEnrolled(userId); } public boolean isFaceSupported() { @@ -2784,8 +2793,14 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // Waiting for ERROR_CANCELED before requesting auth again return; } - mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason()); - mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId()); + mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent); + mUiEventLogger.logWithInstanceIdAndPosition( + faceAuthUiEvent, + 0, + null, + getKeyguardSessionId(), + faceAuthUiEvent.getExtraInfo() + ); if (unlockPossible) { mFaceCancelSignal = new CancellationSignal(); @@ -3564,11 +3579,16 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab // TODO: use these callbacks elsewhere in place of the existing notifyScreen*() // (KeyguardViewMediator, KeyguardHostView) - public void dispatchStartedWakingUp() { + /** + * Dispatch wakeup events to: + * - update biometric listening states + * - send to registered KeyguardUpdateMonitorCallbacks + */ + public void dispatchStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) { synchronized (this) { mDeviceInteractive = true; } - mHandler.sendEmptyMessage(MSG_STARTED_WAKING_UP); + mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_WAKING_UP, pmWakeReason, 0)); } public void dispatchStartedGoingToSleep(int why) { diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 70758dfec932..8fbbd3840964 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -24,6 +24,8 @@ import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; import static com.android.systemui.classifier.Classifier.LOCK_ICON; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.res.Configuration; import android.content.res.Resources; @@ -46,6 +48,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.android.systemui.Dumpable; @@ -55,6 +58,10 @@ import com.android.systemui.biometrics.AuthRippleController; import com.android.systemui.biometrics.UdfpsController; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; @@ -67,6 +74,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor; import java.io.PrintWriter; import java.util.Objects; +import java.util.function.Consumer; import javax.inject.Inject; @@ -103,6 +111,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull private CharSequence mLockedLabel; @NonNull private final VibratorHelper mVibrator; @Nullable private final AuthRippleController mAuthRippleController; + @NonNull private final FeatureFlags mFeatureFlags; + @NonNull private final KeyguardTransitionInteractor mTransitionInteractor; + @NonNull private final KeyguardInteractor mKeyguardInteractor; // Tracks the velocity of a touch to help filter out the touches that move too fast. private VelocityTracker mVelocityTracker; @@ -139,6 +150,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme private boolean mDownDetected; private final Rect mSensorTouchLocation = new Rect(); + @VisibleForTesting + final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> { + mInterpolatedDarkAmount = step.getValue(); + mView.setDozeAmount(step.getValue()); + updateBurnInOffsets(); + }; + + @VisibleForTesting + final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + }; + @Inject public LockIconViewController( @Nullable LockIconView view, @@ -154,7 +179,10 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @NonNull @Main DelayableExecutor executor, @NonNull VibratorHelper vibrator, @Nullable AuthRippleController authRippleController, - @NonNull @Main Resources resources + @NonNull @Main Resources resources, + @NonNull KeyguardTransitionInteractor transitionInteractor, + @NonNull KeyguardInteractor keyguardInteractor, + @NonNull FeatureFlags featureFlags ) { super(view); mStatusBarStateController = statusBarStateController; @@ -168,6 +196,9 @@ public class LockIconViewController extends ViewController<LockIconView> impleme mExecutor = executor; mVibrator = vibrator; mAuthRippleController = authRippleController; + mTransitionInteractor = transitionInteractor; + mKeyguardInteractor = keyguardInteractor; + mFeatureFlags = featureFlags; mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x); mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y); @@ -184,6 +215,12 @@ public class LockIconViewController extends ViewController<LockIconView> impleme @Override protected void onInit() { mView.setAccessibilityDelegate(mAccessibilityDelegate); + + if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(), + mDozeTransitionCallback); + collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback); + } } @Override @@ -379,14 +416,17 @@ public class LockIconViewController extends ViewController<LockIconView> impleme pw.println(" mShowUnlockIcon: " + mShowUnlockIcon); pw.println(" mShowLockIcon: " + mShowLockIcon); pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon); - pw.println(" mIsDozing: " + mIsDozing); - pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); - pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); - pw.println(" mRunningFPS: " + mRunningFPS); - pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); - pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); - pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); - pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); + pw.println(); + pw.println(" mIsDozing: " + mIsDozing); + pw.println(" isFlagEnabled(DOZING_MIGRATION_1): " + + mFeatureFlags.isEnabled(DOZING_MIGRATION_1)); + pw.println(" mIsBouncerShowing: " + mIsBouncerShowing); + pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric); + pw.println(" mRunningFPS: " + mRunningFPS); + pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen); + pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState)); + pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount); + pw.println(" mSensorTouchLocation: " + mSensorTouchLocation); if (mView != null) { mView.dump(pw, args); @@ -427,16 +467,20 @@ public class LockIconViewController extends ViewController<LockIconView> impleme new StatusBarStateController.StateListener() { @Override public void onDozeAmountChanged(float linear, float eased) { - mInterpolatedDarkAmount = eased; - mView.setDozeAmount(eased); - updateBurnInOffsets(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mInterpolatedDarkAmount = eased; + mView.setDozeAmount(eased); + updateBurnInOffsets(); + } } @Override public void onDozingChanged(boolean isDozing) { - mIsDozing = isDozing; - updateBurnInOffsets(); - updateVisibility(); + if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) { + mIsDozing = isDozing; + updateBurnInOffsets(); + updateVisibility(); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 2f79e30a0b5b..31fc3204a7f1 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -17,9 +17,12 @@ package com.android.keyguard.logging import android.hardware.biometrics.BiometricConstants.LockoutMode +import android.os.PowerManager +import android.os.PowerManager.WakeReason import android.telephony.ServiceState import android.telephony.SubscriptionInfo import com.android.keyguard.ActiveUnlockConfig +import com.android.keyguard.FaceAuthUiEvent import com.android.keyguard.KeyguardListenModel import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.plugins.log.LogBuffer @@ -269,11 +272,19 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" }) } - fun logStartedListeningForFace(faceRunningState: Int, faceAuthReason: String) { + fun logStartedListeningForFace(faceRunningState: Int, faceAuthUiEvent: FaceAuthUiEvent) { logBuffer.log(TAG, VERBOSE, { int1 = faceRunningState - str1 = faceAuthReason - }, { "startListeningForFace(): $int1, reason: $str1" }) + str1 = faceAuthUiEvent.reason + str2 = faceAuthUiEvent.extraInfoToString() + }, { "startListeningForFace(): $int1, reason: $str1 $str2" }) + } + + fun logStartedListeningForFaceFromWakeUp(faceRunningState: Int, @WakeReason pmWakeReason: Int) { + logBuffer.log(TAG, VERBOSE, { + int1 = faceRunningState + str1 = PowerManager.wakeReasonToString(pmWakeReason) + }, { "startListeningForFace(): $int1, reason: wakeUp-$str1" }) } fun logStoppedListeningForFace(faceRunningState: Int, faceAuthReason: String) { @@ -383,4 +394,10 @@ class KeyguardUpdateMonitorLogger @Inject constructor( }, { "#update secure=$bool1 canDismissKeyguard=$bool2" + " trusted=$bool3 trustManaged=$bool4" }) } + + fun logSkipUpdateFaceListeningOnWakeup(@WakeReason pmWakeReason: Int) { + logBuffer.log(TAG, VERBOSE, { + str1 = PowerManager.wakeReasonToString(pmWakeReason) + }, { "Skip updating face listening state on wakeup from $str1"}) + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index ea334b27fa09..777d10c7acfd 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -28,6 +28,7 @@ import android.os.UserHandle; import android.text.TextUtils; import android.view.Display; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.annotation.MainThread; @@ -56,6 +57,7 @@ public class AccessibilityFloatingMenuController implements private Context mContext; private final WindowManager mWindowManager; private final DisplayManager mDisplayManager; + private final AccessibilityManager mAccessibilityManager; private final FeatureFlags mFeatureFlags; @VisibleForTesting IAccessibilityFloatingMenu mFloatingMenu; @@ -96,6 +98,7 @@ public class AccessibilityFloatingMenuController implements public AccessibilityFloatingMenuController(Context context, WindowManager windowManager, DisplayManager displayManager, + AccessibilityManager accessibilityManager, AccessibilityButtonTargetsObserver accessibilityButtonTargetsObserver, AccessibilityButtonModeObserver accessibilityButtonModeObserver, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -103,6 +106,7 @@ public class AccessibilityFloatingMenuController implements mContext = context; mWindowManager = windowManager; mDisplayManager = displayManager; + mAccessibilityManager = accessibilityManager; mAccessibilityButtonTargetsObserver = accessibilityButtonTargetsObserver; mAccessibilityButtonModeObserver = accessibilityButtonModeObserver; mKeyguardUpdateMonitor = keyguardUpdateMonitor; @@ -180,7 +184,8 @@ public class AccessibilityFloatingMenuController implements final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY); mFloatingMenu = new MenuViewLayerController( mContext.createWindowContext(defaultDisplay, - TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager); + TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager, + mAccessibilityManager); } else { mFloatingMenu = new AccessibilityFloatingMenu(mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java new file mode 100644 index 000000000000..ee048e1a02d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.ComponentCallbacks; +import android.content.res.Configuration; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.dynamicanimation.animation.DynamicAnimation; + +import com.android.systemui.R; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +/** + * Controls the interaction between {@link MagnetizedObject} and + * {@link MagnetizedObject.MagneticTarget}. + */ +class DismissAnimationController implements ComponentCallbacks { + private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float COMPLETELY_TRANSPARENT = 0.0f; + private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; + private static final float ANIMATING_MAX_ALPHA = 0.7f; + + private final DismissView mDismissView; + private final MenuView mMenuView; + private final ValueAnimator mDismissAnimator; + private final MagnetizedObject<?> mMagnetizedObject; + private float mMinDismissSize; + private float mSizePercent; + + DismissAnimationController(DismissView dismissView, MenuView menuView) { + mDismissView = dismissView; + mDismissView.setPivotX(dismissView.getWidth() / 2.0f); + mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + mMenuView = menuView; + + updateResources(); + + mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); + mDismissAnimator.addUpdateListener(dismissAnimation -> { + final float animatedValue = (float) dismissAnimation.getAnimatedValue(); + final float scaleValue = Math.max(animatedValue, mSizePercent); + dismissView.getCircle().setScaleX(scaleValue); + dismissView.getCircle().setScaleY(scaleValue); + + menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + }); + + mDismissAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + super.onAnimationEnd(animation, isReverse); + + if (isReverse) { + mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); + mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); + mMenuView.setAlpha(COMPLETELY_OPAQUE); + } + } + }); + + mMagnetizedObject = + new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView, + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_X), + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_Y)) { + @Override + public void getLocationOnScreen(MenuView underlyingObject, int[] loc) { + underlyingObject.getLocationOnScreen(loc); + } + + @Override + public float getHeight(MenuView underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public float getWidth(MenuView underlyingObject) { + return underlyingObject.getWidth(); + } + }; + + final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget( + dismissView.getCircle(), (int) mMinDismissSize); + mMagnetizedObject.addTarget(magneticTarget); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + updateResources(); + } + + @Override + public void onLowMemory() { + // Do nothing + } + + void showDismissView(boolean show) { + if (show) { + mDismissView.show(); + } else { + mDismissView.hide(); + } + } + + void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) { + mMagnetizedObject.setMagnetListener(magnetListener); + } + + void maybeConsumeDownMotionEvent(MotionEvent event) { + mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was + * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * + * @param event that move the magnetized object which is also the menu list view. + * @return true if the location of the motion events moves within the magnetic field of a + * target, but false if didn't set + * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. + */ + boolean maybeConsumeMoveMotionEvent(MotionEvent event) { + return mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * This used to pass {@link MotionEvent#ACTION_UP} to the magnetized object to check if it was + * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * + * @param event that move the magnetized object which is also the menu list view. + * @return true if the location of the motion events moves within the magnetic field of a + * target, but false if didn't set + * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. + */ + boolean maybeConsumeUpMotionEvent(MotionEvent event) { + return mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + void animateDismissMenu(boolean scaleUp) { + if (scaleUp) { + mDismissAnimator.start(); + } else { + mDismissAnimator.reverse(); + } + } + + private void updateResources() { + final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_size); + mMinDismissSize = mDismissView.getResources().getDimensionPixelSize( + R.dimen.dismiss_circle_small); + mSizePercent = mMinDismissSize / maxDismissSize; + } + + interface DismissCallback { + void onDismiss(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index d6d039903505..396f584d76a6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -35,6 +35,8 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import androidx.recyclerview.widget.RecyclerView; +import com.android.internal.util.Preconditions; + import java.util.HashMap; /** @@ -47,6 +49,9 @@ class MenuAnimationController { private static final float MIN_PERCENT = 0.0f; private static final float MAX_PERCENT = 1.0f; private static final float COMPLETELY_OPAQUE = 1.0f; + private static final float COMPLETELY_TRANSPARENT = 0.0f; + private static final float SCALE_SHRINK = 0.0f; + private static final float SCALE_GROW = 1.0f; private static final float FLING_FRICTION_SCALAR = 1.9f; private static final float DEFAULT_FRICTION = 4.2f; private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f; @@ -61,6 +66,7 @@ class MenuAnimationController { private final Handler mHandler; private boolean mIsMovedToEdge; private boolean mIsFadeEffectEnabled; + private DismissAnimationController.DismissCallback mDismissCallback; // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler @@ -99,6 +105,11 @@ class MenuAnimationController { } } + void setDismissCallback( + DismissAnimationController.DismissCallback dismissCallback) { + mDismissCallback = dismissCallback; + } + void moveToTopLeftPosition() { mIsMovedToEdge = false; final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); @@ -129,6 +140,13 @@ class MenuAnimationController { constrainPositionAndUpdate(position); } + void removeMenu() { + Preconditions.checkArgument(mDismissCallback != null, + "The dismiss callback should be initialized first."); + + mDismissCallback.onDismiss(); + } + void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) { final boolean shouldMenuFlingLeft = isOnLeftSide() ? velocityX < ESCAPE_VELOCITY @@ -297,6 +315,28 @@ class MenuAnimationController { mMenuView.onDraggingStart(); } + void startShrinkAnimation(Runnable endAction) { + mMenuView.animate().cancel(); + + mMenuView.animate() + .scaleX(SCALE_SHRINK) + .scaleY(SCALE_SHRINK) + .alpha(COMPLETELY_TRANSPARENT) + .translationY(mMenuView.getTranslationY()) + .withEndAction(endAction).start(); + } + + void startGrowAnimation() { + mMenuView.animate().cancel(); + + mMenuView.animate() + .scaleX(SCALE_GROW) + .scaleY(SCALE_GROW) + .alpha(COMPLETELY_OPAQUE) + .translationY(mMenuView.getTranslationY()) + .start(); + } + private void onSpringAnimationEnd(PointF position) { mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); constrainPositionAndUpdate(position); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java index e69a24810fdc..ac5736b0c26d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -84,6 +84,12 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It new AccessibilityNodeInfoCompat.AccessibilityActionCompat(moveEdgeId, res.getString(moveEdgeTextResId)); info.addAction(moveToOrOutEdge); + + final AccessibilityNodeInfoCompat.AccessibilityActionCompat removeMenu = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_remove_menu, + res.getString(R.string.accessibility_floating_button_action_remove_menu)); + info.addAction(removeMenu); } @Override @@ -126,6 +132,11 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It return true; } + if (action == R.id.action_remove_menu) { + mAnimationController.removeMenu(); + return true; + } + return super.performAccessibilityAction(host, action, args); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java index 3146c9f0d2af..bc3cf0a6bab0 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java @@ -38,9 +38,12 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { private final PointF mMenuTranslationDown = new PointF(); private boolean mIsDragging = false; private float mTouchSlop; + private final DismissAnimationController mDismissAnimationController; - MenuListViewTouchHandler(MenuAnimationController menuAnimationController) { + MenuListViewTouchHandler(MenuAnimationController menuAnimationController, + DismissAnimationController dismissAnimationController) { mMenuAnimationController = menuAnimationController; + mDismissAnimationController = dismissAnimationController; } @Override @@ -61,6 +64,7 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY()); mMenuAnimationController.cancelAnimations(); + mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent); break; case MotionEvent.ACTION_MOVE: if (mIsDragging || Math.hypot(dx, dy) > mTouchSlop) { @@ -69,8 +73,13 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuAnimationController.onDraggingStart(); } - mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); - mMenuAnimationController.moveToPositionYIfNeeded(mMenuTranslationDown.y + dy); + mDismissAnimationController.showDismissView(/* show= */ true); + + if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) { + mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); + mMenuAnimationController.moveToPositionYIfNeeded( + mMenuTranslationDown.y + dy); + } } break; case MotionEvent.ACTION_UP: @@ -79,10 +88,18 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { final float endX = mMenuTranslationDown.x + dx; mIsDragging = false; - if (!mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { + mDismissAnimationController.showDismissView(/* show= */ false); + mMenuAnimationController.fadeOutIfEnabled(); + + return true; + } + + if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) { mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); mMenuAnimationController.flingMenuThenSpringToEdge(endX, mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); + mDismissAnimationController.showDismissView(/* show= */ false); } // Avoid triggering the listener of the item. diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java new file mode 100644 index 000000000000..9875ad06f1ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static android.util.TypedValue.COMPLEX_UNIT_PX; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import android.annotation.IntDef; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.settingslib.Utils; +import com.android.systemui.R; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * The message view with the action prompt to whether to undo operation for users when removing + * the {@link MenuView}. + */ +class MenuMessageView extends LinearLayout implements + ViewTreeObserver.OnComputeInternalInsetsListener { + private final TextView mTextView; + private final Button mUndoButton; + + @IntDef({ + Index.TEXT_VIEW, + Index.UNDO_BUTTON + }) + @Retention(RetentionPolicy.SOURCE) + @interface Index { + int TEXT_VIEW = 0; + int UNDO_BUTTON = 1; + } + + MenuMessageView(Context context) { + super(context); + + setVisibility(GONE); + + mTextView = new TextView(context); + mUndoButton = new Button(context); + + addView(mTextView, Index.TEXT_VIEW, + new LayoutParams(/* width= */ 0, WRAP_CONTENT, /* weight= */ 1)); + addView(mUndoButton, Index.UNDO_BUTTON, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + updateResources(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(WRAP_CONTENT, + WRAP_CONTENT); + containerParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; + setLayoutParams(containerParams); + setGravity(Gravity.CENTER_VERTICAL); + + mUndoButton.setBackground(null); + + updateResources(); + + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + + if (getVisibility() == VISIBLE) { + final int x = (int) getX(); + final int y = (int) getY(); + inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight())); + } + } + + /** + * Registers a listener to be invoked when this undo action button is clicked. It should be + * called after {@link View#onAttachedToWindow()}. + * + * @param listener The listener that will run + */ + void setUndoListener(OnClickListener listener) { + mUndoButton.setOnClickListener(listener); + } + + private void updateResources() { + final Resources res = getResources(); + + final int containerPadding = + res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_container_horizontal_padding); + final int margin = res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_margin); + final FrameLayout.LayoutParams containerParams = + (FrameLayout.LayoutParams) getLayoutParams(); + containerParams.setMargins(margin, margin, margin, margin); + setLayoutParams(containerParams); + setBackground(res.getDrawable(R.drawable.accessibility_floating_message_background)); + setPadding(containerPadding, /* top= */ 0, containerPadding, /* bottom= */ 0); + setMinimumWidth( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_width)); + setMinimumHeight( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_min_height)); + setElevation( + res.getDimensionPixelSize(R.dimen.accessibility_floating_menu_message_elevation)); + + final int textPadding = + res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_text_vertical_padding); + final int textColor = res.getColor(R.color.accessibility_floating_menu_message_text); + final int textSize = res.getDimensionPixelSize( + R.dimen.accessibility_floating_menu_message_text_size); + mTextView.setPadding(/* left= */ 0, textPadding, /* right= */ 0, textPadding); + mTextView.setTextSize(COMPLEX_UNIT_PX, textSize); + mTextView.setTextColor(textColor); + + final ColorStateList colorAccent = Utils.getColorAccent(getContext()); + mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo)); + mUndoButton.setTextSize(COMPLEX_UNIT_PX, textSize); + mUndoButton.setTextColor(colorAccent); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 15d139cf15da..6a14af52fbaf 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -42,7 +42,7 @@ import java.util.Collections; import java.util.List; /** - * The menu view displays the accessibility features. + * The container view displays the accessibility features. */ @SuppressLint("ViewConstructor") class MenuView extends FrameLayout implements @@ -64,13 +64,14 @@ class MenuView extends FrameLayout implements this::onTargetFeaturesChanged; private final MenuViewAppearance mMenuViewAppearance; + private OnTargetFeaturesChangeListener mFeaturesChangeListener; + MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) { super(context); mMenuViewModel = menuViewModel; mMenuViewAppearance = menuViewAppearance; mMenuAnimationController = new MenuAnimationController(this); - mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); @@ -96,7 +97,9 @@ class MenuView extends FrameLayout implements @Override public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - inoutInfo.touchableRegion.set(mBoundsInParent); + if (getVisibility() == VISIBLE) { + inoutInfo.touchableRegion.union(mBoundsInParent); + } } @Override @@ -108,10 +111,18 @@ class MenuView extends FrameLayout implements mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); } + void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) { + mFeaturesChangeListener = listener; + } + void addOnItemTouchListenerToList(RecyclerView.OnItemTouchListener listener) { mTargetFeaturesView.addOnItemTouchListener(listener); } + MenuAnimationController getMenuAnimationController() { + return mMenuAnimationController; + } + @SuppressLint("NotifyDataSetChanged") private void onItemSizeChanged() { mAdapter.setItemPadding(mMenuViewAppearance.getMenuPadding()); @@ -139,7 +150,7 @@ class MenuView extends FrameLayout implements onEdgeChanged(); } - private void onEdgeChanged() { + void onEdgeChanged() { final int[] insets = mMenuViewAppearance.getMenuInsets(); getContainerViewInsetLayer().setLayerInset(INDEX_MENU_ITEM, insets[0], insets[1], insets[2], insets[3]); @@ -193,6 +204,9 @@ class MenuView extends FrameLayout implements onEdgeChanged(); onPositionChanged(); + if (mFeaturesChangeListener != null) { + mFeaturesChangeListener.onChange(newTargetFeatures); + } mMenuAnimationController.fadeOutIfEnabled(); } @@ -299,4 +313,17 @@ class MenuView extends FrameLayout implements final ViewGroup parentView = (ViewGroup) getParent(); parentView.setSystemGestureExclusionRects(Collections.singletonList(mBoundsInParent)); } + + /** + * Interface definition for the {@link AccessibilityTarget} list changes. + */ + interface OnTargetFeaturesChangeListener { + /** + * Called when the list of accessibility target features was updated. This will be + * invoked when the end of {@code onTargetFeaturesChanged}. + * + * @param newTargetFeatures the list related to the current accessibility features. + */ + void onChange(List<AccessibilityTarget> newTargetFeatures); + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 5252519e9faf..33e155df80e3 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -16,42 +16,155 @@ package com.android.systemui.accessibility.floatingmenu; +import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index; + import android.annotation.IntDef; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.Configuration; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.util.PluralsMessageFormatter; import android.view.MotionEvent; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.NonNull; +import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.systemui.R; +import com.android.wm.shell.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** - * The basic interactions with the child view {@link MenuView}. + * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and + * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the + * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu + * message view would be shown and allowed users to undo it. */ @SuppressLint("ViewConstructor") class MenuViewLayer extends FrameLayout { + private static final int SHOW_MESSAGE_DELAY_MS = 3000; + private final MenuView mMenuView; + private final MenuMessageView mMessageView; + private final DismissView mDismissView; + private final MenuAnimationController mMenuAnimationController; + private final AccessibilityManager mAccessibilityManager; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final IAccessibilityFloatingMenu mFloatingMenu; + private final DismissAnimationController mDismissAnimationController; @IntDef({ - LayerIndex.MENU_VIEW + LayerIndex.MENU_VIEW, + LayerIndex.DISMISS_VIEW, + LayerIndex.MESSAGE_VIEW, }) @Retention(RetentionPolicy.SOURCE) @interface LayerIndex { int MENU_VIEW = 0; + int DISMISS_VIEW = 1; + int MESSAGE_VIEW = 2; } - MenuViewLayer(@NonNull Context context, WindowManager windowManager) { + @VisibleForTesting + final Runnable mDismissMenuAction = new Runnable() { + @Override + public void run() { + Settings.Secure.putString(getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ ""); + mFloatingMenu.hide(); + } + }; + + MenuViewLayer(@NonNull Context context, WindowManager windowManager, + AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) { super(context); + mAccessibilityManager = accessibilityManager; + mFloatingMenu = floatingMenu; + final MenuViewModel menuViewModel = new MenuViewModel(context); final MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, windowManager); mMenuView = new MenuView(context, menuViewModel, menuViewAppearance); + mMenuAnimationController = mMenuView.getMenuAnimationController(); + mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); + + mDismissView = new DismissView(context); + mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView); + mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velocityX, float velocityY, boolean wasFlungOut) { + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + hideMenuAndShowMessage(); + mDismissView.hide(); + mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); + } + }); + + final MenuListViewTouchHandler menuListViewTouchHandler = new MenuListViewTouchHandler( + mMenuAnimationController, mDismissAnimationController); + mMenuView.addOnItemTouchListenerToList(menuListViewTouchHandler); + + mMessageView = new MenuMessageView(context); + + mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> { + if (newTargetFeatures.size() < 1) { + return; + } + + // During the undo action period, the pending action will be canceled and undo back + // to the previous state if users did any action related to the accessibility features. + if (mMessageView.getVisibility() == VISIBLE) { + undo(); + } + + final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW); + messageText.setText(getMessageText(newTargetFeatures)); + }); addView(mMenuView, LayerIndex.MENU_VIEW); + addView(mDismissView, LayerIndex.DISMISS_VIEW); + addView(mMessageView, LayerIndex.MESSAGE_VIEW); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mDismissView.updateResources(); + } + + private String getMessageText(List<AccessibilityTarget> newTargetFeatures) { + Preconditions.checkArgument(newTargetFeatures.size() > 0, + "The list should at least have one feature."); + + final Map<String, Object> arguments = new HashMap<>(); + arguments.put("count", newTargetFeatures.size()); + arguments.put("label", newTargetFeatures.get(0).getLabel()); + return PluralsMessageFormatter.format(getResources(), arguments, + R.string.accessibility_floating_button_undo_message_text); } @Override @@ -68,6 +181,8 @@ class MenuViewLayer extends FrameLayout { super.onAttachedToWindow(); mMenuView.show(); + mMessageView.setUndoListener(view -> undo()); + mContext.registerComponentCallbacks(mDismissAnimationController); } @Override @@ -75,5 +190,26 @@ class MenuViewLayer extends FrameLayout { super.onDetachedFromWindow(); mMenuView.hide(); + mHandler.removeCallbacksAndMessages(/* token= */ null); + mContext.unregisterComponentCallbacks(mDismissAnimationController); + } + + private void hideMenuAndShowMessage() { + final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( + SHOW_MESSAGE_DELAY_MS, + AccessibilityManager.FLAG_CONTENT_TEXT + | AccessibilityManager.FLAG_CONTENT_CONTROLS); + mHandler.postDelayed(mDismissMenuAction, delayTime); + mMessageView.setVisibility(VISIBLE); + mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); + } + + private void undo() { + mHandler.removeCallbacksAndMessages(/* token= */ null); + mMessageView.setVisibility(GONE); + mMenuView.onEdgeChanged(); + mMenuView.onPositionChanged(); + mMenuView.setVisibility(VISIBLE); + mMenuAnimationController.startGrowAnimation(); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index d2093c200ca2..b1a64eda46ff 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -22,6 +22,7 @@ import android.content.Context; import android.graphics.PixelFormat; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; /** * Controls the {@link MenuViewLayer} whether to be attached to the window via the interface @@ -32,9 +33,10 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { private final MenuViewLayer mMenuViewLayer; private boolean mIsShowing; - MenuViewLayerController(Context context, WindowManager windowManager) { + MenuViewLayerController(Context context, WindowManager windowManager, + AccessibilityManager accessibilityManager) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context, windowManager); + mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b50bfd7c24f9..f74c721bf114 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -26,6 +26,7 @@ import android.annotation.DurationMillisLong; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AlertDialog; import android.content.Context; import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; @@ -63,6 +64,9 @@ import com.android.internal.widget.LockPatternUtils; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.biometrics.AuthController.ScaleFactorProvider; +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; +import com.android.systemui.biometrics.ui.CredentialView; +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -74,11 +78,13 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import javax.inject.Provider; + /** * Top level container/controller for the BiometricPrompt UI. */ public class AuthContainerView extends LinearLayout - implements AuthDialog, WakefulnessLifecycle.Observer { + implements AuthDialog, WakefulnessLifecycle.Observer, CredentialView.Host { private static final String TAG = "AuthContainerView"; @@ -112,15 +118,18 @@ public class AuthContainerView extends LinearLayout private final IBinder mWindowToken = new Binder(); private final WindowManager mWindowManager; private final Interpolator mLinearOutSlowIn; - private final CredentialCallback mCredentialCallback; private final LockPatternUtils mLockPatternUtils; private final WakefulnessLifecycle mWakefulnessLifecycle; private final InteractionJankMonitor mInteractionJankMonitor; + // TODO: these should be migrated out once ready + private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor; + private final Provider<CredentialViewModel> mCredentialViewModelProvider; + @VisibleForTesting final BiometricCallback mBiometricCallback; @Nullable private AuthBiometricView mBiometricView; - @Nullable private AuthCredentialView mCredentialView; + @Nullable private View mCredentialView; private final AuthPanelController mPanelController; private final FrameLayout mFrameLayout; private final ImageView mBackgroundView; @@ -229,11 +238,13 @@ public class AuthContainerView extends LinearLayout @NonNull WakefulnessLifecycle wakefulnessLifecycle, @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, - @NonNull InteractionJankMonitor jankMonitor) { + @NonNull InteractionJankMonitor jankMonitor, + @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<CredentialViewModel> credentialViewModelProvider) { mConfig.mSensorIds = sensorIds; return new AuthContainerView(mConfig, fpProps, faceProps, wakefulnessLifecycle, - userManager, lockPatternUtils, jankMonitor, new Handler(Looper.getMainLooper()), - bgExecutor); + userManager, lockPatternUtils, jankMonitor, biometricPromptInteractor, + credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor); } } @@ -271,12 +282,49 @@ public class AuthContainerView extends LinearLayout } } - final class CredentialCallback implements AuthCredentialView.Callback { - @Override - public void onCredentialMatched(byte[] attestation) { - mCredentialAttestation = attestation; - animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); - } + @Override + public void onCredentialMatched(@NonNull byte[] attestation) { + mCredentialAttestation = attestation; + animateAway(AuthDialogCallback.DISMISSED_CREDENTIAL_AUTHENTICATED); + } + + @Override + public void onCredentialAborted() { + sendEarlyUserCanceled(); + animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); + } + + @Override + public void onCredentialAttemptsRemaining(int remaining, @NonNull String messageBody) { + // Only show dialog if <=1 attempts are left before wiping. + if (remaining == 1) { + showLastAttemptBeforeWipeDialog(messageBody); + } else if (remaining <= 0) { + showNowWipingDialog(messageBody); + } + } + + private void showLastAttemptBeforeWipeDialog(@NonNull String messageBody) { + final AlertDialog alertDialog = new AlertDialog.Builder(mContext) + .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) + .setMessage(messageBody) + .setPositiveButton(android.R.string.ok, null) + .create(); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.show(); + } + + private void showNowWipingDialog(@NonNull String messageBody) { + final AlertDialog alertDialog = new AlertDialog.Builder(mContext) + .setMessage(messageBody) + .setPositiveButton( + com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, + null /* OnClickListener */) + .setOnDismissListener( + dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR)) + .create(); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.show(); } @VisibleForTesting @@ -287,6 +335,8 @@ public class AuthContainerView extends LinearLayout @NonNull UserManager userManager, @NonNull LockPatternUtils lockPatternUtils, @NonNull InteractionJankMonitor jankMonitor, + @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull Handler mainHandler, @NonNull @Background DelayableExecutor bgExecutor) { super(config.mContext); @@ -302,7 +352,6 @@ public class AuthContainerView extends LinearLayout .getDimension(R.dimen.biometric_dialog_animation_translation_offset); mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN; mBiometricCallback = new BiometricCallback(); - mCredentialCallback = new CredentialCallback(); final LayoutInflater layoutInflater = LayoutInflater.from(mContext); mFrameLayout = (FrameLayout) layoutInflater.inflate( @@ -314,6 +363,8 @@ public class AuthContainerView extends LinearLayout mPanelController = new AuthPanelController(mContext, mPanelView); mBackgroundExecutor = bgExecutor; mInteractionJankMonitor = jankMonitor; + mBiometricPromptInteractor = biometricPromptInteractor; + mCredentialViewModelProvider = credentialViewModelProvider; // Inflate biometric view only if necessary. if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { @@ -404,12 +455,12 @@ public class AuthContainerView extends LinearLayout switch (credentialType) { case Utils.CREDENTIAL_PATTERN: - mCredentialView = (AuthCredentialView) factory.inflate( + mCredentialView = factory.inflate( R.layout.auth_credential_pattern_view, null, false); break; case Utils.CREDENTIAL_PIN: case Utils.CREDENTIAL_PASSWORD: - mCredentialView = (AuthCredentialView) factory.inflate( + mCredentialView = factory.inflate( R.layout.auth_credential_password_view, null, false); break; default: @@ -422,16 +473,12 @@ public class AuthContainerView extends LinearLayout mBackgroundView.setOnClickListener(null); mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); - mCredentialView.setContainerView(this); - mCredentialView.setUserId(mConfig.mUserId); - mCredentialView.setOperationId(mConfig.mOperationId); - mCredentialView.setEffectiveUserId(mEffectiveUserId); - mCredentialView.setCredentialType(credentialType); - mCredentialView.setCallback(mCredentialCallback); - mCredentialView.setPromptInfo(mConfig.mPromptInfo); - mCredentialView.setPanelController(mPanelController, animatePanel); - mCredentialView.setShouldAnimateContents(animateContents); - mCredentialView.setBackgroundExecutor(mBackgroundExecutor); + mBiometricPromptInteractor.get().useCredentialsForAuthentication( + mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId); + final CredentialViewModel vm = mCredentialViewModelProvider.get(); + vm.setAnimateContents(animateContents); + ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); + mFrameLayout.addView(mCredentialView); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 8c7e0efee7e6..313ff4157155 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -72,6 +72,8 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.os.SomeArgs; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.CoreStartable; +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -122,6 +124,10 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, private final Provider<UdfpsController> mUdfpsControllerFactory; private final Provider<SidefpsController> mSidefpsControllerFactory; + // TODO: these should be migrated out once ready + @NonNull private final Provider<BiometricPromptCredentialInteractor> mBiometricPromptInteractor; + @NonNull private final Provider<CredentialViewModel> mCredentialViewModelProvider; + private final Display mDisplay; private float mScaleFactor = 1f; // sensor locations without any resolution scaling nor rotation adjustments: @@ -153,6 +159,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps; @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser; + @NonNull private final SparseBooleanArray mFaceEnrolledForUser; @NonNull private final SensorPrivacyManager mSensorPrivacyManager; private final WakefulnessLifecycle mWakefulnessLifecycle; private boolean mAllFingerprintAuthenticatorsRegistered; @@ -349,6 +356,15 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, } } } + if (mFaceProps == null) { + Log.d(TAG, "handleEnrollmentsChanged, mFaceProps is null"); + } else { + for (FaceSensorPropertiesInternal prop : mFaceProps) { + if (prop.sensorId == sensorId) { + mFaceEnrolledForUser.put(userId, hasEnrollments); + } + } + } for (Callback cb : mCallbacks) { cb.onEnrollmentsChanged(modality); } @@ -683,6 +699,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, @NonNull LockPatternUtils lockPatternUtils, @NonNull UdfpsLogger udfpsLogger, @NonNull StatusBarStateController statusBarStateController, + @NonNull Provider<BiometricPromptCredentialInteractor> biometricPromptInteractor, + @NonNull Provider<CredentialViewModel> credentialViewModelProvider, @NonNull InteractionJankMonitor jankMonitor, @Main Handler handler, @Background DelayableExecutor bgExecutor, @@ -704,8 +722,12 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, mWindowManager = windowManager; mInteractionJankMonitor = jankMonitor; mUdfpsEnrolledForUser = new SparseBooleanArray(); + mFaceEnrolledForUser = new SparseBooleanArray(); mVibratorHelper = vibrator; + mBiometricPromptInteractor = biometricPromptInteractor; + mCredentialViewModelProvider = credentialViewModelProvider; + mOrientationListener = new BiometricDisplayListener( context, mDisplayManager, @@ -1054,7 +1076,7 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return false; } - return mFaceManager.hasEnrolledTemplates(userId); + return mFaceEnrolledForUser.get(userId); } /** @@ -1068,6 +1090,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, return mUdfpsEnrolledForUser.get(userId); } + /** If BiometricPrompt is currently being shown to the user. */ + public boolean isShowing() { + return mCurrentDialog != null; + } + private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) { mCurrentDialogArgs = args; @@ -1199,7 +1226,8 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks, .setMultiSensorConfig(multiSensorConfig) .setScaleFactorProvider(() -> getScaleFactor()) .build(bgExecutor, sensorIds, mFpProps, mFaceProps, wakefulnessLifecycle, - userManager, lockPatternUtils, mInteractionJankMonitor); + userManager, lockPatternUtils, mInteractionJankMonitor, + mBiometricPromptInteractor, mCredentialViewModelProvider); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java deleted file mode 100644 index 76cd3f4c4f1d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPasswordView.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.view.WindowInsets.Type.ime; - -import android.annotation.NonNull; -import android.content.Context; -import android.graphics.Insets; -import android.os.UserHandle; -import android.text.InputType; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.View; -import android.view.View.OnApplyWindowInsetsListener; -import android.view.ViewGroup; -import android.view.WindowInsets; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.ImeAwareEditText; -import android.widget.TextView; - -import com.android.internal.widget.LockPatternChecker; -import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.LockscreenCredential; -import com.android.internal.widget.VerifyCredentialResponse; -import com.android.systemui.Dumpable; -import com.android.systemui.R; - -import java.io.PrintWriter; - -/** - * Pin and Password UI - */ -public class AuthCredentialPasswordView extends AuthCredentialView - implements TextView.OnEditorActionListener, OnApplyWindowInsetsListener, Dumpable { - - private static final String TAG = "BiometricPrompt/AuthCredentialPasswordView"; - - private final InputMethodManager mImm; - private ImeAwareEditText mPasswordField; - private ViewGroup mAuthCredentialHeader; - private ViewGroup mAuthCredentialInput; - private int mBottomInset = 0; - - public AuthCredentialPasswordView(Context context, - AttributeSet attrs) { - super(context, attrs); - mImm = mContext.getSystemService(InputMethodManager.class); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - - mAuthCredentialHeader = findViewById(R.id.auth_credential_header); - mAuthCredentialInput = findViewById(R.id.auth_credential_input); - mPasswordField = findViewById(R.id.lockPassword); - mPasswordField.setOnEditorActionListener(this); - // TODO: De-dupe the logic with AuthContainerView - mPasswordField.setOnKeyListener((v, keyCode, event) -> { - if (keyCode != KeyEvent.KEYCODE_BACK) { - return false; - } - if (event.getAction() == KeyEvent.ACTION_UP) { - mContainerView.sendEarlyUserCanceled(); - mContainerView.animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED); - } - return true; - }); - - setOnApplyWindowInsetsListener(this); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - mPasswordField.setTextOperationUser(UserHandle.of(mUserId)); - if (mCredentialType == Utils.CREDENTIAL_PIN) { - mPasswordField.setInputType( - InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); - } - - mPasswordField.requestFocus(); - mPasswordField.scheduleShowSoftInput(); - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - // Check if this was the result of hitting the enter key - final boolean isSoftImeEvent = event == null - && (actionId == EditorInfo.IME_NULL - || actionId == EditorInfo.IME_ACTION_DONE - || actionId == EditorInfo.IME_ACTION_NEXT); - final boolean isKeyboardEnterKey = event != null - && KeyEvent.isConfirmKey(event.getKeyCode()) - && event.getAction() == KeyEvent.ACTION_DOWN; - if (isSoftImeEvent || isKeyboardEnterKey) { - checkPasswordAndUnlock(); - return true; - } - return false; - } - - private void checkPasswordAndUnlock() { - try (LockscreenCredential password = mCredentialType == Utils.CREDENTIAL_PIN - ? LockscreenCredential.createPinOrNone(mPasswordField.getText()) - : LockscreenCredential.createPasswordOrNone(mPasswordField.getText())) { - if (password.isNone()) { - return; - } - - // Request LockSettingsService to return the Gatekeeper Password in the - // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the - // Gatekeeper Password and operationId. - mPendingLockCheck = LockPatternChecker.verifyCredential(mLockPatternUtils, - password, mEffectiveUserId, LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, - this::onCredentialVerified); - } - } - - @Override - protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, - int timeoutMs) { - super.onCredentialVerified(response, timeoutMs); - - if (response.isMatched()) { - mImm.hideSoftInputFromWindow(getWindowToken(), 0 /* flags */); - } else { - mPasswordField.setText(""); - } - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mAuthCredentialInput == null || mAuthCredentialHeader == null || mSubtitleView == null - || mDescriptionView == null || mPasswordField == null || mErrorView == null) { - return; - } - - int inputLeftBound; - int inputTopBound; - int headerRightBound = right; - int headerTopBounds = top; - final int subTitleBottom = (mSubtitleView.getVisibility() == GONE) ? mTitleView.getBottom() - : mSubtitleView.getBottom(); - final int descBottom = (mDescriptionView.getVisibility() == GONE) ? subTitleBottom - : mDescriptionView.getBottom(); - if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - inputTopBound = (bottom - mAuthCredentialInput.getHeight()) / 2; - inputLeftBound = (right - left) / 2; - headerRightBound = inputLeftBound; - headerTopBounds -= Math.min(mIconView.getBottom(), mBottomInset); - } else { - inputTopBound = - descBottom + (bottom - descBottom - mAuthCredentialInput.getHeight()) / 2; - inputLeftBound = (right - left - mAuthCredentialInput.getWidth()) / 2; - } - - if (mDescriptionView.getBottom() > mBottomInset) { - mAuthCredentialHeader.layout(left, headerTopBounds, headerRightBound, bottom); - } - mAuthCredentialInput.layout(inputLeftBound, inputTopBound, right, bottom); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - final int newWidth = MeasureSpec.getSize(widthMeasureSpec); - final int newHeight = MeasureSpec.getSize(heightMeasureSpec) - mBottomInset; - - setMeasuredDimension(newWidth, newHeight); - - final int halfWidthSpec = MeasureSpec.makeMeasureSpec(getWidth() / 2, - MeasureSpec.AT_MOST); - final int fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED); - if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - measureChildren(halfWidthSpec, fullHeightSpec); - } else { - measureChildren(widthMeasureSpec, fullHeightSpec); - } - } - - @NonNull - @Override - public WindowInsets onApplyWindowInsets(@NonNull View v, WindowInsets insets) { - - final Insets bottomInset = insets.getInsets(ime()); - if (v instanceof AuthCredentialPasswordView && mBottomInset != bottomInset.bottom) { - mBottomInset = bottomInset.bottom; - if (mBottomInset > 0 - && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) { - mTitleView.setSingleLine(true); - mTitleView.setEllipsize(TextUtils.TruncateAt.MARQUEE); - mTitleView.setMarqueeRepeatLimit(-1); - // select to enable marquee unless a screen reader is enabled - mTitleView.setSelected(!mAccessibilityManager.isEnabled() - || !mAccessibilityManager.isTouchExplorationEnabled()); - } else { - mTitleView.setSingleLine(false); - mTitleView.setEllipsize(null); - // select to enable marquee unless a screen reader is enabled - mTitleView.setSelected(false); - } - requestLayout(); - } - return insets; - } - - @Override - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { - pw.println(TAG + "State:"); - pw.println(" mBottomInset=" + mBottomInset); - pw.println(" mAuthCredentialHeader size=(" + mAuthCredentialHeader.getWidth() + "," - + mAuthCredentialHeader.getHeight()); - pw.println(" mAuthCredentialInput size=(" + mAuthCredentialInput.getWidth() + "," - + mAuthCredentialInput.getHeight()); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java deleted file mode 100644 index f9e44a0c1724..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialPatternView.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import android.annotation.NonNull; -import android.content.Context; -import android.util.AttributeSet; - -import com.android.internal.widget.LockPatternChecker; -import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.LockPatternView; -import com.android.internal.widget.LockscreenCredential; -import com.android.internal.widget.VerifyCredentialResponse; -import com.android.systemui.R; - -import java.util.List; - -/** - * Pattern UI - */ -public class AuthCredentialPatternView extends AuthCredentialView { - - private LockPatternView mLockPatternView; - - private class UnlockPatternListener implements LockPatternView.OnPatternListener { - - @Override - public void onPatternStart() { - - } - - @Override - public void onPatternCleared() { - - } - - @Override - public void onPatternCellAdded(List<LockPatternView.Cell> pattern) { - - } - - @Override - public void onPatternDetected(List<LockPatternView.Cell> pattern) { - if (mPendingLockCheck != null) { - mPendingLockCheck.cancel(false); - } - - mLockPatternView.setEnabled(false); - - if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { - // Pattern size is less than the minimum, do not count it as a failed attempt. - onPatternVerified(VerifyCredentialResponse.ERROR, 0 /* timeoutMs */); - return; - } - - try (LockscreenCredential credential = LockscreenCredential.createPattern(pattern)) { - // Request LockSettingsService to return the Gatekeeper Password in the - // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the - // Gatekeeper Password and operationId. - mPendingLockCheck = LockPatternChecker.verifyCredential( - mLockPatternUtils, - credential, - mEffectiveUserId, - LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, - this::onPatternVerified); - } - } - - private void onPatternVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) { - AuthCredentialPatternView.this.onCredentialVerified(response, timeoutMs); - if (timeoutMs > 0) { - mLockPatternView.setEnabled(false); - } else { - mLockPatternView.setEnabled(true); - } - } - } - - @Override - protected void onErrorTimeoutFinish() { - super.onErrorTimeoutFinish(); - // select to enable marquee unless a screen reader is enabled - mLockPatternView.setEnabled(!mAccessibilityManager.isEnabled() - || !mAccessibilityManager.isTouchExplorationEnabled()); - } - - public AuthCredentialPatternView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - mLockPatternView = findViewById(R.id.lockPattern); - mLockPatternView.setOnPatternListener(new UnlockPatternListener()); - mLockPatternView.setInStealthMode( - !mLockPatternUtils.isVisiblePatternEnabled(mUserId)); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java deleted file mode 100644 index fa623d146756..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthCredentialView.java +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.biometrics; - -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT; -import static android.app.admin.DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT; -import static android.app.admin.DevicePolicyResources.UNDEFINED; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.AlertDialog; -import android.app.admin.DevicePolicyManager; -import android.content.Context; -import android.content.pm.UserInfo; -import android.graphics.drawable.Drawable; -import android.hardware.biometrics.BiometricPrompt; -import android.hardware.biometrics.PromptInfo; -import android.os.AsyncTask; -import android.os.CountDownTimer; -import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; -import android.os.UserManager; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.StringRes; - -import com.android.internal.widget.LockPatternUtils; -import com.android.internal.widget.VerifyCredentialResponse; -import com.android.systemui.R; -import com.android.systemui.animation.Interpolators; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Abstract base class for Pin, Pattern, or Password authentication, for - * {@link BiometricPrompt.Builder#setAllowedAuthenticators(int)}} - */ -public abstract class AuthCredentialView extends LinearLayout { - private static final String TAG = "BiometricPrompt/AuthCredentialView"; - private static final int ERROR_DURATION_MS = 3000; - - static final int USER_TYPE_PRIMARY = 1; - static final int USER_TYPE_MANAGED_PROFILE = 2; - static final int USER_TYPE_SECONDARY = 3; - @Retention(RetentionPolicy.SOURCE) - @IntDef({USER_TYPE_PRIMARY, USER_TYPE_MANAGED_PROFILE, USER_TYPE_SECONDARY}) - private @interface UserType {} - - protected final Handler mHandler; - protected final LockPatternUtils mLockPatternUtils; - - protected final AccessibilityManager mAccessibilityManager; - private final UserManager mUserManager; - private final DevicePolicyManager mDevicePolicyManager; - - private PromptInfo mPromptInfo; - private AuthPanelController mPanelController; - private boolean mShouldAnimatePanel; - private boolean mShouldAnimateContents; - - protected TextView mTitleView; - protected TextView mSubtitleView; - protected TextView mDescriptionView; - protected ImageView mIconView; - protected TextView mErrorView; - - protected @Utils.CredentialType int mCredentialType; - protected AuthContainerView mContainerView; - protected Callback mCallback; - protected AsyncTask<?, ?, ?> mPendingLockCheck; - protected int mUserId; - protected long mOperationId; - protected int mEffectiveUserId; - protected ErrorTimer mErrorTimer; - - protected @Background DelayableExecutor mBackgroundExecutor; - - interface Callback { - void onCredentialMatched(byte[] attestation); - } - - protected static class ErrorTimer extends CountDownTimer { - private final TextView mErrorView; - private final Context mContext; - - /** - * @param millisInFuture The number of millis in the future from the call - * to {@link #start()} until the countdown is done and {@link - * #onFinish()} - * is called. - * @param countDownInterval The interval along the way to receive - * {@link #onTick(long)} callbacks. - */ - public ErrorTimer(Context context, long millisInFuture, long countDownInterval, - TextView errorView) { - super(millisInFuture, countDownInterval); - mErrorView = errorView; - mContext = context; - } - - @Override - public void onTick(long millisUntilFinished) { - final int secondsCountdown = (int) (millisUntilFinished / 1000); - mErrorView.setText(mContext.getString( - R.string.biometric_dialog_credential_too_many_attempts, secondsCountdown)); - } - - @Override - public void onFinish() { - if (mErrorView != null) { - mErrorView.setText(""); - } - } - } - - protected final Runnable mClearErrorRunnable = new Runnable() { - @Override - public void run() { - if (mErrorView != null) { - mErrorView.setText(""); - } - } - }; - - public AuthCredentialView(Context context, AttributeSet attrs) { - super(context, attrs); - - mLockPatternUtils = new LockPatternUtils(mContext); - mHandler = new Handler(Looper.getMainLooper()); - mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); - mUserManager = mContext.getSystemService(UserManager.class); - mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); - } - - protected void showError(String error) { - if (mHandler != null) { - mHandler.removeCallbacks(mClearErrorRunnable); - mHandler.postDelayed(mClearErrorRunnable, ERROR_DURATION_MS); - } - if (mErrorView != null) { - mErrorView.setText(error); - } - } - - private void setTextOrHide(TextView view, CharSequence text) { - if (TextUtils.isEmpty(text)) { - view.setVisibility(View.GONE); - } else { - view.setText(text); - } - - Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); - } - - private void setText(TextView view, CharSequence text) { - view.setText(text); - } - - void setUserId(int userId) { - mUserId = userId; - } - - void setOperationId(long operationId) { - mOperationId = operationId; - } - - void setEffectiveUserId(int effectiveUserId) { - mEffectiveUserId = effectiveUserId; - } - - void setCredentialType(@Utils.CredentialType int credentialType) { - mCredentialType = credentialType; - } - - void setCallback(Callback callback) { - mCallback = callback; - } - - void setPromptInfo(PromptInfo promptInfo) { - mPromptInfo = promptInfo; - } - - void setPanelController(AuthPanelController panelController, boolean animatePanel) { - mPanelController = panelController; - mShouldAnimatePanel = animatePanel; - } - - void setShouldAnimateContents(boolean animateContents) { - mShouldAnimateContents = animateContents; - } - - void setContainerView(AuthContainerView containerView) { - mContainerView = containerView; - } - - void setBackgroundExecutor(@Background DelayableExecutor bgExecutor) { - mBackgroundExecutor = bgExecutor; - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - - final CharSequence title = getTitle(mPromptInfo); - setText(mTitleView, title); - setTextOrHide(mSubtitleView, getSubtitle(mPromptInfo)); - setTextOrHide(mDescriptionView, getDescription(mPromptInfo)); - announceForAccessibility(title); - - if (mIconView != null) { - final boolean isManagedProfile = Utils.isManagedProfile(mContext, mEffectiveUserId); - final Drawable image; - if (isManagedProfile) { - image = getResources().getDrawable(R.drawable.auth_dialog_enterprise, - mContext.getTheme()); - } else { - image = getResources().getDrawable(R.drawable.auth_dialog_lock, - mContext.getTheme()); - } - mIconView.setImageDrawable(image); - } - - // Only animate this if we're transitioning from a biometric view. - if (mShouldAnimateContents) { - setTranslationY(getResources() - .getDimension(R.dimen.biometric_dialog_credential_translation_offset)); - setAlpha(0); - - postOnAnimation(() -> { - animate().translationY(0) - .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS) - .alpha(1.f) - .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) - .withLayer() - .start(); - }); - } - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mErrorTimer != null) { - mErrorTimer.cancel(); - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTitleView = findViewById(R.id.title); - mSubtitleView = findViewById(R.id.subtitle); - mDescriptionView = findViewById(R.id.description); - mIconView = findViewById(R.id.icon); - mErrorView = findViewById(R.id.error); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - if (mShouldAnimatePanel) { - // Credential view is always full screen. - mPanelController.setUseFullScreen(true); - mPanelController.updateForContentDimensions(mPanelController.getContainerWidth(), - mPanelController.getContainerHeight(), 0 /* animateDurationMs */); - mShouldAnimatePanel = false; - } - } - - protected void onErrorTimeoutFinish() {} - - protected void onCredentialVerified(@NonNull VerifyCredentialResponse response, int timeoutMs) { - if (response.isMatched()) { - mClearErrorRunnable.run(); - mLockPatternUtils.userPresent(mEffectiveUserId); - - // The response passed into this method contains the Gatekeeper Password. We still - // have to request Gatekeeper to create a Hardware Auth Token with the - // Gatekeeper Password and Challenge (keystore operationId in this case) - final long pwHandle = response.getGatekeeperPasswordHandle(); - final VerifyCredentialResponse gkResponse = mLockPatternUtils - .verifyGatekeeperPasswordHandle(pwHandle, mOperationId, mEffectiveUserId); - - mCallback.onCredentialMatched(gkResponse.getGatekeeperHAT()); - mLockPatternUtils.removeGatekeeperPasswordHandle(pwHandle); - } else { - if (timeoutMs > 0) { - mHandler.removeCallbacks(mClearErrorRunnable); - long deadline = mLockPatternUtils.setLockoutAttemptDeadline( - mEffectiveUserId, timeoutMs); - mErrorTimer = new ErrorTimer(mContext, - deadline - SystemClock.elapsedRealtime(), - LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS, - mErrorView) { - @Override - public void onFinish() { - onErrorTimeoutFinish(); - mClearErrorRunnable.run(); - } - }; - mErrorTimer.start(); - } else { - final boolean didUpdateErrorText = reportFailedAttempt(); - if (!didUpdateErrorText) { - final @StringRes int errorRes; - switch (mCredentialType) { - case Utils.CREDENTIAL_PIN: - errorRes = R.string.biometric_dialog_wrong_pin; - break; - case Utils.CREDENTIAL_PATTERN: - errorRes = R.string.biometric_dialog_wrong_pattern; - break; - case Utils.CREDENTIAL_PASSWORD: - default: - errorRes = R.string.biometric_dialog_wrong_password; - break; - } - showError(getResources().getString(errorRes)); - } - } - } - } - - private boolean reportFailedAttempt() { - boolean result = updateErrorMessage( - mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId) + 1); - mLockPatternUtils.reportFailedPasswordAttempt(mEffectiveUserId); - return result; - } - - private boolean updateErrorMessage(int numAttempts) { - // Don't show any message if there's no maximum number of attempts. - final int maxAttempts = mLockPatternUtils.getMaximumFailedPasswordsForWipe( - mEffectiveUserId); - if (maxAttempts <= 0 || numAttempts <= 0) { - return false; - } - - // Update the on-screen error string. - if (mErrorView != null) { - final String message = getResources().getString( - R.string.biometric_dialog_credential_attempts_before_wipe, - numAttempts, - maxAttempts); - showError(message); - } - - // Only show dialog if <=1 attempts are left before wiping. - final int remainingAttempts = maxAttempts - numAttempts; - if (remainingAttempts == 1) { - showLastAttemptBeforeWipeDialog(); - } else if (remainingAttempts <= 0) { - showNowWipingDialog(); - } - return true; - } - - private void showLastAttemptBeforeWipeDialog() { - mBackgroundExecutor.execute(() -> { - final AlertDialog alertDialog = new AlertDialog.Builder(mContext) - .setTitle(R.string.biometric_dialog_last_attempt_before_wipe_dialog_title) - .setMessage( - getLastAttemptBeforeWipeMessage(getUserTypeForWipe(), mCredentialType)) - .setPositiveButton(android.R.string.ok, null) - .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); - mHandler.post(alertDialog::show); - }); - } - - private void showNowWipingDialog() { - mBackgroundExecutor.execute(() -> { - String nowWipingMessage = getNowWipingMessage(getUserTypeForWipe()); - final AlertDialog alertDialog = new AlertDialog.Builder(mContext) - .setMessage(nowWipingMessage) - .setPositiveButton( - com.android.settingslib.R.string.failed_attempts_now_wiping_dialog_dismiss, - null /* OnClickListener */) - .setOnDismissListener( - dialog -> mContainerView.animateAway( - AuthDialogCallback.DISMISSED_ERROR)) - .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); - mHandler.post(alertDialog::show); - }); - } - - private @UserType int getUserTypeForWipe() { - final UserInfo userToBeWiped = mUserManager.getUserInfo( - mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(mEffectiveUserId)); - if (userToBeWiped == null || userToBeWiped.isPrimary()) { - return USER_TYPE_PRIMARY; - } else if (userToBeWiped.isManagedProfile()) { - return USER_TYPE_MANAGED_PROFILE; - } else { - return USER_TYPE_SECONDARY; - } - } - - // This should not be called on the main thread to avoid making an IPC. - private String getLastAttemptBeforeWipeMessage( - @UserType int userType, @Utils.CredentialType int credentialType) { - switch (userType) { - case USER_TYPE_PRIMARY: - return getLastAttemptBeforeWipeDeviceMessage(credentialType); - case USER_TYPE_MANAGED_PROFILE: - return getLastAttemptBeforeWipeProfileMessage(credentialType); - case USER_TYPE_SECONDARY: - return getLastAttemptBeforeWipeUserMessage(credentialType); - default: - throw new IllegalArgumentException("Unrecognized user type:" + userType); - } - } - - private String getLastAttemptBeforeWipeDeviceMessage( - @Utils.CredentialType int credentialType) { - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - return mContext.getString( - R.string.biometric_dialog_last_pin_attempt_before_wipe_device); - case Utils.CREDENTIAL_PATTERN: - return mContext.getString( - R.string.biometric_dialog_last_pattern_attempt_before_wipe_device); - case Utils.CREDENTIAL_PASSWORD: - default: - return mContext.getString( - R.string.biometric_dialog_last_password_attempt_before_wipe_device); - } - } - - // This should not be called on the main thread to avoid making an IPC. - private String getLastAttemptBeforeWipeProfileMessage( - @Utils.CredentialType int credentialType) { - return mDevicePolicyManager.getResources().getString( - getLastAttemptBeforeWipeProfileUpdatableStringId(credentialType), - () -> getLastAttemptBeforeWipeProfileDefaultMessage(credentialType)); - } - - private static String getLastAttemptBeforeWipeProfileUpdatableStringId( - @Utils.CredentialType int credentialType) { - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - return BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT; - case Utils.CREDENTIAL_PATTERN: - return BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT; - case Utils.CREDENTIAL_PASSWORD: - default: - return BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT; - } - } - - private String getLastAttemptBeforeWipeProfileDefaultMessage( - @Utils.CredentialType int credentialType) { - int resId; - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_profile; - break; - case Utils.CREDENTIAL_PATTERN: - resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile; - break; - case Utils.CREDENTIAL_PASSWORD: - default: - resId = R.string.biometric_dialog_last_password_attempt_before_wipe_profile; - } - return mContext.getString(resId); - } - - private String getLastAttemptBeforeWipeUserMessage( - @Utils.CredentialType int credentialType) { - int resId; - switch (credentialType) { - case Utils.CREDENTIAL_PIN: - resId = R.string.biometric_dialog_last_pin_attempt_before_wipe_user; - break; - case Utils.CREDENTIAL_PATTERN: - resId = R.string.biometric_dialog_last_pattern_attempt_before_wipe_user; - break; - case Utils.CREDENTIAL_PASSWORD: - default: - resId = R.string.biometric_dialog_last_password_attempt_before_wipe_user; - } - return mContext.getString(resId); - } - - private String getNowWipingMessage(@UserType int userType) { - return mDevicePolicyManager.getResources().getString( - getNowWipingUpdatableStringId(userType), - () -> getNowWipingDefaultMessage(userType)); - } - - private String getNowWipingUpdatableStringId(@UserType int userType) { - switch (userType) { - case USER_TYPE_MANAGED_PROFILE: - return BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS; - default: - return UNDEFINED; - } - } - - private String getNowWipingDefaultMessage(@UserType int userType) { - int resId; - switch (userType) { - case USER_TYPE_PRIMARY: - resId = com.android.settingslib.R.string.failed_attempts_now_wiping_device; - break; - case USER_TYPE_MANAGED_PROFILE: - resId = com.android.settingslib.R.string.failed_attempts_now_wiping_profile; - break; - case USER_TYPE_SECONDARY: - resId = com.android.settingslib.R.string.failed_attempts_now_wiping_user; - break; - default: - throw new IllegalArgumentException("Unrecognized user type:" + userType); - } - return mContext.getString(resId); - } - - @Nullable - private static CharSequence getTitle(@NonNull PromptInfo promptInfo) { - final CharSequence credentialTitle = promptInfo.getDeviceCredentialTitle(); - return credentialTitle != null ? credentialTitle : promptInfo.getTitle(); - } - - @Nullable - private static CharSequence getSubtitle(@NonNull PromptInfo promptInfo) { - final CharSequence credentialSubtitle = promptInfo.getDeviceCredentialSubtitle(); - return credentialSubtitle != null ? credentialSubtitle : promptInfo.getSubtitle(); - } - - @Nullable - private static CharSequence getDescription(@NonNull PromptInfo promptInfo) { - final CharSequence credentialDescription = promptInfo.getDeviceCredentialDescription(); - return credentialDescription != null ? credentialDescription : promptInfo.getDescription(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java index f1e42e0c5454..5c616f005d4d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java @@ -177,11 +177,11 @@ public class AuthPanelController extends ViewOutlineProvider { } } - int getContainerWidth() { + public int getContainerWidth() { return mContainerWidth; } - int getContainerHeight() { + public int getContainerHeight() { return mContainerHeight; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index b5d81f253916..7c0c3b710e66 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -16,32 +16,45 @@ package com.android.systemui.biometrics.dagger +import com.android.systemui.biometrics.data.repository.PromptRepository +import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl +import com.android.systemui.biometrics.domain.interactor.CredentialInteractor +import com.android.systemui.biometrics.domain.interactor.CredentialInteractorImpl import com.android.systemui.dagger.SysUISingleton import com.android.systemui.util.concurrency.ThreadFactory +import dagger.Binds import dagger.Module import dagger.Provides import java.util.concurrent.Executor import javax.inject.Qualifier -/** - * Dagger module for all things biometric. - */ +/** Dagger module for all things biometric. */ @Module -object BiometricsModule { +interface BiometricsModule { - /** Background [Executor] for HAL related operations. */ - @Provides + @Binds @SysUISingleton - @JvmStatic - @BiometricsBackground - fun providesPluginExecutor(threadFactory: ThreadFactory): Executor = - threadFactory.buildExecutorOnNewThread("biometrics") + fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository + + @Binds + @SysUISingleton + fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor + + companion object { + /** Background [Executor] for HAL related operations. */ + @Provides + @SysUISingleton + @JvmStatic + @BiometricsBackground + fun providesPluginExecutor(threadFactory: ThreadFactory): Executor = + threadFactory.buildExecutorOnNewThread("biometrics") + } } /** - * Background executor for HAL operations that are latency sensitive but too - * slow to run on the main thread. Prefer the shared executors, such as - * [com.android.systemui.dagger.qualifiers.Background] when a HAL is not directly involved. + * Background executor for HAL operations that are latency sensitive but too slow to run on the main + * thread. Prefer the shared executors, such as [com.android.systemui.dagger.qualifiers.Background] + * when a HAL is not directly involved. */ @Qualifier @MustBeDocumented diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt new file mode 100644 index 000000000000..e82646f0d861 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/model/PromptKind.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.data.model + +import com.android.systemui.biometrics.Utils + +// TODO(b/251476085): this should eventually replace Utils.CredentialType +/** Credential options for biometric prompt. Shadows [Utils.CredentialType]. */ +enum class PromptKind { + ANY_BIOMETRIC, + PIN, + PATTERN, + PASSWORD, +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt new file mode 100644 index 000000000000..92a13cfe538b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -0,0 +1,102 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * A repository for the global state of BiometricPrompt. + * + * There is never more than one instance of the prompt at any given time. + */ +interface PromptRepository { + + /** If the prompt is showing. */ + val isShowing: Flow<Boolean> + + /** The app-specific details to show in the prompt. */ + val promptInfo: StateFlow<PromptInfo?> + + /** The user that the prompt is for. */ + val userId: StateFlow<Int?> + + /** The gatekeeper challenge, if one is associated with this prompt. */ + val challenge: StateFlow<Long?> + + /** The kind of credential to use (biometric, pin, pattern, etc.). */ + val kind: StateFlow<PromptKind> + + /** Update the prompt configuration, which should be set before [isShowing]. */ + fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind = PromptKind.ANY_BIOMETRIC, + ) + + /** Unset the prompt info. */ + fun unsetPrompt() +} + +@SysUISingleton +class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) : + PromptRepository { + + override val isShowing: Flow<Boolean> = conflatedCallbackFlow { + val callback = + object : AuthController.Callback { + override fun onBiometricPromptShown() = + trySendWithFailureLogging(true, TAG, "set isShowing") + + override fun onBiometricPromptDismissed() = + trySendWithFailureLogging(false, TAG, "unset isShowing") + } + authController.addCallback(callback) + trySendWithFailureLogging(authController.isShowing, TAG, "update isShowing") + awaitClose { authController.removeCallback(callback) } + } + + private val _promptInfo: MutableStateFlow<PromptInfo?> = MutableStateFlow(null) + override val promptInfo = _promptInfo.asStateFlow() + + private val _challenge: MutableStateFlow<Long?> = MutableStateFlow(null) + override val challenge: StateFlow<Long?> = _challenge.asStateFlow() + + private val _userId: MutableStateFlow<Int?> = MutableStateFlow(null) + override val userId = _userId.asStateFlow() + + private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.ANY_BIOMETRIC) + override val kind = _kind.asStateFlow() + + override fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind, + ) { + _kind.value = kind + _userId.value = userId + _challenge.value = gatekeeperChallenge + _promptInfo.value = promptInfo + } + + override fun unsetPrompt() { + _promptInfo.value = null + _userId.value = null + _challenge.value = null + _kind.value = PromptKind.ANY_BIOMETRIC + } + + companion object { + private const val TAG = "BiometricPromptRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt new file mode 100644 index 000000000000..1f1a1b5c83bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -0,0 +1,282 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources +import android.content.Context +import android.os.UserManager +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockscreenCredential +import com.android.internal.widget.VerifyCredentialResponse +import com.android.systemui.R +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * A wrapper for [LockPatternUtils] to verify PIN, pattern, or password credentials. + * + * This class also uses the [DevicePolicyManager] to generate appropriate error messages when policy + * exceptions are raised (i.e. wipe device due to excessive failed attempts, etc.). + */ +interface CredentialInteractor { + /** If the user's pattern credential should be hidden */ + fun isStealthModeActive(userId: Int): Boolean + + /** Get the effective user id (profile owner, if one exists) */ + fun getCredentialOwnerOrSelfId(userId: Int): Int + + /** + * Verifies a credential and returns a stream of results. + * + * The final emitted value will either be a [CredentialStatus.Fail.Error] or a + * [CredentialStatus.Success.Verified]. + */ + fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential, + ): Flow<CredentialStatus> +} + +/** Standard implementation of [CredentialInteractor]. */ +class CredentialInteractorImpl +@Inject +constructor( + @Application private val applicationContext: Context, + private val lockPatternUtils: LockPatternUtils, + private val userManager: UserManager, + private val devicePolicyManager: DevicePolicyManager, + private val systemClock: SystemClock, +) : CredentialInteractor { + + override fun isStealthModeActive(userId: Int): Boolean = + !lockPatternUtils.isVisiblePatternEnabled(userId) + + override fun getCredentialOwnerOrSelfId(userId: Int): Int = + userManager.getCredentialOwnerProfile(userId) + + override fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential, + ): Flow<CredentialStatus> = flow { + // Request LockSettingsService to return the Gatekeeper Password in the + // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the + // Gatekeeper Password and operationId. + val effectiveUserId = request.userInfo.deviceCredentialOwnerId + val response = + lockPatternUtils.verifyCredential( + credential, + effectiveUserId, + LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE + ) + + if (response.isMatched) { + lockPatternUtils.userPresent(effectiveUserId) + + // The response passed into this method contains the Gatekeeper + // Password. We still have to request Gatekeeper to create a + // Hardware Auth Token with the Gatekeeper Password and Challenge + // (keystore operationId in this case) + val pwHandle = response.gatekeeperPasswordHandle + val gkResponse: VerifyCredentialResponse = + lockPatternUtils.verifyGatekeeperPasswordHandle( + pwHandle, + request.operationInfo.gatekeeperChallenge, + effectiveUserId + ) + val hat = gkResponse.gatekeeperHAT + lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle) + emit(CredentialStatus.Success.Verified(hat)) + } else if (response.timeout > 0) { + // if requests are being throttled, update the error message every + // second until the temporary lock has expired + val deadline: Long = + lockPatternUtils.setLockoutAttemptDeadline(effectiveUserId, response.timeout) + val interval = LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS + var remaining = deadline - systemClock.elapsedRealtime() + while (remaining > 0) { + emit( + CredentialStatus.Fail.Throttled( + applicationContext.getString( + R.string.biometric_dialog_credential_too_many_attempts, + remaining / 1000 + ) + ) + ) + delay(interval) + remaining -= interval + } + emit(CredentialStatus.Fail.Error("")) + } else { // bad request, but not throttled + val numAttempts = lockPatternUtils.getCurrentFailedPasswordAttempts(effectiveUserId) + 1 + val maxAttempts = lockPatternUtils.getMaximumFailedPasswordsForWipe(effectiveUserId) + if (maxAttempts <= 0 || numAttempts <= 0) { + // use a generic message if there's no maximum number of attempts + emit(CredentialStatus.Fail.Error()) + } else { + val remainingAttempts = (maxAttempts - numAttempts).coerceAtLeast(0) + emit( + CredentialStatus.Fail.Error( + applicationContext.getString( + R.string.biometric_dialog_credential_attempts_before_wipe, + numAttempts, + maxAttempts + ), + remainingAttempts, + fetchFinalAttemptMessageOrNull(request, remainingAttempts) + ) + ) + } + lockPatternUtils.reportFailedPasswordAttempt(effectiveUserId) + } + } + + private fun fetchFinalAttemptMessageOrNull( + request: BiometricPromptRequest.Credential, + remainingAttempts: Int?, + ): String? = + if (remainingAttempts != null && remainingAttempts <= 1) { + applicationContext.getFinalAttemptMessageOrBlank( + request, + devicePolicyManager, + userManager.getUserTypeForWipe( + devicePolicyManager, + request.userInfo.deviceCredentialOwnerId + ), + remainingAttempts + ) + } else { + null + } +} + +private enum class UserType { + PRIMARY, + MANAGED_PROFILE, + SECONDARY, +} + +private fun UserManager.getUserTypeForWipe( + devicePolicyManager: DevicePolicyManager, + effectiveUserId: Int, +): UserType { + val userToBeWiped = + getUserInfo( + devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(effectiveUserId) + ) + return when { + userToBeWiped == null || userToBeWiped.isPrimary -> UserType.PRIMARY + userToBeWiped.isManagedProfile -> UserType.MANAGED_PROFILE + else -> UserType.SECONDARY + } +} + +private fun Context.getFinalAttemptMessageOrBlank( + request: BiometricPromptRequest.Credential, + devicePolicyManager: DevicePolicyManager, + userType: UserType, + remaining: Int, +): String = + when { + remaining == 1 -> getLastAttemptBeforeWipeMessage(request, devicePolicyManager, userType) + remaining <= 0 -> getNowWipingMessage(devicePolicyManager, userType) + else -> "" + } + +private fun Context.getLastAttemptBeforeWipeMessage( + request: BiometricPromptRequest.Credential, + devicePolicyManager: DevicePolicyManager, + userType: UserType, +): String = + when (userType) { + UserType.PRIMARY -> getLastAttemptBeforeWipeDeviceMessage(request) + UserType.MANAGED_PROFILE -> + getLastAttemptBeforeWipeProfileMessage(request, devicePolicyManager) + UserType.SECONDARY -> getLastAttemptBeforeWipeUserMessage(request) + } + +private fun Context.getLastAttemptBeforeWipeDeviceMessage( + request: BiometricPromptRequest.Credential, +): String { + val id = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + R.string.biometric_dialog_last_pin_attempt_before_wipe_device + is BiometricPromptRequest.Credential.Pattern -> + R.string.biometric_dialog_last_pattern_attempt_before_wipe_device + is BiometricPromptRequest.Credential.Password -> + R.string.biometric_dialog_last_password_attempt_before_wipe_device + } + return getString(id) +} + +private fun Context.getLastAttemptBeforeWipeProfileMessage( + request: BiometricPromptRequest.Credential, + devicePolicyManager: DevicePolicyManager, +): String { + val id = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT + is BiometricPromptRequest.Credential.Pattern -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT + is BiometricPromptRequest.Credential.Password -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT + } + return devicePolicyManager.resources.getString(id) { + // use fallback a string if not found + val defaultId = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + R.string.biometric_dialog_last_pin_attempt_before_wipe_profile + is BiometricPromptRequest.Credential.Pattern -> + R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile + is BiometricPromptRequest.Credential.Password -> + R.string.biometric_dialog_last_password_attempt_before_wipe_profile + } + getString(defaultId) + } +} + +private fun Context.getLastAttemptBeforeWipeUserMessage( + request: BiometricPromptRequest.Credential, +): String { + val resId = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + R.string.biometric_dialog_last_pin_attempt_before_wipe_user + is BiometricPromptRequest.Credential.Pattern -> + R.string.biometric_dialog_last_pattern_attempt_before_wipe_user + is BiometricPromptRequest.Credential.Password -> + R.string.biometric_dialog_last_password_attempt_before_wipe_user + } + return getString(resId) +} + +private fun Context.getNowWipingMessage( + devicePolicyManager: DevicePolicyManager, + userType: UserType, +): String { + val id = + when (userType) { + UserType.MANAGED_PROFILE -> + DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS + else -> DevicePolicyResources.UNDEFINED + } + return devicePolicyManager.resources.getString(id) { + // use fallback a string if not found + val defaultId = + when (userType) { + UserType.PRIMARY -> + com.android.settingslib.R.string.failed_attempts_now_wiping_device + UserType.MANAGED_PROFILE -> + com.android.settingslib.R.string.failed_attempts_now_wiping_profile + UserType.SECONDARY -> + com.android.settingslib.R.string.failed_attempts_now_wiping_user + } + getString(defaultId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt new file mode 100644 index 000000000000..40b76121f237 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialStatus.kt @@ -0,0 +1,23 @@ +package com.android.systemui.biometrics.domain.interactor + +/** Result of a [CredentialInteractor.verifyCredential] check. */ +sealed interface CredentialStatus { + /** A successful result. */ + sealed interface Success : CredentialStatus { + /** The credential is valid and a [hat] has been generated. */ + data class Verified(val hat: ByteArray) : Success + } + /** A failed result. */ + sealed interface Fail : CredentialStatus { + val error: String? + + /** The credential check failed with an [error]. */ + data class Error( + override val error: String? = null, + val remainingAttempts: Int? = null, + val urgentMessage: String? = null, + ) : Fail + /** The credential check failed with an [error] and is temporarily locked out. */ + data class Throttled(override val error: String) : Fail + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt new file mode 100644 index 000000000000..6362c2f627d3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -0,0 +1,189 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.hardware.biometrics.PromptInfo +import com.android.internal.widget.LockPatternView +import com.android.internal.widget.LockscreenCredential +import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.biometrics.data.repository.PromptRepository +import com.android.systemui.biometrics.domain.model.BiometricOperationInfo +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.biometrics.domain.model.BiometricUserInfo +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.lastOrNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext + +/** + * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users + * PIN, pattern, or password credential instead of a biometric. + */ +class BiometricPromptCredentialInteractor +@Inject +constructor( + @Background private val bgDispatcher: CoroutineDispatcher, + private val biometricPromptRepository: PromptRepository, + private val credentialInteractor: CredentialInteractor, +) { + /** If the prompt is currently showing. */ + val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing + + /** Metadata about the current credential prompt, including app-supplied preferences. */ + val prompt: Flow<BiometricPromptRequest?> = + combine( + biometricPromptRepository.promptInfo, + biometricPromptRepository.challenge, + biometricPromptRepository.userId, + biometricPromptRepository.kind + ) { promptInfo, challenge, userId, kind -> + if (promptInfo == null || userId == null || challenge == null) { + return@combine null + } + + when (kind) { + PromptKind.PIN -> + BiometricPromptRequest.Credential.Pin( + info = promptInfo, + userInfo = userInfo(userId), + operationInfo = operationInfo(challenge) + ) + PromptKind.PATTERN -> + BiometricPromptRequest.Credential.Pattern( + info = promptInfo, + userInfo = userInfo(userId), + operationInfo = operationInfo(challenge), + stealthMode = credentialInteractor.isStealthModeActive(userId) + ) + PromptKind.PASSWORD -> + BiometricPromptRequest.Credential.Password( + info = promptInfo, + userInfo = userInfo(userId), + operationInfo = operationInfo(challenge) + ) + else -> null + } + } + .distinctUntilChanged() + + private fun userInfo(userId: Int): BiometricUserInfo = + BiometricUserInfo( + userId = userId, + deviceCredentialOwnerId = credentialInteractor.getCredentialOwnerOrSelfId(userId) + ) + + private fun operationInfo(challenge: Long): BiometricOperationInfo = + BiometricOperationInfo(gatekeeperChallenge = challenge) + + /** Most recent error due to [verifyCredential]. */ + private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null) + val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow() + + /** Update the current request to use credential-based authentication instead of biometrics. */ + fun useCredentialsForAuthentication( + promptInfo: PromptInfo, + @Utils.CredentialType kind: Int, + userId: Int, + challenge: Long, + ) { + biometricPromptRepository.setPrompt( + promptInfo, + userId, + challenge, + kind.asBiometricPromptCredential() + ) + } + + /** Unset the current authentication request. */ + fun resetPrompt() { + biometricPromptRepository.unsetPrompt() + } + + /** + * Check a credential and return the attestation token (HAT) if successful. + * + * This method will not return if credential checks are being throttled until the throttling has + * expired and the user can try again. It will periodically update the [verificationError] until + * cancelled or the throttling has completed. If the request is not throttled, but unsuccessful, + * the [verificationError] will be set and an optional + * [CredentialStatus.Fail.Error.urgentMessage] message may be provided to indicate additional + * hints to the user (i.e. device will be wiped on next failure, etc.). + * + * The check happens on the background dispatcher given in the constructor. + */ + suspend fun checkCredential( + request: BiometricPromptRequest.Credential, + text: CharSequence? = null, + pattern: List<LockPatternView.Cell>? = null, + ): CredentialStatus = + withContext(bgDispatcher) { + val credential = + when (request) { + is BiometricPromptRequest.Credential.Pin -> + LockscreenCredential.createPinOrNone(text ?: "") + is BiometricPromptRequest.Credential.Password -> + LockscreenCredential.createPasswordOrNone(text ?: "") + is BiometricPromptRequest.Credential.Pattern -> + LockscreenCredential.createPattern(pattern ?: listOf()) + } + + credential.use { c -> verifyCredential(request, c) } + } + + private suspend fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential? + ): CredentialStatus { + if (credential == null || credential.isNone) { + return CredentialStatus.Fail.Error() + } + + val finalStatus = + credentialInteractor + .verifyCredential(request, credential) + .onEach { status -> + when (status) { + is CredentialStatus.Success -> _verificationError.value = null + is CredentialStatus.Fail -> _verificationError.value = status + } + } + .lastOrNull() + + return finalStatus ?: CredentialStatus.Fail.Error() + } + + /** + * Report a user-visible error. + * + * Use this instead of calling [verifyCredential] when it is not necessary because the check + * will obviously fail (i.e. too short, empty, etc.) + */ + fun setVerificationError(error: CredentialStatus.Fail.Error?) { + if (error != null) { + _verificationError.value = error + } else { + resetVerificationError() + } + } + + /** Clear the current error message, if any. */ + fun resetVerificationError() { + _verificationError.value = null + } +} + +// TODO(b/251476085): remove along with Utils.CredentialType +/** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */ +private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind = + when (this) { + Utils.CREDENTIAL_PIN -> PromptKind.PIN + Utils.CREDENTIAL_PASSWORD -> PromptKind.PASSWORD + Utils.CREDENTIAL_PATTERN -> PromptKind.PATTERN + else -> PromptKind.ANY_BIOMETRIC + } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt new file mode 100644 index 000000000000..c619b12361c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricOperationInfo.kt @@ -0,0 +1,4 @@ +package com.android.systemui.biometrics.domain.model + +/** Metadata about an in-progress biometric operation. */ +data class BiometricOperationInfo(val gatekeeperChallenge: Long = -1) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt new file mode 100644 index 000000000000..5ee0381db630 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -0,0 +1,69 @@ +package com.android.systemui.biometrics.domain.model + +import android.hardware.biometrics.PromptInfo + +/** + * Preferences for BiometricPrompt, such as title & description, that are immutable while the prompt + * is showing. + * + * This roughly corresponds to a "request" by the system or an app to show BiometricPrompt and it + * contains a subset of the information in a [PromptInfo] that is relevant to SysUI. + */ +sealed class BiometricPromptRequest( + val title: String, + val subtitle: String, + val description: String, + val userInfo: BiometricUserInfo, + val operationInfo: BiometricOperationInfo, +) { + /** Prompt using one or more biometrics. */ + class Biometric( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : + BiometricPromptRequest( + title = info.title?.toString() ?: "", + subtitle = info.subtitle?.toString() ?: "", + description = info.description?.toString() ?: "", + userInfo = userInfo, + operationInfo = operationInfo + ) + + /** Prompt using a credential (pin, pattern, password). */ + sealed class Credential( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : + BiometricPromptRequest( + title = (info.deviceCredentialTitle ?: info.title)?.toString() ?: "", + subtitle = (info.deviceCredentialSubtitle ?: info.subtitle)?.toString() ?: "", + description = (info.deviceCredentialDescription ?: info.description)?.toString() ?: "", + userInfo = userInfo, + operationInfo = operationInfo, + ) { + + /** PIN prompt. */ + class Pin( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : Credential(info, userInfo, operationInfo) + + /** Password prompt. */ + class Password( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + ) : Credential(info, userInfo, operationInfo) + + /** Pattern prompt. */ + class Pattern( + info: PromptInfo, + userInfo: BiometricUserInfo, + operationInfo: BiometricOperationInfo, + val stealthMode: Boolean, + ) : Credential(info, userInfo, operationInfo) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt new file mode 100644 index 000000000000..08da04d27606 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricUserInfo.kt @@ -0,0 +1,7 @@ +package com.android.systemui.biometrics.domain.model + +/** Metadata about the current user BiometricPrompt is being shown to. */ +data class BiometricUserInfo( + val userId: Int, + val deviceCredentialOwnerId: Int = userId, +) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt new file mode 100644 index 000000000000..bcc0575651e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt @@ -0,0 +1,130 @@ +package com.android.systemui.biometrics.ui + +import android.content.Context +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.text.TextUtils +import android.util.AttributeSet +import android.view.View +import android.view.WindowInsets +import android.view.WindowInsets.Type.ime +import android.view.accessibility.AccessibilityManager +import android.widget.ImageView +import android.widget.ImeAwareEditText +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isGone +import com.android.systemui.R +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel + +/** PIN or password credential view for BiometricPrompt. */ +class CredentialPasswordView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), CredentialView, View.OnApplyWindowInsetsListener { + + private lateinit var titleView: TextView + private lateinit var subtitleView: TextView + private lateinit var descriptionView: TextView + private lateinit var iconView: ImageView + private lateinit var passwordField: ImeAwareEditText + private lateinit var credentialHeader: View + private lateinit var credentialInput: View + + private var bottomInset: Int = 0 + + private val accessibilityManager by lazy { + context.getSystemService(AccessibilityManager::class.java) + } + + /** Initializes the view. */ + override fun init( + viewModel: CredentialViewModel, + host: CredentialView.Host, + panelViewController: AuthPanelController, + animatePanel: Boolean, + ) { + CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + } + + override fun onFinishInflate() { + super.onFinishInflate() + + titleView = requireViewById(R.id.title) + subtitleView = requireViewById(R.id.subtitle) + descriptionView = requireViewById(R.id.description) + iconView = requireViewById(R.id.icon) + subtitleView = requireViewById(R.id.subtitle) + passwordField = requireViewById(R.id.lockPassword) + credentialHeader = requireViewById(R.id.auth_credential_header) + credentialInput = requireViewById(R.id.auth_credential_input) + + setOnApplyWindowInsetsListener(this) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, left, top, right, bottom) + + val inputLeftBound: Int + val inputTopBound: Int + var headerRightBound = right + var headerTopBounds = top + val subTitleBottom: Int = if (subtitleView.isGone) titleView.bottom else subtitleView.bottom + val descBottom = if (descriptionView.isGone) subTitleBottom else descriptionView.bottom + if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { + inputTopBound = (bottom - credentialInput.height) / 2 + inputLeftBound = (right - left) / 2 + headerRightBound = inputLeftBound + headerTopBounds -= iconView.bottom.coerceAtMost(bottomInset) + } else { + inputTopBound = descBottom + (bottom - descBottom - credentialInput.height) / 2 + inputLeftBound = (right - left - credentialInput.width) / 2 + } + + if (descriptionView.bottom > bottomInset) { + credentialHeader.layout(left, headerTopBounds, headerRightBound, bottom) + } + credentialInput.layout(inputLeftBound, inputTopBound, right, bottom) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val newWidth = MeasureSpec.getSize(widthMeasureSpec) + val newHeight = MeasureSpec.getSize(heightMeasureSpec) - bottomInset + + setMeasuredDimension(newWidth, newHeight) + + val halfWidthSpec = MeasureSpec.makeMeasureSpec(width / 2, MeasureSpec.AT_MOST) + val fullHeightSpec = MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.UNSPECIFIED) + if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { + measureChildren(halfWidthSpec, fullHeightSpec) + } else { + measureChildren(widthMeasureSpec, fullHeightSpec) + } + } + + override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { + val bottomInsets = insets.getInsets(ime()) + if (bottomInset != bottomInsets.bottom) { + bottomInset = bottomInsets.bottom + + if (bottomInset > 0 && resources.configuration.orientation == ORIENTATION_LANDSCAPE) { + titleView.isSingleLine = true + titleView.ellipsize = TextUtils.TruncateAt.MARQUEE + titleView.marqueeRepeatLimit = -1 + // select to enable marquee unless a screen reader is enabled + titleView.isSelected = accessibilityManager.shouldMarquee() + } else { + titleView.isSingleLine = false + titleView.ellipsize = null + // select to enable marquee unless a screen reader is enabled + titleView.isSelected = false + } + + requestLayout() + } + return insets + } +} + +private fun AccessibilityManager.shouldMarquee(): Boolean = !isEnabled || !isTouchExplorationEnabled diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt new file mode 100644 index 000000000000..75331f083851 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt @@ -0,0 +1,23 @@ +package com.android.systemui.biometrics.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel + +/** Pattern credential view for BiometricPrompt. */ +class CredentialPatternView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), CredentialView { + + /** Initializes the view. */ + override fun init( + viewModel: CredentialViewModel, + host: CredentialView.Host, + panelViewController: AuthPanelController, + animatePanel: Boolean, + ) { + CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt new file mode 100644 index 000000000000..b7c6a4566108 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt @@ -0,0 +1,31 @@ +package com.android.systemui.biometrics.ui + +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel + +/** A credential variant of BiometricPrompt. */ +sealed interface CredentialView { + /** + * Callbacks for the "host" container view that contains this credential view. + * + * TODO(b/251476085): Removed when the host view is converted to use a parent view model. + */ + interface Host { + /** When the user's credential has been verified. */ + fun onCredentialMatched(attestation: ByteArray) + + /** When the user abandons credential verification. */ + fun onCredentialAborted() + + /** Warn the user is warned about excessive attempts. */ + fun onCredentialAttemptsRemaining(remaining: Int, messageBody: String) + } + + // TODO(251476085): remove AuthPanelController + fun init( + viewModel: CredentialViewModel, + host: Host, + panelViewController: AuthPanelController, + animatePanel: Boolean, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt new file mode 100644 index 000000000000..c619648a314c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt @@ -0,0 +1,104 @@ +package com.android.systemui.biometrics.ui.binder + +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.ImeAwareEditText +import android.widget.TextView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.biometrics.ui.CredentialPasswordView +import com.android.systemui.biometrics.ui.CredentialView +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** Sub-binder for the [CredentialPasswordView]. */ +object CredentialPasswordViewBinder { + + /** Bind the view. */ + fun bind( + view: CredentialPasswordView, + host: CredentialView.Host, + viewModel: CredentialViewModel, + ) { + val imeManager = view.context.getSystemService(InputMethodManager::class.java)!! + + val passwordField: ImeAwareEditText = view.requireViewById(R.id.lockPassword) + + view.repeatWhenAttached { + passwordField.requestFocus() + passwordField.scheduleShowSoftInput() + + repeatOnLifecycle(Lifecycle.State.STARTED) { + // observe credential validation attempts and submit/cancel buttons + launch { + viewModel.header.collect { header -> + passwordField.setTextOperationUser(header.user) + passwordField.setOnEditorActionListener( + OnImeSubmitListener { text -> + launch { viewModel.checkCredential(text, header) } + } + ) + passwordField.setOnKeyListener( + OnBackButtonListener { host.onCredentialAborted() } + ) + } + } + + launch { + viewModel.inputFlags.collect { flags -> + flags?.let { passwordField.inputType = it } + } + } + + // dismiss on a valid credential check + launch { + viewModel.validatedAttestation.collect { attestation -> + if (attestation != null) { + imeManager.hideSoftInputFromWindow(view.windowToken, 0 /* flags */) + host.onCredentialMatched(attestation) + } else { + passwordField.setText("") + } + } + } + } + } + } +} + +private class OnBackButtonListener(private val onBack: () -> Unit) : View.OnKeyListener { + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode != KeyEvent.KEYCODE_BACK) { + return false + } + if (event.action == KeyEvent.ACTION_UP) { + onBack() + } + return true + } +} + +private class OnImeSubmitListener(private val onSubmit: (text: CharSequence) -> Unit) : + TextView.OnEditorActionListener { + override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean { + val isSoftImeEvent = + event == null && + (actionId == EditorInfo.IME_NULL || + actionId == EditorInfo.IME_ACTION_DONE || + actionId == EditorInfo.IME_ACTION_NEXT) + val isKeyboardEnterKey = + event != null && + KeyEvent.isConfirmKey(event.keyCode) && + event.action == KeyEvent.ACTION_DOWN + if (isSoftImeEvent || isKeyboardEnterKey) { + onSubmit(v.text) + return true + } + return false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt new file mode 100644 index 000000000000..4765551df3f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPatternViewBinder.kt @@ -0,0 +1,75 @@ +package com.android.systemui.biometrics.ui.binder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockPatternView +import com.android.systemui.R +import com.android.systemui.biometrics.ui.CredentialPatternView +import com.android.systemui.biometrics.ui.CredentialView +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +/** Sub-binder for the [CredentialPatternView]. */ +object CredentialPatternViewBinder { + + /** Bind the view. */ + fun bind( + view: CredentialPatternView, + host: CredentialView.Host, + viewModel: CredentialViewModel, + ) { + val lockPatternView: LockPatternView = view.requireViewById(R.id.lockPattern) + + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // observe credential validation attempts and submit/cancel buttons + launch { + viewModel.header.collect { header -> + lockPatternView.setOnPatternListener( + OnPatternDetectedListener { pattern -> + if (pattern.isPatternLongEnough()) { + // Pattern size is less than the minimum + // do not count it as a failed attempt + viewModel.showPatternTooShortError() + } else { + lockPatternView.isEnabled = false + launch { viewModel.checkCredential(pattern, header) } + } + } + ) + } + } + + launch { viewModel.stealthMode.collect { lockPatternView.isInStealthMode = it } } + + // dismiss on a valid credential check + launch { + viewModel.validatedAttestation.collect { attestation -> + val matched = attestation != null + lockPatternView.isEnabled = !matched + if (matched) { + host.onCredentialMatched(attestation!!) + } + } + } + } + } + } +} + +private class OnPatternDetectedListener( + private val onDetected: (pattern: List<LockPatternView.Cell>) -> Unit +) : LockPatternView.OnPatternListener { + override fun onPatternCellAdded(pattern: List<LockPatternView.Cell>) {} + override fun onPatternCleared() {} + override fun onPatternStart() {} + override fun onPatternDetected(pattern: List<LockPatternView.Cell>) { + onDetected(pattern) + } +} + +private fun List<LockPatternView.Cell>.isPatternLongEnough(): Boolean = + size < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt new file mode 100644 index 000000000000..fcc948756972 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -0,0 +1,140 @@ +package com.android.systemui.biometrics.ui.binder + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.R +import com.android.systemui.animation.Interpolators +import com.android.systemui.biometrics.AuthDialog +import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.CredentialPasswordView +import com.android.systemui.biometrics.ui.CredentialPatternView +import com.android.systemui.biometrics.ui.CredentialView +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +/** + * View binder for all credential variants of BiometricPrompt, including [CredentialPatternView] and + * [CredentialPasswordView]. + * + * This binder delegates to sub-binders for each variant, such as the [CredentialPasswordViewBinder] + * and [CredentialPatternViewBinder]. + */ +object CredentialViewBinder { + + /** Binds a [CredentialPasswordView] or [CredentialPatternView] to a [CredentialViewModel]. */ + @JvmStatic + fun bind( + view: ViewGroup, + host: CredentialView.Host, + viewModel: CredentialViewModel, + panelViewController: AuthPanelController, + animatePanel: Boolean, + maxErrorDuration: Long = 3_000L, + ) { + val titleView: TextView = view.requireViewById(R.id.title) + val subtitleView: TextView = view.requireViewById(R.id.subtitle) + val descriptionView: TextView = view.requireViewById(R.id.description) + val iconView: ImageView? = view.findViewById(R.id.icon) + val errorView: TextView = view.requireViewById(R.id.error) + + var errorTimer: Job? = null + + // bind common elements + view.repeatWhenAttached { + if (animatePanel) { + with(panelViewController) { + // Credential view is always full screen. + setUseFullScreen(true) + updateForContentDimensions( + containerWidth, + containerHeight, + 0 /* animateDurationMs */ + ) + } + } + + repeatOnLifecycle(Lifecycle.State.STARTED) { + // show prompt metadata + launch { + viewModel.header.collect { header -> + titleView.text = header.title + view.announceForAccessibility(header.title) + + subtitleView.textOrHide = header.subtitle + descriptionView.textOrHide = header.description + + iconView?.setImageDrawable(header.icon) + + // Only animate this if we're transitioning from a biometric view. + if (viewModel.animateContents.value) { + view.animateCredentialViewIn() + } + } + } + + // show transient error messages + launch { + viewModel.errorMessage + .onEach { msg -> + errorTimer?.cancel() + if (msg.isNotBlank()) { + errorTimer = launch { + delay(maxErrorDuration) + viewModel.resetErrorMessage() + } + } + } + .collect { errorView.textOrHide = it } + } + + // show an extra dialog if the remaining attempts becomes low + launch { + viewModel.remainingAttempts + .filter { it.remaining != null } + .collect { info -> + host.onCredentialAttemptsRemaining(info.remaining!!, info.message) + } + } + } + } + + // bind the auth widget + when (view) { + is CredentialPasswordView -> CredentialPasswordViewBinder.bind(view, host, viewModel) + is CredentialPatternView -> CredentialPatternViewBinder.bind(view, host, viewModel) + else -> throw IllegalStateException("unexpected view type: ${view.javaClass.name}") + } + } +} + +private fun View.animateCredentialViewIn() { + translationY = resources.getDimension(R.dimen.biometric_dialog_credential_translation_offset) + alpha = 0f + postOnAnimation { + animate() + .translationY(0f) + .setDuration(AuthDialog.ANIMATE_CREDENTIAL_INITIAL_DURATION_MS.toLong()) + .alpha(1f) + .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) + .withLayer() + .start() + } +} + +private var TextView.textOrHide: String? + set(value) { + val gone = value.isNullOrBlank() + visibility = if (gone) View.GONE else View.VISIBLE + text = if (gone) "" else value + } + get() = text?.toString() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt new file mode 100644 index 000000000000..84bbceb38fa7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -0,0 +1,178 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.text.InputType +import com.android.internal.widget.LockPatternView +import com.android.systemui.R +import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.CredentialStatus +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.dagger.qualifiers.Application +import javax.inject.Inject +import kotlin.reflect.KClass +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map + +/** View-model for all CredentialViews within BiometricPrompt. */ +class CredentialViewModel +@Inject +constructor( + @Application private val applicationContext: Context, + private val credentialInteractor: BiometricPromptCredentialInteractor, +) { + + /** Top level information about the prompt. */ + val header: Flow<HeaderViewModel> = + credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>().map { + request -> + BiometricPromptHeaderViewModelImpl( + request, + user = UserHandle.of(request.userInfo.userId), + title = request.title, + subtitle = request.subtitle, + description = request.description, + icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId), + ) + } + + /** Input flags for text based credential views */ + val inputFlags: Flow<Int?> = + credentialInteractor.prompt.map { + when (it) { + is BiometricPromptRequest.Credential.Pin -> + InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + else -> null + } + } + + /** If stealth mode is active (hide user credential input). */ + val stealthMode: Flow<Boolean> = + credentialInteractor.prompt.map { + when (it) { + is BiometricPromptRequest.Credential.Pattern -> it.stealthMode + else -> false + } + } + + private val _animateContents: MutableStateFlow<Boolean> = MutableStateFlow(true) + /** If this view should be animated on transitions. */ + val animateContents = _animateContents.asStateFlow() + + /** Error messages to show the user. */ + val errorMessage: Flow<String> = + combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p -> + when (error) { + is CredentialStatus.Fail.Error -> error.error + ?: applicationContext.asBadCredentialErrorMessage(p) + is CredentialStatus.Fail.Throttled -> error.error + null -> "" + } + } + + private val _validatedAttestation: MutableSharedFlow<ByteArray?> = MutableSharedFlow() + /** Results of [checkPatternCredential]. A non-null attestation is supplied on success. */ + val validatedAttestation: Flow<ByteArray?> = _validatedAttestation.asSharedFlow() + + private val _remainingAttempts: MutableStateFlow<RemainingAttempts> = + MutableStateFlow(RemainingAttempts()) + /** If set, the number of remaining attempts before the user must stop. */ + val remainingAttempts: Flow<RemainingAttempts> = _remainingAttempts.asStateFlow() + + /** Enable transition animations. */ + fun setAnimateContents(animate: Boolean) { + _animateContents.value = animate + } + + /** Show an error message to inform the user the pattern is too short to attempt validation. */ + fun showPatternTooShortError() { + credentialInteractor.setVerificationError( + CredentialStatus.Fail.Error( + applicationContext.asBadCredentialErrorMessage( + BiometricPromptRequest.Credential.Pattern::class + ) + ) + ) + } + + /** Reset the error message to an empty string. */ + fun resetErrorMessage() { + credentialInteractor.resetVerificationError() + } + + /** Check a PIN or password and update [validatedAttestation] or [remainingAttempts]. */ + suspend fun checkCredential(text: CharSequence, header: HeaderViewModel) = + checkCredential(credentialInteractor.checkCredential(header.asRequest(), text = text)) + + /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */ + suspend fun checkCredential(pattern: List<LockPatternView.Cell>, header: HeaderViewModel) = + checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern)) + + private suspend fun checkCredential(result: CredentialStatus) { + when (result) { + is CredentialStatus.Success.Verified -> { + _validatedAttestation.emit(result.hat) + _remainingAttempts.value = RemainingAttempts() + } + is CredentialStatus.Fail.Error -> { + _validatedAttestation.emit(null) + _remainingAttempts.value = + RemainingAttempts(result.remainingAttempts, result.urgentMessage ?: "") + } + is CredentialStatus.Fail.Throttled -> { + // required for completeness, but a throttled error cannot be the final result + _validatedAttestation.emit(null) + _remainingAttempts.value = RemainingAttempts() + } + } + } +} + +private fun Context.asBadCredentialErrorMessage(prompt: BiometricPromptRequest?): String = + asBadCredentialErrorMessage( + if (prompt != null) prompt::class else BiometricPromptRequest.Credential.Password::class + ) + +private fun <T : BiometricPromptRequest> Context.asBadCredentialErrorMessage( + clazz: KClass<T> +): String = + getString( + when (clazz) { + BiometricPromptRequest.Credential.Pin::class -> R.string.biometric_dialog_wrong_pin + BiometricPromptRequest.Credential.Password::class -> + R.string.biometric_dialog_wrong_password + BiometricPromptRequest.Credential.Pattern::class -> + R.string.biometric_dialog_wrong_pattern + else -> R.string.biometric_dialog_wrong_password + } + ) + +private fun Context.asLockIcon(userId: Int): Drawable { + val id = + if (Utils.isManagedProfile(this, userId)) { + R.drawable.auth_dialog_enterprise + } else { + R.drawable.auth_dialog_lock + } + return resources.getDrawable(id, theme) +} + +private class BiometricPromptHeaderViewModelImpl( + val request: BiometricPromptRequest.Credential, + override val user: UserHandle, + override val title: String, + override val subtitle: String, + override val description: String, + override val icon: Drawable, +) : HeaderViewModel + +private fun HeaderViewModel.asRequest(): BiometricPromptRequest.Credential = + (this as BiometricPromptHeaderViewModelImpl).request diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt new file mode 100644 index 000000000000..ba23f1cfa22d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/HeaderViewModel.kt @@ -0,0 +1,13 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import android.graphics.drawable.Drawable +import android.os.UserHandle + +/** View model for the top-level header / info area of BiometricPrompt. */ +interface HeaderViewModel { + val user: UserHandle + val title: String + val subtitle: String + val description: String + val icon: Drawable +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt new file mode 100644 index 000000000000..0f221734cb44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/RemainingAttempts.kt @@ -0,0 +1,4 @@ +package com.android.systemui.biometrics.ui.viewmodel + +/** Metadata about the number of credential attempts the user has left [remaining], if known. */ +data class RemainingAttempts(val remaining: Int? = null, val message: String = "") diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 7e31626983e7..e47e636fa445 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -48,6 +48,7 @@ import com.android.systemui.log.dagger.LogModule; import com.android.systemui.mediaprojection.appselector.MediaProjectionModule; import com.android.systemui.model.SysUiState; import com.android.systemui.navigationbar.NavigationBarComponent; +import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; @@ -152,6 +153,7 @@ import dagger.Provides; TunerModule.class, UserModule.class, UtilModule.class, + NoteTaskModule.class, WalletModule.class }, subcomponents = { diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java index 29bb2f42cca5..41f557850f88 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java @@ -164,7 +164,8 @@ public interface Complication { COMPLICATION_TYPE_AIR_QUALITY, COMPLICATION_TYPE_CAST_INFO, COMPLICATION_TYPE_HOME_CONTROLS, - COMPLICATION_TYPE_SMARTSPACE + COMPLICATION_TYPE_SMARTSPACE, + COMPLICATION_TYPE_MEDIA_ENTRY }) @Retention(RetentionPolicy.SOURCE) @interface ComplicationType {} @@ -177,6 +178,7 @@ public interface Complication { int COMPLICATION_TYPE_CAST_INFO = 1 << 4; int COMPLICATION_TYPE_HOME_CONTROLS = 1 << 5; int COMPLICATION_TYPE_SMARTSPACE = 1 << 6; + int COMPLICATION_TYPE_MEDIA_ENTRY = 1 << 7; /** * The {@link Host} interface specifies a way a {@link Complication} to communicate with its diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java index 75a97de10e7e..18aacd21bd12 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java @@ -20,6 +20,7 @@ import static com.android.systemui.dreams.complication.Complication.COMPLICATION import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS; +import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_NONE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME; @@ -54,6 +55,8 @@ public class ComplicationUtils { return COMPLICATION_TYPE_HOME_CONTROLS; case DreamBackend.COMPLICATION_TYPE_SMARTSPACE: return COMPLICATION_TYPE_SMARTSPACE; + case DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY: + return COMPLICATION_TYPE_MEDIA_ENTRY; default: return COMPLICATION_TYPE_NONE; } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java index 1166c2fc1120..deff0608bedd 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java @@ -55,6 +55,11 @@ public class DreamMediaEntryComplication implements Complication { return mComponentFactory.create().getViewHolder(); } + @Override + public int getRequiredTypeAvailability() { + return COMPLICATION_TYPE_MEDIA_ENTRY; + } + /** * Contains values/logic associated with the dream complication view. */ diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 8f430dea37af..4818bccb1f64 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -118,6 +118,12 @@ object Flags { */ @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212) + /** + * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository + * will occur in stages. This is one stage of many to come. + */ + @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true) + // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = ReleasedFlag(300) @@ -325,6 +331,9 @@ object Flags { // 1800 - shade container @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true) + // 1900 - note task + @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks") + // Pay no attention to the reflection behind the curtain. // ========================== Curtain ========================== // | | diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 1c6cec22ffca..021431399ab6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -518,7 +518,7 @@ public class KeyguardService extends Service { @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) { Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp"); checkPermission(); - mKeyguardViewMediator.onStartedWakingUp(cameraGestureTriggered); + mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered); mKeyguardLifecyclesDispatcher.dispatch( KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason); Trace.endSection(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 480fd93fc761..41abb62f05cc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -342,12 +342,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, */ private int mDelayedProfileShowingSequence; - /** - * If the user has disabled the keyguard, then requests to exit, this is - * how we'll ultimately let them know whether it was successful. We use this - * var being non-null as an indicator that there is an in progress request. - */ - private IKeyguardExitCallback mExitSecureCallback; private final DismissCallbackRegistry mDismissCallbackRegistry; // the properties of the keyguard @@ -1342,18 +1336,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, || !mLockPatternUtils.isSecure(currentUser); long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser()); mLockLater = false; - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled"); - try { - mExitSecureCallback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } - mExitSecureCallback = null; - if (!mExternallyEnabled) { - hideLocked(); - } - } else if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) { + if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) { // If we are going to sleep but the keyguard is showing (and will continue to be // showing, not in the process of going away) then reset its state. Otherwise, let // this fall through and explicitly re-lock the keyguard. @@ -1595,7 +1578,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, /** * It will let us know when the device is waking up. */ - public void onStartedWakingUp(boolean cameraGestureTriggered) { + public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason, + boolean cameraGestureTriggered) { Trace.beginSection("KeyguardViewMediator#onStartedWakingUp"); // TODO: Rename all screen off/on references to interactive/sleeping @@ -1610,7 +1594,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence); notifyStartedWakingUp(); } - mUpdateMonitor.dispatchStartedWakingUp(); + mUpdateMonitor.dispatchStartedWakingUp(pmWakeReason); maybeSendUserPresentBroadcast(); Trace.endSection(); } @@ -1672,13 +1656,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mExternallyEnabled = enabled; if (!enabled && mShowing) { - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring"); - // we're in the process of handling a request to verify the user - // can get past the keyguard. ignore extraneous requests to disable / re-enable - return; - } - // hiding keyguard that is showing, remember to reshow later if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, " + "disabling status bar expansion"); @@ -1692,33 +1669,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mNeedToReshowWhenReenabled = false; updateInputRestrictedLocked(); - if (mExitSecureCallback != null) { - if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting"); + showLocked(null); + + // block until we know the keyguard is done drawing (and post a message + // to unblock us after a timeout, so we don't risk blocking too long + // and causing an ANR). + mWaitingUntilKeyguardVisible = true; + mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, + KEYGUARD_DONE_DRAWING_TIMEOUT_MS); + if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); + while (mWaitingUntilKeyguardVisible) { try { - mExitSecureCallback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); + wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } - mExitSecureCallback = null; - resetStateLocked(); - } else { - showLocked(null); - - // block until we know the keyguard is done drawing (and post a message - // to unblock us after a timeout, so we don't risk blocking too long - // and causing an ANR). - mWaitingUntilKeyguardVisible = true; - mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS); - if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false"); - while (mWaitingUntilKeyguardVisible) { - try { - wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); } + if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible"); } } } @@ -1748,13 +1715,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } catch (RemoteException e) { Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); } - } else if (mExitSecureCallback != null) { - // already in progress with someone else - try { - callback.onKeyguardExitResult(false); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e); - } } else if (!isSecure()) { // Keyguard is not secure, no need to do anything, and we don't need to reshow @@ -2289,21 +2249,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, return; } setPendingLock(false); // user may have authenticated during the screen off animation - if (mExitSecureCallback != null) { - try { - mExitSecureCallback.onKeyguardExitResult(true /* authenciated */); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to call onKeyguardExitResult()", e); - } - - mExitSecureCallback = null; - - // after successfully exiting securely, no need to reshow - // the keyguard when they've released the lock - mExternallyEnabled = true; - mNeedToReshowWhenReenabled = false; - updateInputRestricted(); - } handleHide(); mUpdateMonitor.clearBiometricRecognizedWhenKeyguardDone(currentUser); @@ -3111,7 +3056,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, pw.print(" mInputRestricted: "); pw.println(mInputRestricted); pw.print(" mOccluded: "); pw.println(mOccluded); pw.print(" mDelayedShowingSequence: "); pw.println(mDelayedShowingSequence); - pw.print(" mExitSecureCallback: "); pw.println(mExitSecureCallback); pw.print(" mDeviceInteractive: "); pw.println(mDeviceInteractive); pw.print(" mGoingToSleep: "); pw.println(mGoingToSleep); pw.print(" mHiding: "); pw.println(mHiding); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt new file mode 100644 index 000000000000..a069582f6692 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/BuiltInKeyguardQuickAffordanceKeys.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.data.quickaffordance + +/** + * Unique identifier keys for all known built-in quick affordances. + * + * Please ensure uniqueness by never associating more than one class with each key. + */ +object BuiltInKeyguardQuickAffordanceKeys { + // Please keep alphabetical order of const names to simplify future maintenance. + const val HOME_CONTROLS = "home" + const val QR_CODE_SCANNER = "qr_code_scanner" + const val QUICK_ACCESS_WALLET = "wallet" + // Please keep alphabetical order of const names to simplify future maintenance. +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index 83842602cbee..d3bb34cd29d4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.content.Intent @@ -51,6 +51,8 @@ constructor( private val appContext = context.applicationContext + override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + override val state: Flow<KeyguardQuickAffordanceConfig.State> = component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked -> if (canShowWhileLocked) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 95027d00c46c..0dd0ad797411 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.content.Intent import com.android.systemui.animation.Expandable @@ -26,8 +26,18 @@ import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ interface KeyguardQuickAffordanceConfig { + /** Unique identifier for this quick affordance. It must be globally unique. */ + val key: String + + /** The observable [State] of the affordance. */ val state: Flow<State> + /** + * Notifies that the affordance was clicked by the user. + * + * @param expandable An [Expandable] to use when animating dialogs or activities + * @return An [OnClickedResult] telling the caller what to do next + */ fun onQuickAffordanceClicked(expandable: Expandable?): OnClickedResult /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt index 502a6070a422..9a441392aa07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import com.android.systemui.R import com.android.systemui.animation.Expandable @@ -37,6 +37,8 @@ constructor( private val controller: QRCodeScannerController, ) : KeyguardQuickAffordanceConfig { + override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow { val callback = object : QRCodeScannerController.Callback { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index a24a0d62465f..8a1267ebadd1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.graphics.drawable.Drawable import android.service.quickaccesswallet.GetWalletCardsError @@ -44,6 +44,8 @@ constructor( private val activityStarter: ActivityStarter, ) : KeyguardQuickAffordanceConfig { + override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow { val callback = object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index e8532ecfdc37..ab25597b077c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -20,6 +20,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.animation.ValueAnimator.AnimatorUpdateListener import android.annotation.FloatRange +import android.os.Trace import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.KeyguardState @@ -157,12 +158,36 @@ class KeyguardTransitionRepository @Inject constructor() { value: Float, transitionState: TransitionState ) { + trace(info, transitionState) + if (transitionState == TransitionState.FINISHED) { currentTransitionInfo = null } _transitions.value = TransitionStep(info.from, info.to, value, transitionState) } + private fun trace(info: TransitionInfo, transitionState: TransitionState) { + if ( + transitionState != TransitionState.STARTED && + transitionState != TransitionState.FINISHED + ) { + return + } + val traceName = + "Transition: ${info.from} -> ${info.to} " + + if (info.animator == null) { + "(manual)" + } else { + "" + } + val traceCookie = traceName.hashCode() + if (transitionState == TransitionState.STARTED) { + Trace.beginAsyncSection(traceName, traceCookie) + } else if (transitionState == TransitionState.FINISHED) { + Trace.endAsyncSection(traceName, traceCookie) + } + } + companion object { private const val TAG = "KeyguardTransitionRepository" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index f663b0dd23cd..914b9fc52c1a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -21,15 +21,14 @@ import android.content.Intent import com.android.internal.widget.LockPatternUtils import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject -import kotlin.reflect.KClass import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onStart @@ -70,10 +69,10 @@ constructor( * @param expandable An optional [Expandable] for the activity- or dialog-launch animation */ fun onQuickAffordanceClicked( - configKey: KClass<out KeyguardQuickAffordanceConfig>, + configKey: String, expandable: Expandable?, ) { - @Suppress("UNCHECKED_CAST") val config = registry.get(configKey as KClass<Nothing>) + @Suppress("UNCHECKED_CAST") val config = registry.get(configKey) when (val result = config.onQuickAffordanceClicked(expandable)) { is KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity -> launchQuickAffordance( @@ -102,7 +101,7 @@ constructor( if (index != -1) { val visibleState = states[index] as KeyguardQuickAffordanceConfig.State.Visible KeyguardQuickAffordanceModel.Visible( - configKey = configs[index]::class, + configKey = configs[index].key, icon = visibleState.icon, toggle = visibleState.toggle, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 59bb22786917..7409aec57b4c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -24,6 +24,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** Encapsulates business-logic related to the keyguard transitions. */ @SysUISingleton @@ -34,4 +36,17 @@ constructor( ) { /** AOD->LOCKSCREEN transition information. */ val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) + + /** LOCKSCREEN->AOD transition information. */ + val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) + + /** + * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> + * Lockscreen (0f). + */ + val dozeAmountTransition: Flow<TransitionStep> = + merge( + aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) }, + lockscreenToAodTransition, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt index e56b25967910..fc644a9e6067 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordanceModel.kt @@ -18,9 +18,7 @@ package com.android.systemui.keyguard.domain.model import com.android.systemui.common.shared.model.Icon -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState -import kotlin.reflect.KClass /** * Models a "quick affordance" in the keyguard bottom area (for example, a button on the @@ -33,7 +31,7 @@ sealed class KeyguardQuickAffordanceModel { /** A affordance is visible. */ data class Visible( /** Identifier for the affordance this is modeling. */ - val configKey: KClass<out KeyguardQuickAffordanceConfig>, + val configKey: String, /** An icon for the affordance. */ val icon: Icon, /** The toggle state for the affordance. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt index 94024d4a0ace..b48acb65849e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.quickaffordance +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt index ad40ee7a0183..8526ada69569 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/quickaffordance/KeyguardQuickAffordanceRegistry.kt @@ -17,14 +17,17 @@ package com.android.systemui.keyguard.domain.quickaffordance -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import javax.inject.Inject -import kotlin.reflect.KClass /** Central registry of all known quick affordance configs. */ interface KeyguardQuickAffordanceRegistry<T : KeyguardQuickAffordanceConfig> { fun getAll(position: KeyguardQuickAffordancePosition): List<T> - fun get(configClass: KClass<out T>): T + fun get(key: String): T } class KeyguardQuickAffordanceRegistryImpl @@ -46,8 +49,8 @@ constructor( qrCodeScanner, ), ) - private val configByClass = - configsByPosition.values.flatten().associateBy { config -> config::class } + private val configByKey = + configsByPosition.values.flatten().associateBy { config -> config.key } override fun getAll( position: KeyguardQuickAffordancePosition, @@ -56,8 +59,8 @@ constructor( } override fun get( - configClass: KClass<out KeyguardQuickAffordanceConfig> + key: String, ): KeyguardQuickAffordanceConfig { - return configByClass.getValue(configClass) + return configByKey.getValue(key) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt index 581dafa33df7..a18b036c5189 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/model/KeyguardQuickAffordancePosition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.keyguard.domain.model +package com.android.systemui.keyguard.shared.quickaffordance /** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */ enum class KeyguardQuickAffordancePosition { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt index 535ca7210244..6aac9124bab9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt @@ -22,7 +22,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInterac import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import javax.inject.Inject import kotlinx.coroutines.flow.Flow diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt index bf598ba85932..44f48f97b62e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt @@ -18,12 +18,10 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig -import kotlin.reflect.KClass /** Models the UI state of a keyguard quick affordance button. */ data class KeyguardQuickAffordanceViewModel( - val configKey: KClass<out KeyguardQuickAffordanceConfig>? = null, + val configKey: String? = null, val isVisible: Boolean = false, /** Whether to animate the transition of the quick affordance from invisible to visible. */ val animateReveal: Boolean = false, @@ -33,7 +31,7 @@ data class KeyguardQuickAffordanceViewModel( val isActivated: Boolean = false, ) { data class OnClickedParameters( - val configKey: KClass<out KeyguardQuickAffordanceConfig>, + val configKey: String, val expandable: Expandable?, ) } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt new file mode 100644 index 000000000000..d247f249e2fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.app.KeyguardManager +import android.content.Context +import android.os.UserManager +import android.view.KeyEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.kotlin.getOrNull +import com.android.wm.shell.floating.FloatingTasks +import java.util.Optional +import javax.inject.Inject + +/** + * Entry point for creating and managing note. + * + * The controller decides how a note is launched based in the device state: locked or unlocked. + * + * Currently, we only support a single task per time. + */ +@SysUISingleton +internal class NoteTaskController +@Inject +constructor( + private val context: Context, + private val intentResolver: NoteTaskIntentResolver, + private val optionalFloatingTasks: Optional<FloatingTasks>, + private val optionalKeyguardManager: Optional<KeyguardManager>, + private val optionalUserManager: Optional<UserManager>, + @NoteTaskEnabledKey private val isEnabled: Boolean, +) { + + fun handleSystemKey(keyCode: Int) { + if (!isEnabled) return + + if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) { + showNoteTask() + } + } + + private fun showNoteTask() { + val floatingTasks = optionalFloatingTasks.getOrNull() ?: return + val keyguardManager = optionalKeyguardManager.getOrNull() ?: return + val userManager = optionalUserManager.getOrNull() ?: return + val intent = intentResolver.resolveIntent() ?: return + + // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing. + if (!userManager.isUserUnlocked) return + + if (keyguardManager.isKeyguardLocked) { + context.startActivity(intent) + } else { + // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. + floatingTasks.showOrSetStashed(intent) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt new file mode 100644 index 000000000000..e0bf1da2f652 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import javax.inject.Qualifier + +/** Key associated with a [Boolean] flag that enables or disables the note task feature. */ +@Qualifier internal annotation class NoteTaskEnabledKey diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt new file mode 100644 index 000000000000..d84717da3a21 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import com.android.systemui.statusbar.CommandQueue +import com.android.wm.shell.floating.FloatingTasks +import dagger.Lazy +import java.util.Optional +import javax.inject.Inject + +/** Class responsible to "glue" all note task dependencies. */ +internal class NoteTaskInitializer +@Inject +constructor( + private val optionalFloatingTasks: Optional<FloatingTasks>, + private val lazyNoteTaskController: Lazy<NoteTaskController>, + private val commandQueue: CommandQueue, + @NoteTaskEnabledKey private val isEnabled: Boolean, +) { + + private val callbacks = + object : CommandQueue.Callbacks { + override fun handleSystemKey(keyCode: Int) { + lazyNoteTaskController.get().handleSystemKey(keyCode) + } + } + + fun initialize() { + if (isEnabled && optionalFloatingTasks.isPresent) { + commandQueue.addCallback(callbacks) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt new file mode 100644 index 000000000000..98d69910aac3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import javax.inject.Inject + +/** + * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an + * [Intent] ready for be launched will be returned. Otherwise, returns null. + * + * TODO(b/248274123): should be revisited once the notes role is implemented. + */ +internal class NoteTaskIntentResolver +@Inject +constructor( + private val packageManager: PackageManager, +) { + + fun resolveIntent(): Intent? { + val intent = Intent(NOTES_ACTION) + val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + val infoList = packageManager.queryIntentActivities(intent, flags) + + for (info in infoList) { + val packageName = info.serviceInfo.applicationInfo.packageName ?: continue + val activityName = resolveActivityNameForNotesAction(packageName) ?: continue + + return Intent(NOTES_ACTION) + .setComponent(ComponentName(packageName, activityName)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + return null + } + + private fun resolveActivityNameForNotesAction(packageName: String): String? { + val intent = Intent(NOTES_ACTION).setPackage(packageName) + val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()) + val resolveInfo = packageManager.resolveActivity(intent, flags) + + val activityInfo = resolveInfo?.activityInfo ?: return null + if (activityInfo.name.isNullOrBlank()) return null + if (!activityInfo.exported) return null + if (!activityInfo.enabled) return null + if (!activityInfo.showWhenLocked) return null + if (!activityInfo.turnScreenOn) return null + + return activityInfo.name + } + + companion object { + // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead. + const val NOTES_ACTION = "android.intent.action.NOTES" + } +} + +private val ActivityInfo.showWhenLocked: Boolean + get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0 + +private val ActivityInfo.turnScreenOn: Boolean + get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0 diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt new file mode 100644 index 000000000000..035396a6fc76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.app.KeyguardManager +import android.content.Context +import android.os.UserManager +import androidx.core.content.getSystemService +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import dagger.Module +import dagger.Provides +import java.util.* + +/** Compose all dependencies required by Note Task feature. */ +@Module +internal class NoteTaskModule { + + @[Provides NoteTaskEnabledKey] + fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean { + return featureFlags.isEnabled(Flags.NOTE_TASKS) + } + + @Provides + fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> { + return Optional.ofNullable(context.getSystemService()) + } + + @Provides + fun provideOptionalUserManager(context: Context): Optional<UserManager> { + return Optional.ofNullable(context.getSystemService()) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 976710351a44..c189acec2930 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -649,37 +649,6 @@ public class NotificationIconContainer extends ViewGroup { return mNumDots > 0; } - /** - * If the overflow is in the range [1, max_dots - 1) (basically 1 or 2 dots), then - * extra padding will have to be accounted for - * - * This method has no meaning for non-static containers - */ - public boolean hasPartialOverflow() { - return mNumDots > 0 && mNumDots < MAX_DOTS; - } - - /** - * Get padding that can account for extra dots up to the max. The only valid values for - * this method are for 1 or 2 dots. - * @return only extraDotPadding or extraDotPadding * 2 - */ - public int getPartialOverflowExtraPadding() { - if (!hasPartialOverflow()) { - return 0; - } - - int partialOverflowAmount = (MAX_DOTS - mNumDots) * (mStaticDotDiameter + mDotPadding); - - int adjustedWidth = getFinalTranslationX() + partialOverflowAmount; - // In case we actually give too much padding... - if (adjustedWidth > getWidth()) { - partialOverflowAmount = getWidth() - getFinalTranslationX(); - } - - return partialOverflowAmount; - } - // Give some extra room for btw notifications if we can public int getNoOverflowExtraPadding() { if (mNumDots != 0) { diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt new file mode 100644 index 000000000000..9653985cb6e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.util.kotlin + +import android.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import java.util.function.Consumer +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect + +/** + * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to + * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners + * during onViewAttached() and removing during onViewRemoved() + */ +@JvmOverloads +fun <T> collectFlow( + view: View, + flow: Flow<T>, + consumer: Consumer<T>, + state: Lifecycle.State = Lifecycle.State.CREATED, +) { + view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } +} diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java index 42d7d52a71ab..44f6d03207b1 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java @@ -47,7 +47,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor; +import com.android.systemui.wallpapers.canvas.WallpaperLocalColorExtractor; import com.android.systemui.wallpapers.gl.EglHelper; import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer; @@ -521,7 +521,7 @@ public class ImageWallpaper extends WallpaperService { class CanvasEngine extends WallpaperService.Engine implements DisplayListener { private WallpaperManager mWallpaperManager; - private final WallpaperColorExtractor mWallpaperColorExtractor; + private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor; private SurfaceHolder mSurfaceHolder; @VisibleForTesting static final int MIN_SURFACE_WIDTH = 128; @@ -543,9 +543,9 @@ public class ImageWallpaper extends WallpaperService { super(); setFixedSizeAllowed(true); setShowForAllUsers(true); - mWallpaperColorExtractor = new WallpaperColorExtractor( + mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor( mBackgroundExecutor, - new WallpaperColorExtractor.WallpaperColorExtractorCallback() { + new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override public void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { @@ -570,7 +570,7 @@ public class ImageWallpaper extends WallpaperService { // if the number of pages is already computed, transmit it to the color extractor if (mPagesComputed) { - mWallpaperColorExtractor.onPageChanged(mPages); + mWallpaperLocalColorExtractor.onPageChanged(mPages); } } @@ -597,7 +597,7 @@ public class ImageWallpaper extends WallpaperService { public void onDestroy() { getDisplayContext().getSystemService(DisplayManager.class) .unregisterDisplayListener(this); - mWallpaperColorExtractor.cleanUp(); + mWallpaperLocalColorExtractor.cleanUp(); unloadBitmap(); } @@ -813,7 +813,7 @@ public class ImageWallpaper extends WallpaperService { @VisibleForTesting void recomputeColorExtractorMiniBitmap() { - mWallpaperColorExtractor.onBitmapChanged(mBitmap); + mWallpaperLocalColorExtractor.onBitmapChanged(mBitmap); } @VisibleForTesting @@ -830,14 +830,14 @@ public class ImageWallpaper extends WallpaperService { public void addLocalColorsAreas(@NonNull List<RectF> regions) { // this call will activate the offset notifications // if no colors were being processed before - mWallpaperColorExtractor.addLocalColorsAreas(regions); + mWallpaperLocalColorExtractor.addLocalColorsAreas(regions); } @Override public void removeLocalColorsAreas(@NonNull List<RectF> regions) { // this call will deactivate the offset notifications // if we are no longer processing colors - mWallpaperColorExtractor.removeLocalColorAreas(regions); + mWallpaperLocalColorExtractor.removeLocalColorAreas(regions); } @Override @@ -853,7 +853,7 @@ public class ImageWallpaper extends WallpaperService { if (pages != mPages || !mPagesComputed) { mPages = pages; mPagesComputed = true; - mWallpaperColorExtractor.onPageChanged(mPages); + mWallpaperLocalColorExtractor.onPageChanged(mPages); } } @@ -881,7 +881,7 @@ public class ImageWallpaper extends WallpaperService { .getSystemService(WindowManager.class) .getCurrentWindowMetrics() .getBounds(); - mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height()); + mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height()); } @@ -902,7 +902,7 @@ public class ImageWallpaper extends WallpaperService { : mBitmap.isRecycled() ? "recycled" : mBitmap.getWidth() + "x" + mBitmap.getHeight()); - mWallpaperColorExtractor.dump(prefix, fd, out, args); + mWallpaperLocalColorExtractor.dump(prefix, fd, out, args); } } } diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java index e2e4555bb965..6cac5c952b7c 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java @@ -45,14 +45,14 @@ import java.util.concurrent.Executor; * It uses a background executor, and uses callbacks to inform that the work is done. * It uses a downscaled version of the wallpaper to extract the colors. */ -public class WallpaperColorExtractor { +public class WallpaperLocalColorExtractor { private Bitmap mMiniBitmap; @VisibleForTesting static final int SMALL_SIDE = 128; - private static final String TAG = WallpaperColorExtractor.class.getSimpleName(); + private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName(); private static final @NonNull RectF LOCAL_COLOR_BOUNDS = new RectF(0, 0, 1, 1); @@ -70,12 +70,12 @@ public class WallpaperColorExtractor { @Background private final Executor mBackgroundExecutor; - private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback; + private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback; /** * Interface to handle the callbacks after the different steps of the color extraction */ - public interface WallpaperColorExtractorCallback { + public interface WallpaperLocalColorExtractorCallback { /** * Callback after the colors of new regions have been extracted * @param regions the list of new regions that have been processed @@ -103,13 +103,13 @@ public class WallpaperColorExtractor { /** * Creates a new color extractor. * @param backgroundExecutor the executor on which the color extraction will be performed - * @param wallpaperColorExtractorCallback an interface to handle the callbacks from + * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from * the color extractor. */ - public WallpaperColorExtractor(@Background Executor backgroundExecutor, - WallpaperColorExtractorCallback wallpaperColorExtractorCallback) { + public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor, + WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) { mBackgroundExecutor = backgroundExecutor; - mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback; + mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback; } /** @@ -157,7 +157,7 @@ public class WallpaperColorExtractor { mBitmapWidth = bitmap.getWidth(); mBitmapHeight = bitmap.getHeight(); mMiniBitmap = createMiniBitmap(bitmap); - mWallpaperColorExtractorCallback.onMiniBitmapUpdated(); + mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated(); recomputeColors(); } } @@ -206,7 +206,7 @@ public class WallpaperColorExtractor { boolean wasActive = isActive(); mPendingRegions.addAll(regions); if (!wasActive && isActive()) { - mWallpaperColorExtractorCallback.onActivated(); + mWallpaperLocalColorExtractorCallback.onActivated(); } processColorsInternal(); } @@ -228,7 +228,7 @@ public class WallpaperColorExtractor { mPendingRegions.removeAll(regions); regions.forEach(mProcessedRegions::remove); if (wasActive && !isActive()) { - mWallpaperColorExtractorCallback.onDeactivated(); + mWallpaperLocalColorExtractorCallback.onDeactivated(); } } } @@ -252,7 +252,7 @@ public class WallpaperColorExtractor { } private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) { - Trace.beginSection("WallpaperColorExtractor#createMiniBitmap"); + Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap"); // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap. int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight()); float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide); @@ -359,7 +359,7 @@ public class WallpaperColorExtractor { */ if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return; - Trace.beginSection("WallpaperColorExtractor#processColorsInternal"); + Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal"); List<WallpaperColors> processedColors = new ArrayList<>(); for (int i = 0; i < mPendingRegions.size(); i++) { RectF nextArea = mPendingRegions.get(i); @@ -372,7 +372,7 @@ public class WallpaperColorExtractor { mPendingRegions.clear(); Trace.endSection(); - mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors); + mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors); } /** diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 309f1681b964..02738d5ae48b 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -49,6 +49,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; +import com.android.systemui.notetask.NoteTaskInitializer; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.statusbar.CommandQueue; @@ -58,7 +59,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.systemui.tracing.nano.SystemUiTraceProto; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.nano.WmShellTraceProto; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; @@ -113,7 +113,6 @@ public final class WMShell implements private final Optional<Pip> mPipOptional; private final Optional<SplitScreen> mSplitScreenOptional; private final Optional<OneHanded> mOneHandedOptional; - private final Optional<FloatingTasks> mFloatingTasksOptional; private final Optional<DesktopMode> mDesktopModeOptional; private final CommandQueue mCommandQueue; @@ -125,6 +124,7 @@ public final class WMShell implements private final WakefulnessLifecycle mWakefulnessLifecycle; private final ProtoTracer mProtoTracer; private final UserTracker mUserTracker; + private final NoteTaskInitializer mNoteTaskInitializer; private final Executor mSysUiMainExecutor; // Listeners and callbacks. Note that we prefer member variable over anonymous class here to @@ -176,7 +176,6 @@ public final class WMShell implements Optional<Pip> pipOptional, Optional<SplitScreen> splitScreenOptional, Optional<OneHanded> oneHandedOptional, - Optional<FloatingTasks> floatingTasksOptional, Optional<DesktopMode> desktopMode, CommandQueue commandQueue, ConfigurationController configurationController, @@ -187,6 +186,7 @@ public final class WMShell implements ProtoTracer protoTracer, WakefulnessLifecycle wakefulnessLifecycle, UserTracker userTracker, + NoteTaskInitializer noteTaskInitializer, @Main Executor sysUiMainExecutor) { mContext = context; mShell = shell; @@ -203,7 +203,7 @@ public final class WMShell implements mWakefulnessLifecycle = wakefulnessLifecycle; mProtoTracer = protoTracer; mUserTracker = userTracker; - mFloatingTasksOptional = floatingTasksOptional; + mNoteTaskInitializer = noteTaskInitializer; mSysUiMainExecutor = sysUiMainExecutor; } @@ -226,6 +226,8 @@ public final class WMShell implements mSplitScreenOptional.ifPresent(this::initSplitScreen); mOneHandedOptional.ifPresent(this::initOneHanded); mDesktopModeOptional.ifPresent(this::initDesktopMode); + + mNoteTaskInitializer.initialize(); } @VisibleForTesting diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt new file mode 100644 index 000000000000..6c5620d42abb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt @@ -0,0 +1,76 @@ +/* + * 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.keyguard + +import android.os.PowerManager +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.settings.GlobalSettings +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class FaceWakeUpTriggersConfigTest : SysuiTestCase() { + @Mock lateinit var globalSettings: GlobalSettings + @Mock lateinit var dumpManager: DumpManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun testShouldTriggerFaceAuthOnWakeUpFrom_inConfig_returnsTrue() { + val faceWakeUpTriggersConfig = + createFaceWakeUpTriggersConfig( + intArrayOf(PowerManager.WAKE_REASON_POWER_BUTTON, PowerManager.WAKE_REASON_GESTURE) + ) + + assertTrue( + faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom( + PowerManager.WAKE_REASON_POWER_BUTTON + ) + ) + assertTrue( + faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom( + PowerManager.WAKE_REASON_GESTURE + ) + ) + assertFalse( + faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom( + PowerManager.WAKE_REASON_APPLICATION + ) + ) + } + + private fun createFaceWakeUpTriggersConfig(wakeUpTriggers: IntArray): FaceWakeUpTriggersConfig { + overrideResource( + com.android.systemui.R.array.config_face_auth_wake_up_triggers, + wakeUpTriggers + ) + + return FaceWakeUpTriggersConfig(mContext.getResources(), globalSettings, dumpManager) + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index c6233b54c028..66be6ecd9da4 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -110,6 +110,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.telephony.TelephonyListenerManager; +import com.android.systemui.util.settings.GlobalSettings; import org.junit.After; import org.junit.Assert; @@ -210,6 +211,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { private UiEventLogger mUiEventLogger; @Mock private PowerManager mPowerManager; + @Mock + private GlobalSettings mGlobalSettings; + private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; private final int mCurrentUserId = 100; private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0); @@ -237,8 +241,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mActivityService.getCurrentUser()).thenReturn(mCurrentUserInfo); when(mActivityService.getCurrentUserId()).thenReturn(mCurrentUserId); when(mFaceManager.isHardwareDetected()).thenReturn(true); - when(mFaceManager.hasEnrolledTemplates()).thenReturn(true); - when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true); + when(mAuthController.isFaceAuthEnrolled(anyInt())).thenReturn(true); when(mFaceManager.getSensorPropertiesInternal()).thenReturn(mFaceSensorProperties); when(mSessionTracker.getSessionId(SESSION_KEYGUARD)).thenReturn(mKeyguardInstanceId); @@ -294,6 +297,12 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .when(ActivityManager::getCurrentUser); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); + mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( + mContext.getResources(), + mGlobalSettings, + mDumpManager + ); + mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext); @@ -593,7 +602,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); verify(mFaceManager).isHardwareDetected(); - verify(mFaceManager).hasEnrolledTemplates(anyInt()); + verify(mFaceManager, never()).hasEnrolledTemplates(anyInt()); } @Test @@ -607,16 +616,22 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenKeyguard() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); - mTestableLooper.processAllMessages(); keyguardIsVisible(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mTestableLooper.processAllMessages(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); + verify(mUiEventLogger).logWithInstanceIdAndPosition( + eq(FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP), + eq(0), + eq(null), + any(), + eq(PowerManager.WAKE_REASON_POWER_BUTTON)); } @Test public void skipsAuthentication_whenStatusBarShadeLocked() { mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -630,7 +645,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { STRONG_AUTH_REQUIRED_AFTER_BOOT); mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(), @@ -684,7 +699,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(strongAuth); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); @@ -709,7 +724,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenTrustOnAgentKeyguard_ifBypass() { mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); when(mKeyguardBypassController.canBypass()).thenReturn(true); mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, @@ -721,7 +736,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testIgnoresAuth_whenTrustAgentOnKeyguard_withoutBypass() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */, KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>()); @@ -732,7 +747,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testIgnoresAuth_whenLockdown() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); @@ -744,7 +759,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testTriesToAuthenticate_whenLockout() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn( KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); @@ -768,7 +783,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFaceAndFingerprintLockout_onlyFace() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -779,7 +794,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFaceAndFingerprintLockout_onlyFingerprint() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -791,7 +806,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFaceAndFingerprintLockout() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -890,7 +905,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser))) .thenReturn(faceLockoutMode); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -1064,7 +1079,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testOccludingAppFingerprintListeningState() { // GIVEN keyguard isn't visible (app occluding) - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mKeyguardUpdateMonitor.setKeyguardShowing(true, true); when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true); @@ -1079,7 +1094,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testOccludingAppRequestsFingerprint() { // GIVEN keyguard isn't visible (app occluding) - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mKeyguardUpdateMonitor.setKeyguardShowing(true, true); // WHEN an occluding app requests fp @@ -1170,7 +1185,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { biometricsNotDisabledThroughDevicePolicyManager(); mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED); setKeyguardBouncerVisibility(false /* isVisible */); - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); when(mKeyguardBypassController.canBypass()).thenReturn(true); keyguardIsVisible(); @@ -1549,7 +1564,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); mTestableLooper.processAllMessages(); keyguardIsVisible(); @@ -1598,6 +1613,36 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); } + @Test + public void testDreamingStopped_faceDoesNotRun() { + mKeyguardUpdateMonitor.dispatchDreamingStopped(); + mTestableLooper.processAllMessages(); + + verify(mFaceManager, never()).authenticate( + any(), any(), any(), any(), anyInt(), anyBoolean()); + } + + @Test + public void testFaceWakeupTrigger_runFaceAuth_onlyOnConfiguredTriggers() { + // keyguard is visible + keyguardIsVisible(); + + // WHEN device wakes up from an application + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_APPLICATION); + mTestableLooper.processAllMessages(); + + // THEN face auth isn't triggered + verify(mFaceManager, never()).authenticate( + any(), any(), any(), any(), anyInt(), anyBoolean()); + + // WHEN device wakes up from the power button + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); + mTestableLooper.processAllMessages(); + + // THEN face auth is triggered + verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean()); + } + private void cleanupKeyguardUpdateMonitor() { if (mKeyguardUpdateMonitor != null) { mKeyguardUpdateMonitor.removeCallback(mTestCallback); @@ -1718,7 +1763,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void deviceIsInteractive() { - mKeyguardUpdateMonitor.dispatchStartedWakingUp(); + mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON); } private void bouncerFullyVisible() { @@ -1768,7 +1813,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker, mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, - mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager); + mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager, + mFaceWakeUpTriggersConfig); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java new file mode 100644 index 000000000000..ae8f419d4e64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -0,0 +1,225 @@ +/* + * 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.keyguard; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedStateListDrawable; +import android.util.Pair; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; + +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.AuthController; +import com.android.systemui.biometrics.AuthRippleController; +import com.android.systemui.doze.util.BurnInHelperKt; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; +import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.concurrency.FakeExecutor; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.After; +import org.junit.Before; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +public class LockIconViewControllerBaseTest extends SysuiTestCase { + protected static final String UNLOCKED_LABEL = "unlocked"; + protected static final int PADDING = 10; + + protected MockitoSession mStaticMockSession; + + protected @Mock LockIconView mLockIconView; + protected @Mock AnimatedStateListDrawable mIconDrawable; + protected @Mock Context mContext; + protected @Mock Resources mResources; + protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; + protected @Mock StatusBarStateController mStatusBarStateController; + protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; + protected @Mock KeyguardViewController mKeyguardViewController; + protected @Mock KeyguardStateController mKeyguardStateController; + protected @Mock FalsingManager mFalsingManager; + protected @Mock AuthController mAuthController; + protected @Mock DumpManager mDumpManager; + protected @Mock AccessibilityManager mAccessibilityManager; + protected @Mock ConfigurationController mConfigurationController; + protected @Mock VibratorHelper mVibrator; + protected @Mock AuthRippleController mAuthRippleController; + protected @Mock FeatureFlags mFeatureFlags; + protected @Mock KeyguardTransitionRepository mTransitionRepository; + protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); + + protected LockIconViewController mUnderTest; + + // Capture listeners so that they can be used to send events + @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); + + @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = + ArgumentCaptor.forClass(KeyguardStateController.Callback.class); + protected KeyguardStateController.Callback mKeyguardStateCallback; + + @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + protected StatusBarStateController.StateListener mStatusBarStateListener; + + @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; + protected AuthController.Callback mAuthControllerCallback; + + @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback> + mKeyguardUpdateMonitorCallbackCaptor = + ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); + protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; + + @Captor protected ArgumentCaptor<Point> mPointCaptor; + + @Before + public void setUp() throws Exception { + mStaticMockSession = mockitoSession() + .mockStatic(BurnInHelperKt.class) + .strictness(Strictness.LENIENT) + .startMocking(); + MockitoAnnotations.initMocks(this); + + setupLockIconViewMocks(); + when(mContext.getResources()).thenReturn(mResources); + when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); + Rect windowBounds = new Rect(0, 0, 800, 1200); + when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); + when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); + when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); + when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); + when(mAuthController.getScaleFactor()).thenReturn(1f); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + + mUnderTest = new LockIconViewController( + mLockIconView, + mStatusBarStateController, + mKeyguardUpdateMonitor, + mKeyguardViewController, + mKeyguardStateController, + mFalsingManager, + mAuthController, + mDumpManager, + mAccessibilityManager, + mConfigurationController, + mDelayableExecutor, + mVibrator, + mAuthRippleController, + mResources, + new KeyguardTransitionInteractor(mTransitionRepository), + new KeyguardInteractor(new FakeKeyguardRepository()), + mFeatureFlags + ); + } + + @After + public void tearDown() { + mStaticMockSession.finishMocking(); + } + + protected Pair<Float, Point> setupUdfps() { + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); + final Point udfpsLocation = new Point(50, 75); + final float radius = 33f; + when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); + when(mAuthController.getUdfpsRadius()).thenReturn(radius); + + return new Pair(radius, udfpsLocation); + } + + protected void setupShowLockIcon() { + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); + when(mStatusBarStateController.isDozing()).thenReturn(false); + when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); + when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); + } + + protected void captureAuthControllerCallback() { + verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); + mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); + } + + protected void captureKeyguardStateCallback() { + verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); + mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); + } + + protected void captureStatusBarStateListener() { + verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); + mStatusBarStateListener = mStatusBarStateCaptor.getValue(); + } + + protected void captureKeyguardUpdateMonitorCallback() { + verify(mKeyguardUpdateMonitor).registerCallback( + mKeyguardUpdateMonitorCallbackCaptor.capture()); + mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); + } + + protected void setupLockIconViewMocks() { + when(mLockIconView.getResources()).thenReturn(mResources); + when(mLockIconView.getContext()).thenReturn(mContext); + } + + protected void resetLockIconView() { + reset(mLockIconView); + setupLockIconViewMocks(); + } + + protected void init(boolean useMigrationFlag) { + when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag); + mUnderTest.init(); + + verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture()); + mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView); + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java new file mode 100644 index 000000000000..f4c2284de2d3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.keyguard; + +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + +import static com.android.keyguard.LockIconView.ICON_LOCK; +import static com.android.keyguard.LockIconView.ICON_UNLOCK; + +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.hardware.biometrics.BiometricSourceType; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Pair; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.doze.util.BurnInHelperKt; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { + + @Test + public void testUpdateFingerprintLocationOnInit() { + // GIVEN fp sensor location is available pre-attached + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated to the udfps location with UDFPS radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdatePaddingBasedOnResolutionScale() { + // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 + Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location + when(mAuthController.getScaleFactor()).thenReturn(5f); + + // WHEN lock icon view controller is initialized and attached + init(/* useMigrationFlag= */false); + + // THEN lock icon view location is updated with the scaled radius + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING * 5)); + } + + @Test + public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN all authenticators are registered + mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testUpdateLockIconLocationOnUdfpsLocationChanged() { + // GIVEN fp sensor location is not available pre-init + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); + init(/* useMigrationFlag= */false); + resetLockIconView(); // reset any method call counts for when we verify method calls later + + // GIVEN fp sensor location is available post-attached + captureAuthControllerCallback(); + Pair<Float, Point> udfps = setupUdfps(); + + // WHEN udfps location changes + mAuthControllerCallback.onUdfpsLocationChanged(); + mDelayableExecutor.runAllReady(); + + // THEN lock icon view location is updated with the same coordinates as auth controller vals + verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), + eq(PADDING)); + } + + @Test + public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { + // GIVEN Udpfs sensor location is available + setupUdfps(); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be enabled + verify(mLockIconView).setUseBackground(true); + } + + @Test + public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { + // GIVEN Udfps sensor location is not supported + when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); + + // WHEN the view is attached + init(/* useMigrationFlag= */false); + + // THEN the lock icon view background should be disabled + verify(mLockIconView).setUseBackground(false); + } + + @Test + public void testUnlockIconShows_biometricUnlockedTrue() { + // GIVEN UDFPS sensor location is available + setupUdfps(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardUpdateMonitorCallback(); + + // GIVEN user has unlocked with a biometric auth (ie: face auth) + when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); + reset(mLockIconView); + + // WHEN face auth's biometric running state changes + mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, + BiometricSourceType.FACE); + + // THEN the unlock icon is shown + verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); + } + + @Test + public void testLockIconStartState() { + // GIVEN lock icon state + setupShowLockIcon(); + + // WHEN lock icon controller is initialized + init(/* useMigrationFlag= */false); + + // THEN the lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, false); + } + + @Test + public void testLockIcon_updateToUnlock() { + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureKeyguardStateCallback(); + reset(mLockIconView); + + // WHEN the unlocked state changes to canDismissLockScreen=true + when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); + mKeyguardStateCallback.onUnlockedChanged(); + + // THEN the unlock should show + verify(mLockIconView).updateIcon(ICON_UNLOCK, false); + } + + @Test + public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { + // GIVEN udfps not enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the icon is cleared + verify(mLockIconView).clearIcon(); + } + + @Test + public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN starting state for the lock icon + setupShowLockIcon(); + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN the dozing state changes + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true); + } + + @Test + public void testBurnInOffsetsUpdated_onDozeAmountChanged() { + // GIVEN udfps enrolled + setupUdfps(); + when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); + + // GIVEN burn-in offset = 5 + int burnInOffset = 5; + when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon(); + init(/* useMigrationFlag= */false); + captureStatusBarStateListener(); + reset(mLockIconView); + + // WHEN dozing updates + mStatusBarStateListener.onDozingChanged(true /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(1f, 1f); + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset); + verify(mLockIconView).setTranslationX(burnInOffset); + reset(mLockIconView); + + // WHEN the device is no longer dozing + mStatusBarStateListener.onDozingChanged(false /* isDozing */); + mStatusBarStateListener.onDozeAmountChanged(0f, 0f); + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0); + verify(mLockIconView).setTranslationX(0); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt new file mode 100644 index 000000000000..d2c54b4cc0e7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt @@ -0,0 +1,123 @@ +/* + * 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.keyguard + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.keyguard.LockIconView.ICON_LOCK +import com.android.systemui.doze.util.getBurnInOffset +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() { + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps not enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the icon is cleared + verify(mLockIconView).clearIcon() + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN starting state for the lock icon + setupShowLockIcon() + + // GIVEN lock icon controller is initialized and view is attached + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN the dozing state changes + mUnderTest.mIsDozingCallback.accept(true) + + // THEN the AOD lock icon should show + verify(mLockIconView).updateIcon(ICON_LOCK, true) + } + + /** After migration, replaces LockIconViewControllerTest version */ + @Test + fun testBurnInOffsetsUpdated_onDozeAmountChanged() = + runBlocking(IMMEDIATE) { + // GIVEN udfps enrolled + setupUdfps() + whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true) + + // GIVEN burn-in offset = 5 + val burnInOffset = 5 + whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset) + + // GIVEN starting state for the lock icon (keyguard) + setupShowLockIcon() + init(/* useMigrationFlag= */ true) + reset(mLockIconView) + + // WHEN dozing updates + mUnderTest.mIsDozingCallback.accept(true) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + + // THEN the view's translation is updated to use the AoD burn-in offsets + verify(mLockIconView).setTranslationY(burnInOffset.toFloat()) + verify(mLockIconView).setTranslationX(burnInOffset.toFloat()) + reset(mLockIconView) + + // WHEN the device is no longer dozing + mUnderTest.mIsDozingCallback.accept(false) + mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED)) + + // THEN the view is updated to NO translation (no burn-in offsets anymore) + verify(mLockIconView).setTranslationY(0f) + verify(mLockIconView).setTranslationX(0f) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java index 19a6c66652dd..77d38c58e685 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java @@ -35,6 +35,7 @@ import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -68,6 +69,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { public MockitoRule mockito = MockitoJUnit.rule(); private Context mContextWrapper; + private AccessibilityManager mAccessibilityManager; private KeyguardUpdateMonitor mKeyguardUpdateMonitor; private AccessibilityFloatingMenuController mController; private AccessibilityButtonTargetsObserver mTargetsObserver; @@ -87,6 +89,7 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { } }; + mAccessibilityManager = mContext.getSystemService(AccessibilityManager.class); mLastButtonTargets = Settings.Secure.getStringForUser(mContextWrapper.getContentResolver(), Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT); mLastButtonMode = Settings.Secure.getIntForUser(mContextWrapper.getContentResolver(), @@ -348,8 +351,8 @@ public class AccessibilityFloatingMenuControllerTest extends SysuiTestCase { mKeyguardUpdateMonitor = Dependency.get(KeyguardUpdateMonitor.class); final AccessibilityFloatingMenuController controller = new AccessibilityFloatingMenuController(mContextWrapper, windowManager, - displayManager, mTargetsObserver, mModeObserver, mKeyguardUpdateMonitor, - featureFlags); + displayManager, mAccessibilityManager, mTargetsObserver, mModeObserver, + mKeyguardUpdateMonitor, featureFlags); controller.init(); return controller; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java new file mode 100644 index 000000000000..8ef65dcb2c3a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.floatingmenu; + +import static org.mockito.Mockito.verify; + +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.WindowManager; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.wm.shell.bubbles.DismissView; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link DismissAnimationController}. */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class DismissAnimationControllerTest extends SysuiTestCase { + private DismissAnimationController mDismissAnimationController; + private DismissView mDismissView; + + @Before + public void setUp() throws Exception { + final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, + stubWindowManager); + final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, + stubMenuViewAppearance); + mDismissView = new DismissView(mContext); + mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView); + } + + @Test + public void showDismissView_success() { + mDismissAnimationController.showDismissView(true); + + verify(mDismissView).show(); + } + + @Test + public void hideDismissView_success() { + mDismissAnimationController.showDismissView(false); + + verify(mDismissView).hide(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index dbf291c49ee5..d0bd4f7026eb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -18,9 +18,16 @@ package com.android.systemui.accessibility.floatingmenu; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + import android.graphics.PointF; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; +import android.view.ViewPropertyAnimator; import android.view.WindowManager; import androidx.test.filters.SmallTest; @@ -36,6 +43,8 @@ import org.junit.runner.RunWith; @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class MenuAnimationControllerTest extends SysuiTestCase { + + private ViewPropertyAnimator mViewPropertyAnimator; private MenuView mMenuView; private MenuAnimationController mMenuAnimationController; @@ -45,7 +54,11 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); - mMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mViewPropertyAnimator = spy(mMenuView.animate()); + doReturn(mViewPropertyAnimator).when(mMenuView).animate(); + mMenuAnimationController = new MenuAnimationController(mMenuView); } @@ -58,4 +71,20 @@ public class MenuAnimationControllerTest extends SysuiTestCase { assertThat(mMenuView.getTranslationX()).isEqualTo(50); assertThat(mMenuView.getTranslationY()).isEqualTo(60); } + + @Test + public void startShrinkAnimation_verifyAnimationEndAction() { + mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(View.VISIBLE)); + + verify(mViewPropertyAnimator).withEndAction(any(Runnable.class)); + } + + @Test + public void startGrowAnimation_menuCompletelyOpaque() { + mMenuAnimationController.startShrinkAnimation(null); + + mMenuAnimationController.startGrowAnimation(); + + assertThat(mMenuView.getAlpha()).isEqualTo(/* completelyOpaque */ 1.0f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index bf6d574a0f67..78ee627a9a2f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -43,6 +43,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -54,6 +55,9 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { @Rule public MockitoRule mockito = MockitoJUnit.rule(); + @Mock + private DismissAnimationController.DismissCallback mStubDismissCallback; + private RecyclerView mStubListView; private MenuView mMenuView; private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate; @@ -87,7 +91,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info); - assertThat(info.getActionList().size()).isEqualTo(5); + assertThat(info.getActionList().size()).isEqualTo(6); } @Test @@ -156,6 +160,17 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { } @Test + public void performRemoveMenuAction_success() { + mMenuAnimationController.setDismissCallback(mStubDismissCallback); + final boolean removeMenuAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_remove_menu, null); + + assertThat(removeMenuAction).isTrue(); + verify(mMenuAnimationController).removeMenu(); + } + + @Test public void performFocusAction_fadeIn() { mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, ACTION_ACCESSIBILITY_FOCUS, null); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index c5b9a294fc34..4acb394bee95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -21,6 +21,8 @@ import static android.view.View.OVER_SCROLL_NEVER; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -36,6 +38,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.MotionEventHelper; +import com.android.wm.shell.bubbles.DismissView; import org.junit.After; import org.junit.Before; @@ -57,7 +60,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { private MenuView mStubMenuView; private MenuListViewTouchHandler mTouchHandler; private MenuAnimationController mMenuAnimationController; + private DismissAnimationController mDismissAnimationController; private RecyclerView mStubListView; + private DismissView mDismissView; @Before public void setUp() throws Exception { @@ -69,7 +74,11 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { mStubMenuView.setTranslationX(0); mStubMenuView.setTranslationY(0); mMenuAnimationController = spy(new MenuAnimationController(mStubMenuView)); - mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController); + mDismissView = spy(new DismissView(mContext)); + mDismissAnimationController = + spy(new DismissAnimationController(mDismissView, mStubMenuView)); + mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController, + mDismissAnimationController); final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets); mStubListView = (RecyclerView) mStubMenuView.getChildAt(0); mStubListView.setAdapter(stubAdapter); @@ -88,7 +97,9 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test - public void onActionMoveEvent_shouldMoveToPosition() { + public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() { + doReturn(false).when(mDismissAnimationController).maybeConsumeMoveMotionEvent( + any(MotionEvent.class)); final int offset = 100; final MotionEvent stubDownEvent = mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, @@ -108,6 +119,24 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test + public void onActionMoveEvent_shouldShowDismissView() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + + verify(mDismissView).show(); + } + + @Test public void dragAndDrop_shouldFlingMenuThenSpringToEdge() { final int offset = 100; final MotionEvent stubDownEvent = diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java index 8c8d6aca7cd7..dd7ce0e06c32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerControllerTest.java @@ -34,6 +34,7 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -59,6 +60,9 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { private WindowManager mWindowManager; @Mock + private AccessibilityManager mAccessibilityManager; + + @Mock private WindowMetrics mWindowMetrics; private MenuViewLayerController mMenuViewLayerController; @@ -72,7 +76,8 @@ public class MenuViewLayerControllerTest extends SysuiTestCase { when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); when(mWindowMetrics.getBounds()).thenReturn(new Rect(0, 0, 1080, 2340)); when(mWindowMetrics.getWindowInsets()).thenReturn(stubDisplayInsets()); - mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager); + mMenuViewLayerController = new MenuViewLayerController(mContext, mWindowManager, + mAccessibilityManager); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 23c6ef1338b3..d20eeafde09c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -23,18 +23,25 @@ import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.Laye import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; + import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Tests for {@link MenuViewLayer}. */ @RunWith(AndroidTestingRunner.class) @@ -43,10 +50,19 @@ import org.junit.runner.RunWith; public class MenuViewLayerTest extends SysuiTestCase { private MenuViewLayer mMenuViewLayer; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private IAccessibilityFloatingMenu mFloatingMenu; + @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager); + final AccessibilityManager stubAccessibilityManager = mContext.getSystemService( + AccessibilityManager.class); + mMenuViewLayer = new MenuViewLayer(mContext, stubWindowManager, stubAccessibilityManager, + mFloatingMenu); } @Test @@ -64,4 +80,11 @@ public class MenuViewLayerTest extends SysuiTestCase { assertThat(menuView.getVisibility()).isEqualTo(GONE); } + + @Test + public void tiggerDismissMenuAction_hideFloatingMenu() { + mMenuViewLayer.mDismissMenuAction.run(); + + verify(mFloatingMenu).hide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index d1107c612977..45b8ce1ce247 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -41,10 +41,15 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.internal.widget.LockPatternUtils import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers import org.junit.After import org.junit.Rule import org.junit.Test @@ -80,6 +85,15 @@ class AuthContainerViewTest : SysuiTestCase() { @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + private val biometricPromptRepository = FakePromptRepository() + private val credentialInteractor = FakeCredentialInteractor() + private val bpCredentialInteractor = BiometricPromptCredentialInteractor( + Dispatchers.Main.immediate, + biometricPromptRepository, + credentialInteractor + ) + private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) + private var authContainer: TestAuthContainerView? = null @After @@ -466,6 +480,8 @@ class AuthContainerViewTest : SysuiTestCase() { userManager, lockPatternUtils, interactionJankMonitor, + { bpCredentialInteractor }, + { credentialViewModel }, Handler(TestableLooper.get(this).looper), FakeExecutor(FakeSystemClock()) ) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index 8e45067c8e13..4dd46edd0912 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -25,7 +25,9 @@ import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWA import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -87,6 +89,8 @@ import com.android.internal.R; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.widget.LockPatternUtils; import com.android.systemui.SysuiTestCase; +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor; +import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.CommandQueue; @@ -163,6 +167,11 @@ public class AuthControllerTest extends SysuiTestCase { private UdfpsLogger mUdfpsLogger; @Mock private InteractionJankMonitor mInteractionJankMonitor; + @Mock + private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor; + @Mock + private CredentialViewModel mCredentialViewModel; + @Captor private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor; @Captor @@ -236,7 +245,7 @@ public class AuthControllerTest extends SysuiTestCase { 2 /* sensorId */, SensorProperties.STRENGTH_STRONG, 1 /* maxEnrollmentsPerUser */, - fpComponentInfo, + faceComponentInfo, FaceSensorProperties.TYPE_RGB, true /* supportsFaceDetection */, true /* supportsSelfIllumination */, @@ -276,8 +285,6 @@ public class AuthControllerTest extends SysuiTestCase { reset(mFingerprintManager); reset(mFaceManager); - when(mVibratorHelper.hasVibrator()).thenReturn(true); - // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, @@ -308,8 +315,6 @@ public class AuthControllerTest extends SysuiTestCase { reset(mFingerprintManager); reset(mFaceManager); - when(mVibratorHelper.hasVibrator()).thenReturn(true); - // This test requires an uninitialized AuthController. AuthController authController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager, @@ -343,6 +348,36 @@ public class AuthControllerTest extends SysuiTestCase { } @Test + public void testFaceAuthEnrollmentStatus() throws RemoteException { + final int userId = 0; + + reset(mFaceManager); + mAuthController.start(); + + verify(mFaceManager).addAuthenticatorsRegisteredCallback( + mFaceAuthenticatorsRegisteredCaptor.capture()); + + mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered( + mFaceManager.getSensorPropertiesInternal()); + mTestableLooper.processAllMessages(); + + verify(mFaceManager).registerBiometricStateListener( + mBiometricStateCaptor.capture()); + + assertFalse(mAuthController.isFaceAuthEnrolled(userId)); + + // Enrollments changed for an unknown sensor. + for (BiometricStateListener listener : mBiometricStateCaptor.getAllValues()) { + listener.onEnrollmentsChanged(userId, + 2 /* sensorId */, true /* hasEnrollments */); + } + mTestableLooper.processAllMessages(); + + assertTrue(mAuthController.isFaceAuthEnrolled(userId)); + } + + + @Test public void testSendsReasonUserCanceled_whenDismissedByUserCancel() throws Exception { showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); mAuthController.onDismissed(AuthDialogCallback.DISMISSED_USER_CANCELED, @@ -981,6 +1016,7 @@ public class AuthControllerTest extends SysuiTestCase { fingerprintManager, faceManager, udfpsControllerFactory, sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle, mUserManager, mLockPatternUtils, mUdfpsLogger, statusBarStateController, + () -> mBiometricPromptCredentialInteractor, () -> mCredentialViewModel, mInteractionJankMonitor, mHandler, mBackgroundExecutor, vibratorHelper); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 8820c164cba4..1379a0eeebdd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -22,12 +22,11 @@ import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.SensorProperties -import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.face.FaceSensorProperties +import android.hardware.face.FaceSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.os.Bundle - import android.testing.ViewUtils import android.view.LayoutInflater @@ -83,26 +82,31 @@ internal fun AuthBiometricView?.destroyDialog() { internal fun fingerprintSensorPropertiesInternal( ids: List<Int> = listOf(0) ): List<FingerprintSensorPropertiesInternal> { - val componentInfo = listOf( + val componentInfo = + listOf( ComponentInfoInternal( - "fingerprintSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */ + "fingerprintSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, + "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, + "" /* softwareVersion */ ), ComponentInfoInternal( - "matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */ + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, + "" /* firmwareVersion */, + "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ ) - ) + ) return ids.map { id -> FingerprintSensorPropertiesInternal( - id, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_REAR, - false /* resetLockoutRequiresHardwareAuthToken */ + id, + SensorProperties.STRENGTH_STRONG, + 5 /* maxEnrollmentsPerUser */, + componentInfo, + FingerprintSensorProperties.TYPE_REAR, + false /* resetLockoutRequiresHardwareAuthToken */ ) } } @@ -111,28 +115,53 @@ internal fun fingerprintSensorPropertiesInternal( internal fun faceSensorPropertiesInternal( ids: List<Int> = listOf(1) ): List<FaceSensorPropertiesInternal> { - val componentInfo = listOf( + val componentInfo = + listOf( ComponentInfoInternal( - "faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */ + "faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, + "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, + "" /* softwareVersion */ ), ComponentInfoInternal( - "matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */ + "matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, + "" /* firmwareVersion */, + "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */ ) - ) + ) return ids.map { id -> FaceSensorPropertiesInternal( - id, - SensorProperties.STRENGTH_STRONG, - 2 /* maxEnrollmentsPerUser */, - componentInfo, - FaceSensorProperties.TYPE_RGB, - true /* supportsFaceDetection */, - true /* supportsSelfIllumination */, - false /* resetLockoutRequiresHardwareAuthToken */ + id, + SensorProperties.STRENGTH_STRONG, + 2 /* maxEnrollmentsPerUser */, + componentInfo, + FaceSensorProperties.TYPE_RGB, + true /* supportsFaceDetection */, + true /* supportsSelfIllumination */, + false /* resetLockoutRequiresHardwareAuthToken */ ) } } + +internal fun promptInfo( + title: String = "title", + subtitle: String = "sub", + description: String = "desc", + credentialTitle: String? = "cred title", + credentialSubtitle: String? = "cred sub", + credentialDescription: String? = "cred desc", + negativeButton: String = "neg", +): PromptInfo { + val info = PromptInfo() + info.title = title + info.subtitle = subtitle + info.description = description + credentialTitle?.let { info.deviceCredentialTitle = it } + credentialSubtitle?.let { info.deviceCredentialSubtitle = it } + credentialDescription?.let { info.deviceCredentialDescription = it } + info.negativeButtonText = negativeButton + return info +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt new file mode 100644 index 000000000000..2d5614c15173 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -0,0 +1,81 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(JUnit4::class) +class PromptRepositoryImplTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var authController: AuthController + + private lateinit var repository: PromptRepositoryImpl + + @Before + fun setup() { + repository = PromptRepositoryImpl(authController) + } + + @Test + fun isShowing() = runBlockingTest { + whenever(authController.isShowing).thenReturn(true) + + val values = mutableListOf<Boolean>() + val job = launch { repository.isShowing.toList(values) } + assertThat(values).containsExactly(true) + + withArgCaptor<AuthController.Callback> { + verify(authController).addCallback(capture()) + + value.onBiometricPromptShown() + assertThat(values).containsExactly(true, true) + + value.onBiometricPromptDismissed() + assertThat(values).containsExactly(true, true, false).inOrder() + + job.cancel() + verify(authController).removeCallback(eq(value)) + } + } + + @Test + fun setsAndUnsetsPrompt() = runBlockingTest { + val kind = PromptKind.PIN + val uid = 8 + val challenge = 90L + val promptInfo = PromptInfo() + + repository.setPrompt(promptInfo, uid, challenge, kind) + + assertThat(repository.kind.value).isEqualTo(kind) + assertThat(repository.userId.value).isEqualTo(uid) + assertThat(repository.challenge.value).isEqualTo(challenge) + assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + + repository.unsetPrompt() + + assertThat(repository.promptInfo.value).isNull() + assertThat(repository.userId.value).isNull() + assertThat(repository.challenge.value).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt new file mode 100644 index 000000000000..97d3e688ed80 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt @@ -0,0 +1,216 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResourcesManager +import android.content.pm.UserInfo +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.internal.widget.LockscreenCredential +import com.android.internal.widget.VerifyCredentialResponse +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.domain.model.BiometricOperationInfo +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.biometrics.domain.model.BiometricUserInfo +import com.android.systemui.biometrics.promptInfo +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +private const val USER_ID = 22 +private const val OPERATION_ID = 100L +private const val MAX_ATTEMPTS = 5 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class CredentialInteractorImplTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + @Mock private lateinit var userManager: UserManager + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var devicePolicyResourcesManager: DevicePolicyResourcesManager + + private val systemClock = FakeSystemClock() + + private lateinit var interactor: CredentialInteractorImpl + + @Before + fun setup() { + whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager) + whenever(lockPatternUtils.getMaximumFailedPasswordsForWipe(anyInt())) + .thenReturn(MAX_ATTEMPTS) + whenever(userManager.getUserInfo(eq(USER_ID))).thenReturn(UserInfo(USER_ID, "", 0)) + whenever(devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(eq(USER_ID))) + .thenReturn(USER_ID) + + interactor = + CredentialInteractorImpl( + mContext, + lockPatternUtils, + userManager, + devicePolicyManager, + systemClock + ) + } + + @Test + fun testStealthMode() { + for (value in listOf(true, false, false, true)) { + whenever(lockPatternUtils.isVisiblePatternEnabled(eq(USER_ID))).thenReturn(value) + + assertThat(interactor.isStealthModeActive(USER_ID)).isEqualTo(!value) + } + } + + @Test + fun testCredentialOwner() { + for (value in listOf(12, 8, 4)) { + whenever(userManager.getCredentialOwnerProfile(eq(USER_ID))).thenReturn(value) + + assertThat(interactor.getCredentialOwnerOrSelfId(USER_ID)).isEqualTo(value) + } + } + + @Test fun pinCredentialWhenGood() = pinCredential(goodCredential()) + + @Test fun pinCredentialWhenBad() = pinCredential(badCredential()) + + @Test fun pinCredentialWhenBadAndThrottled() = pinCredential(badCredential(timeout = 5_000)) + + private fun pinCredential(result: VerifyCredentialResponse) = runTest { + val usedAttempts = 1 + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(usedAttempts) + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())).thenReturn(result) + whenever(lockPatternUtils.verifyGatekeeperPasswordHandle(anyLong(), anyLong(), eq(USER_ID))) + .thenReturn(result) + whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer { + systemClock.elapsedRealtime() + (it.arguments[1] as Int) + } + + // wrap in an async block so the test can advance the clock if throttling credential + // checks prevents the method from returning + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) + .toList(statusList) + + val last = statusList.removeLastOrNull() + if (result.isMatched) { + assertThat(statusList).isEmpty() + val successfulResult = last as? CredentialStatus.Success.Verified + assertThat(successfulResult).isNotNull() + assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT) + + verify(lockPatternUtils).userPresent(eq(USER_ID)) + verify(lockPatternUtils) + .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle)) + } else { + val failedResult = last as? CredentialStatus.Fail.Error + assertThat(failedResult).isNotNull() + assertThat(failedResult!!.remainingAttempts) + .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1) + assertThat(failedResult.urgentMessage).isNull() + + if (result.timeout > 0) { // failed and throttled + // messages are in the throttled errors, so the final Error.error is empty + assertThat(failedResult.error).isEmpty() + assertThat(statusList).isNotEmpty() + assertThat(statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java)) + .hasSize(statusList.size) + + verify(lockPatternUtils).setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout)) + } else { // failed + assertThat(failedResult.error) + .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern()) + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } + } + } + + @Test + fun pinCredentialWhenBadAndFinalAttempt() = runTest { + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())) + .thenReturn(badCredential()) + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(MAX_ATTEMPTS - 2) + + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) + .toList(statusList) + + val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error + assertThat(result).isNotNull() + assertThat(result!!.remainingAttempts).isEqualTo(1) + assertThat(result.urgentMessage).isNotEmpty() + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } + + @Test + fun pinCredentialWhenBadAndNoMoreAttempts() = runTest { + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())) + .thenReturn(badCredential()) + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(MAX_ATTEMPTS - 1) + whenever(devicePolicyResourcesManager.getString(any(), any())).thenReturn("wipe") + + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) + .toList(statusList) + + val result = statusList.removeLastOrNull() as? CredentialStatus.Fail.Error + assertThat(result).isNotNull() + assertThat(result!!.remainingAttempts).isEqualTo(0) + assertThat(result.urgentMessage).isNotEmpty() + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } +} + +private fun pinRequest(): BiometricPromptRequest.Credential.Pin = + BiometricPromptRequest.Credential.Pin( + promptInfo(), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ) + +private fun goodCredential( + passwordHandle: Long = 90, + hat: ByteArray = ByteArray(69), +): VerifyCredentialResponse = + VerifyCredentialResponse.Builder() + .setGatekeeperPasswordHandle(passwordHandle) + .setGatekeeperHAT(hat) + .build() + +private fun badCredential(timeout: Int = 0): VerifyCredentialResponse = + if (timeout > 0) { + VerifyCredentialResponse.fromTimeout(timeout) + } else { + VerifyCredentialResponse.fromError() + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt new file mode 100644 index 000000000000..dbcbf415221e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -0,0 +1,270 @@ +package com.android.systemui.biometrics.domain.interactor + +import android.hardware.biometrics.PromptInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.Utils +import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.domain.model.BiometricOperationInfo +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import com.android.systemui.biometrics.domain.model.BiometricUserInfo +import com.android.systemui.biometrics.promptInfo +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.junit.MockitoJUnit + +private const val USER_ID = 22 +private const val OPERATION_ID = 100L + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class PromptCredentialInteractorTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + + private val dispatcher = UnconfinedTestDispatcher() + private val biometricPromptRepository = FakePromptRepository() + private val credentialInteractor = FakeCredentialInteractor() + + private lateinit var interactor: BiometricPromptCredentialInteractor + + @Before + fun setup() { + interactor = + BiometricPromptCredentialInteractor( + dispatcher, + biometricPromptRepository, + credentialInteractor + ) + } + + @Test + fun testIsShowing() = + runTest(dispatcher) { + var showing = false + val job = launch { interactor.isShowing.collect { showing = it } } + + biometricPromptRepository.setIsShowing(false) + assertThat(showing).isFalse() + + biometricPromptRepository.setIsShowing(true) + assertThat(showing).isTrue() + + job.cancel() + } + + @Test + fun testShowError() = + runTest(dispatcher) { + var error: CredentialStatus.Fail? = null + val job = launch { interactor.verificationError.collect { error = it } } + + for (msg in listOf("once", "again")) { + interactor.setVerificationError(error(msg)) + assertThat(error).isEqualTo(error(msg)) + } + + interactor.resetVerificationError() + assertThat(error).isNull() + + job.cancel() + } + + @Test + fun nullWhenNoPromptInfo() = + runTest(dispatcher) { + var prompt: BiometricPromptRequest? = null + val job = launch { interactor.prompt.collect { prompt = it } } + + assertThat(prompt).isNull() + + job.cancel() + } + + @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN) + + @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD) + + @Test fun usePatternCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PATTERN) + + private fun useCredentialForPrompt(kind: Int) = + runTest(dispatcher) { + val isStealth = false + credentialInteractor.stealthMode = isStealth + + var prompt: BiometricPromptRequest? = null + val job = launch { interactor.prompt.collect { prompt = it } } + + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + }, + kind = kind, + userId = USER_ID, + challenge = OPERATION_ID + ) + + val p = prompt as? BiometricPromptRequest.Credential + assertThat(p).isNotNull() + assertThat(p!!.title).isEqualTo(title) + assertThat(p.subtitle).isEqualTo(subtitle) + assertThat(p.description).isEqualTo(description) + assertThat(p.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) + assertThat(p.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) + assertThat(p) + .isInstanceOf( + when (kind) { + Utils.CREDENTIAL_PIN -> BiometricPromptRequest.Credential.Pin::class.java + Utils.CREDENTIAL_PASSWORD -> + BiometricPromptRequest.Credential.Password::class.java + Utils.CREDENTIAL_PATTERN -> + BiometricPromptRequest.Credential.Pattern::class.java + else -> throw Exception("wrong kind") + } + ) + if (p is BiometricPromptRequest.Credential.Pattern) { + assertThat(p.stealthMode).isEqualTo(isStealth) + } + + interactor.resetPrompt() + + assertThat(prompt).isNull() + + job.cancel() + } + + @Test + fun checkCredential() = + runTest(dispatcher) { + val hat = ByteArray(4) + credentialInteractor.verifyCredentialResponse = { _ -> flowOf(verified(hat)) } + + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Success.Verified + + assertThat(checked).isNotNull() + assertThat(checked!!.hat).isSameInstanceAs(hat) + assertThat(errors.map { it?.error }).containsExactly(null) + + job.cancel() + } + + @Test + fun checkCredentialWhenBad() = + runTest(dispatcher) { + val errorMessage = "bad" + val remainingAttempts = 12 + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(error(errorMessage, remainingAttempts)) + } + + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Fail.Error + + assertThat(checked).isNotNull() + assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts) + assertThat(checked.urgentMessage).isNull() + assertThat(errors.map { it?.error }).containsExactly(null, errorMessage).inOrder() + + job.cancel() + } + + @Test + fun checkCredentialWhenBadAndUrgentMessage() = + runTest(dispatcher) { + val error = "not so bad" + val urgentMessage = "really bad" + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(error(error, 10, urgentMessage)) + } + + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Fail.Error + + assertThat(checked).isNotNull() + assertThat(checked!!.urgentMessage).isEqualTo(urgentMessage) + assertThat(errors.map { it?.error }).containsExactly(null, error).inOrder() + assertThat(errors.last() as? CredentialStatus.Fail.Error) + .isEqualTo(error(error, 10, urgentMessage)) + + job.cancel() + } + + @Test + fun checkCredentialWhenBadAndThrottled() = + runTest(dispatcher) { + val remainingAttempts = 3 + val error = ":(" + val urgentMessage = ":D" + credentialInteractor.verifyCredentialResponse = { _ -> + flow { + for (i in 1..3) { + emit(throttled("$i")) + delay(100) + } + emit(error(error, remainingAttempts, urgentMessage)) + } + } + val errors = mutableListOf<CredentialStatus.Fail?>() + val job = launch { interactor.verificationError.toList(errors) } + + val checked = + interactor.checkCredential(pinRequest(), text = "1234") + as? CredentialStatus.Fail.Error + + assertThat(checked).isNotNull() + assertThat(checked!!.remainingAttempts).isEqualTo(remainingAttempts) + assertThat(checked.urgentMessage).isEqualTo(urgentMessage) + assertThat(errors.map { it?.error }) + .containsExactly(null, "1", "2", "3", error) + .inOrder() + + job.cancel() + } +} + +private fun pinRequest(): BiometricPromptRequest.Credential.Pin = + BiometricPromptRequest.Credential.Pin( + promptInfo(), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ) + +private fun verified(hat: ByteArray) = CredentialStatus.Success.Verified(hat) + +private fun throttled(error: String) = CredentialStatus.Fail.Throttled(error) + +private fun error(error: String? = null, remaining: Int? = null, urgentMessage: String? = null) = + CredentialStatus.Fail.Error(error, remaining, urgentMessage) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt new file mode 100644 index 000000000000..2eeff9fcdd8a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -0,0 +1,92 @@ +package com.android.systemui.biometrics.domain.model + +import androidx.test.filters.SmallTest +import com.android.systemui.biometrics.promptInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +private const val USER_ID = 2 +private const val OPERATION_ID = 8L + +@SmallTest +@RunWith(JUnit4::class) +class BiometricPromptRequestTest { + + @Test + fun biometricRequestFromPromptInfo() { + val title = "what" + val subtitle = "a" + val description = "request" + + val request = + BiometricPromptRequest.Biometric( + promptInfo(title = title, subtitle = subtitle, description = description), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ) + + assertThat(request.title).isEqualTo(title) + assertThat(request.subtitle).isEqualTo(subtitle) + assertThat(request.description).isEqualTo(description) + assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) + assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) + } + + @Test + fun credentialRequestFromPromptInfo() { + val title = "what" + val subtitle = "a" + val description = "request" + val stealth = true + + val toCheck = + listOf( + BiometricPromptRequest.Credential.Pin( + promptInfo( + title = title, + subtitle = subtitle, + description = description, + credentialTitle = null, + credentialSubtitle = null, + credentialDescription = null + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ), + BiometricPromptRequest.Credential.Password( + promptInfo( + credentialTitle = title, + credentialSubtitle = subtitle, + credentialDescription = description + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID) + ), + BiometricPromptRequest.Credential.Pattern( + promptInfo( + subtitle = subtitle, + description = description, + credentialTitle = title, + credentialSubtitle = null, + credentialDescription = null + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + stealth + ) + ) + + for (request in toCheck) { + assertThat(request.title).isEqualTo(title) + assertThat(request.subtitle).isEqualTo(subtitle) + assertThat(request.description).isEqualTo(description) + assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) + assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) + if (request is BiometricPromptRequest.Credential.Pattern) { + assertThat(request.stealthMode).isEqualTo(stealth) + } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt new file mode 100644 index 000000000000..d73cdfc4249f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModelTest.kt @@ -0,0 +1,181 @@ +package com.android.systemui.biometrics.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.model.PromptKind +import com.android.systemui.biometrics.data.repository.FakePromptRepository +import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor +import com.android.systemui.biometrics.domain.interactor.CredentialStatus +import com.android.systemui.biometrics.domain.interactor.FakeCredentialInteractor +import com.android.systemui.biometrics.promptInfo +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +private const val USER_ID = 9 +private const val OPERATION_ID = 10L + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class CredentialViewModelTest : SysuiTestCase() { + + private val dispatcher = UnconfinedTestDispatcher() + private val promptRepository = FakePromptRepository() + private val credentialInteractor = FakeCredentialInteractor() + + private lateinit var viewModel: CredentialViewModel + + @Before + fun setup() { + viewModel = + CredentialViewModel( + mContext, + BiometricPromptCredentialInteractor( + dispatcher, + promptRepository, + credentialInteractor + ) + ) + } + + @Test fun setsPinInputFlags() = setsInputFlags(PromptKind.PIN, expectFlags = true) + @Test fun setsPasswordInputFlags() = setsInputFlags(PromptKind.PASSWORD, expectFlags = false) + @Test fun setsPatternInputFlags() = setsInputFlags(PromptKind.PATTERN, expectFlags = false) + + private fun setsInputFlags(type: PromptKind, expectFlags: Boolean) = + runTestWithKind(type) { + var flags: Int? = null + val job = launch { viewModel.inputFlags.collect { flags = it } } + + if (expectFlags) { + assertThat(flags).isNotNull() + } else { + assertThat(flags).isNull() + } + job.cancel() + } + + @Test fun isStealthIgnoredByPin() = isStealthMode(PromptKind.PIN, expectStealth = false) + @Test + fun isStealthIgnoredByPassword() = isStealthMode(PromptKind.PASSWORD, expectStealth = false) + @Test fun isStealthUsedByPattern() = isStealthMode(PromptKind.PATTERN, expectStealth = true) + + private fun isStealthMode(type: PromptKind, expectStealth: Boolean) = + runTestWithKind(type, init = { credentialInteractor.stealthMode = true }) { + var stealth: Boolean? = null + val job = launch { viewModel.stealthMode.collect { stealth = it } } + + assertThat(stealth).isEqualTo(expectStealth) + + job.cancel() + } + + @Test + fun animatesContents() = runTestWithKind { + val expected = arrayOf(true, false, true) + val animate = mutableListOf<Boolean>() + val job = launch { viewModel.animateContents.toList(animate) } + + for (value in expected) { + viewModel.setAnimateContents(value) + viewModel.setAnimateContents(value) + } + assertThat(animate).containsExactly(*expected).inOrder() + + job.cancel() + } + + @Test + fun showAndClearErrors() = runTestWithKind { + var error = "" + val job = launch { viewModel.errorMessage.collect { error = it } } + assertThat(error).isEmpty() + + viewModel.showPatternTooShortError() + assertThat(error).isNotEmpty() + + viewModel.resetErrorMessage() + assertThat(error).isEmpty() + + job.cancel() + } + + @Test + fun checkCredential() = runTestWithKind { + val hat = ByteArray(2) + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(CredentialStatus.Success.Verified(hat)) + } + + val attestations = mutableListOf<ByteArray?>() + val remainingAttempts = mutableListOf<RemainingAttempts?>() + var header: HeaderViewModel? = null + val job = launch { + launch { viewModel.validatedAttestation.toList(attestations) } + launch { viewModel.remainingAttempts.toList(remainingAttempts) } + launch { viewModel.header.collect { header = it } } + } + assertThat(header).isNotNull() + + viewModel.checkCredential("p", header!!) + + val attestation = attestations.removeLastOrNull() + assertThat(attestation).isSameInstanceAs(hat) + assertThat(attestations).isEmpty() + assertThat(remainingAttempts).containsExactly(RemainingAttempts()) + + job.cancel() + } + + @Test + fun checkCredentialWhenBad() = runTestWithKind { + val remaining = 2 + val urgentError = "wow" + credentialInteractor.verifyCredentialResponse = { _ -> + flowOf(CredentialStatus.Fail.Error("error", remaining, urgentError)) + } + + val attestations = mutableListOf<ByteArray?>() + val remainingAttempts = mutableListOf<RemainingAttempts?>() + var header: HeaderViewModel? = null + val job = launch { + launch { viewModel.validatedAttestation.toList(attestations) } + launch { viewModel.remainingAttempts.toList(remainingAttempts) } + launch { viewModel.header.collect { header = it } } + } + assertThat(header).isNotNull() + + viewModel.checkCredential("1111", header!!) + + assertThat(attestations).containsExactly(null) + + val attemptInfo = remainingAttempts.removeLastOrNull() + assertThat(attemptInfo).isNotNull() + assertThat(attemptInfo!!.remaining).isEqualTo(remaining) + assertThat(attemptInfo.message).isEqualTo(urgentError) + assertThat(remainingAttempts).containsExactly(RemainingAttempts()) // initial value + + job.cancel() + } + + private fun runTestWithKind( + kind: PromptKind = PromptKind.PIN, + init: () -> Unit = {}, + block: suspend TestScope.() -> Unit, + ) = + runTest(dispatcher) { + init() + promptRepository.setPrompt(promptInfo(), USER_ID, OPERATION_ID, kind) + block() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java index e099c9269d3f..ea16cb567028 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java @@ -20,6 +20,7 @@ import static com.android.systemui.dreams.complication.Complication.COMPLICATION import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS; +import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME; import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER; @@ -63,6 +64,8 @@ public class ComplicationUtilsTest extends SysuiTestCase { .isEqualTo(COMPLICATION_TYPE_HOME_CONTROLS); assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_SMARTSPACE)) .isEqualTo(COMPLICATION_TYPE_SMARTSPACE); + assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY)) + .isEqualTo(COMPLICATION_TYPE_MEDIA_ENTRY); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java index 50f27ea27ae9..0295030da510 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java @@ -16,8 +16,11 @@ package com.android.systemui.dreams.complication; +import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY; import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -32,6 +35,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.dreams.DreamOverlayStateController; +import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.controls.ui.MediaCarouselController; import com.android.systemui.media.dream.MediaDreamComplication; @@ -51,6 +55,9 @@ import org.mockito.MockitoAnnotations; @TestableLooper.RunWithLooper public class DreamMediaEntryComplicationTest extends SysuiTestCase { @Mock + private DreamMediaEntryComplicationComponent.Factory mComponentFactory; + + @Mock private View mView; @Mock @@ -89,6 +96,14 @@ public class DreamMediaEntryComplicationTest extends SysuiTestCase { when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false); } + @Test + public void testGetRequiredTypeAvailability() { + final DreamMediaEntryComplication complication = + new DreamMediaEntryComplication(mComponentFactory); + assertThat(complication.getRequiredTypeAvailability()).isEqualTo( + COMPLICATION_TYPE_MEDIA_ENTRY); + } + /** * Ensures clicking media entry chip adds/removes media complication. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java deleted file mode 100644 index 27a5190367a8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard; - -import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.keyguard.LockIconView.ICON_LOCK; -import static com.android.keyguard.LockIconView.ICON_UNLOCK; - -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Point; -import android.graphics.Rect; -import android.graphics.drawable.AnimatedStateListDrawable; -import android.hardware.biometrics.BiometricSourceType; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.util.Pair; -import android.view.View; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityManager; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.keyguard.KeyguardViewController; -import com.android.keyguard.LockIconView; -import com.android.keyguard.LockIconViewController; -import com.android.systemui.R; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.AuthController; -import com.android.systemui.biometrics.AuthRippleController; -import com.android.systemui.doze.util.BurnInHelperKt; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.StatusBarState; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.concurrency.FakeExecutor; -import com.android.systemui.util.time.FakeSystemClock; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.MockitoSession; -import org.mockito.quality.Strictness; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper -public class LockIconViewControllerTest extends SysuiTestCase { - private static final String UNLOCKED_LABEL = "unlocked"; - private static final int PADDING = 10; - - private MockitoSession mStaticMockSession; - - private @Mock LockIconView mLockIconView; - private @Mock AnimatedStateListDrawable mIconDrawable; - private @Mock Context mContext; - private @Mock Resources mResources; - private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager; - private @Mock StatusBarStateController mStatusBarStateController; - private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor; - private @Mock KeyguardViewController mKeyguardViewController; - private @Mock KeyguardStateController mKeyguardStateController; - private @Mock FalsingManager mFalsingManager; - private @Mock AuthController mAuthController; - private @Mock DumpManager mDumpManager; - private @Mock AccessibilityManager mAccessibilityManager; - private @Mock ConfigurationController mConfigurationController; - private @Mock VibratorHelper mVibrator; - private @Mock AuthRippleController mAuthRippleController; - private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock()); - - private LockIconViewController mLockIconViewController; - - // Capture listeners so that they can be used to send events - @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - private View.OnAttachStateChangeListener mAttachListener; - - @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor = - ArgumentCaptor.forClass(KeyguardStateController.Callback.class); - private KeyguardStateController.Callback mKeyguardStateCallback; - - @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor = - ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); - private StatusBarStateController.StateListener mStatusBarStateListener; - - @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor; - private AuthController.Callback mAuthControllerCallback; - - @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback> - mKeyguardUpdateMonitorCallbackCaptor = - ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class); - private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback; - - @Captor private ArgumentCaptor<Point> mPointCaptor; - - @Before - public void setUp() throws Exception { - mStaticMockSession = mockitoSession() - .mockStatic(BurnInHelperKt.class) - .strictness(Strictness.LENIENT) - .startMocking(); - MockitoAnnotations.initMocks(this); - - setupLockIconViewMocks(); - when(mContext.getResources()).thenReturn(mResources); - when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager); - Rect windowBounds = new Rect(0, 0, 800, 1200); - when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds); - when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL); - when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable); - when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING); - when(mAuthController.getScaleFactor()).thenReturn(1f); - - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - - mLockIconViewController = new LockIconViewController( - mLockIconView, - mStatusBarStateController, - mKeyguardUpdateMonitor, - mKeyguardViewController, - mKeyguardStateController, - mFalsingManager, - mAuthController, - mDumpManager, - mAccessibilityManager, - mConfigurationController, - mDelayableExecutor, - mVibrator, - mAuthRippleController, - mResources - ); - } - - @After - public void tearDown() { - mStaticMockSession.finishMocking(); - } - - @Test - public void testUpdateFingerprintLocationOnInit() { - // GIVEN fp sensor location is available pre-attached - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated to the udfps location with UDFPS radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdatePaddingBasedOnResolutionScale() { - // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5 - Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location - when(mAuthController.getScaleFactor()).thenReturn(5f); - - // WHEN lock icon view controller is initialized and attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN lock icon view location is updated with the scaled radius - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING * 5)); - } - - @Test - public void testUpdateLockIconLocationOnAuthenticatorsRegistered() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN all authenticators are registered - mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testUpdateLockIconLocationOnUdfpsLocationChanged() { - // GIVEN fp sensor location is not available pre-init - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - when(mAuthController.getFingerprintSensorLocation()).thenReturn(null); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - resetLockIconView(); // reset any method call counts for when we verify method calls later - - // GIVEN fp sensor location is available post-attached - captureAuthControllerCallback(); - Pair<Float, Point> udfps = setupUdfps(); - - // WHEN udfps location changes - mAuthControllerCallback.onUdfpsLocationChanged(); - mDelayableExecutor.runAllReady(); - - // THEN lock icon view location is updated with the same coordinates as auth controller vals - verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first), - eq(PADDING)); - } - - @Test - public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() { - // GIVEN Udpfs sensor location is available - setupUdfps(); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be enabled - verify(mLockIconView).setUseBackground(true); - } - - @Test - public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() { - // GIVEN Udfps sensor location is not supported - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false); - - mLockIconViewController.init(); - captureAttachListener(); - - // WHEN the view is attached - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon view background should be disabled - verify(mLockIconView).setUseBackground(false); - } - - @Test - public void testUnlockIconShows_biometricUnlockedTrue() { - // GIVEN UDFPS sensor location is available - setupUdfps(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardUpdateMonitorCallback(); - - // GIVEN user has unlocked with a biometric auth (ie: face auth) - when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true); - reset(mLockIconView); - - // WHEN face auth's biometric running state changes - mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false, - BiometricSourceType.FACE); - - // THEN the unlock icon is shown - verify(mLockIconView).setContentDescription(UNLOCKED_LABEL); - } - - @Test - public void testLockIconStartState() { - // GIVEN lock icon state - setupShowLockIcon(); - - // WHEN lock icon controller is initialized - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - - // THEN the lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, false); - } - - @Test - public void testLockIcon_updateToUnlock() { - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureKeyguardStateCallback(); - reset(mLockIconView); - - // WHEN the unlocked state changes to canDismissLockScreen=true - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true); - mKeyguardStateCallback.onUnlockedChanged(); - - // THEN the unlock should show - verify(mLockIconView).updateIcon(ICON_UNLOCK, false); - } - - @Test - public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() { - // GIVEN udfps not enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the icon is cleared - verify(mLockIconView).clearIcon(); - } - - @Test - public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN starting state for the lock icon - setupShowLockIcon(); - - // GIVEN lock icon controller is initialized and view is attached - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN the dozing state changes - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - - // THEN the AOD lock icon should show - verify(mLockIconView).updateIcon(ICON_LOCK, true); - } - - @Test - public void testBurnInOffsetsUpdated_onDozeAmountChanged() { - // GIVEN udfps enrolled - setupUdfps(); - when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true); - - // GIVEN burn-in offset = 5 - int burnInOffset = 5; - when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset); - - // GIVEN starting state for the lock icon (keyguard) - setupShowLockIcon(); - mLockIconViewController.init(); - captureAttachListener(); - mAttachListener.onViewAttachedToWindow(mLockIconView); - captureStatusBarStateListener(); - reset(mLockIconView); - - // WHEN dozing updates - mStatusBarStateListener.onDozingChanged(true /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(1f, 1f); - - // THEN the view's translation is updated to use the AoD burn-in offsets - verify(mLockIconView).setTranslationY(burnInOffset); - verify(mLockIconView).setTranslationX(burnInOffset); - reset(mLockIconView); - - // WHEN the device is no longer dozing - mStatusBarStateListener.onDozingChanged(false /* isDozing */); - mStatusBarStateListener.onDozeAmountChanged(0f, 0f); - - // THEN the view is updated to NO translation (no burn-in offsets anymore) - verify(mLockIconView).setTranslationY(0); - verify(mLockIconView).setTranslationX(0); - - } - private Pair<Float, Point> setupUdfps() { - when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true); - final Point udfpsLocation = new Point(50, 75); - final float radius = 33f; - when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation); - when(mAuthController.getUdfpsRadius()).thenReturn(radius); - - return new Pair(radius, udfpsLocation); - } - - private void setupShowLockIcon() { - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false); - when(mStatusBarStateController.isDozing()).thenReturn(false); - when(mStatusBarStateController.getDozeAmount()).thenReturn(0f); - when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD); - when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); - } - - private void captureAuthControllerCallback() { - verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture()); - mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue(); - } - - private void captureAttachListener() { - verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture()); - mAttachListener = mAttachCaptor.getValue(); - } - - private void captureKeyguardStateCallback() { - verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture()); - mKeyguardStateCallback = mKeyguardStateCaptor.getValue(); - } - - private void captureStatusBarStateListener() { - verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture()); - mStatusBarStateListener = mStatusBarStateCaptor.getValue(); - } - - private void captureKeyguardUpdateMonitorCallback() { - verify(mKeyguardUpdateMonitor).registerCallback( - mKeyguardUpdateMonitorCallbackCaptor.capture()); - mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue(); - } - - private void setupLockIconViewMocks() { - when(mLockIconView.getResources()).thenReturn(mResources); - when(mLockIconView.getContext()).thenReturn(mContext); - } - - private void resetLockIconView() { - reset(mLockIconView); - setupLockIconViewMocks(); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index e99c139e9e7e..ce110084dbc4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -15,10 +15,10 @@ * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import com.android.systemui.animation.Expandable -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.yield @@ -29,7 +29,9 @@ import kotlinx.coroutines.yield * This class is abstract to force tests to provide extensions of it as the system that references * these configs uses each implementation's class type to refer to them. */ -abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig { +abstract class FakeKeyguardQuickAffordanceConfig( + override val key: String, +) : KeyguardQuickAffordanceConfig { var onClickedResult: OnClickedResult = OnClickedResult.Handled diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt index 9a91ea91f3a2..b120303d4c04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import androidx.test.filters.SmallTest import com.android.systemui.R diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt index a809f0547ee6..ce8d36d5012a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import androidx.test.filters.SmallTest import com.android.systemui.R @@ -23,7 +23,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import java.util.Optional diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 329c4db0a75c..93464400d1ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -1,26 +1,26 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.content.Intent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult import com.android.systemui.qrcodescanner.controller.QRCodeScannerController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index 98dc4c4f6f76..ae9e3c7a6f04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -1,21 +1,21 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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 + * 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 + * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. * */ -package com.android.systemui.keyguard.domain.quickaffordance +package com.android.systemui.keyguard.data.quickaffordance import android.graphics.drawable.Drawable import android.service.quickaccesswallet.GetWalletCardsResponse diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index b4d5464d1177..114cf19d0837 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -25,11 +25,12 @@ import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.KeyguardStateController @@ -211,7 +212,11 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(expandable.activityLaunchController()).thenReturn(animationController) - homeControls = object : FakeKeyguardQuickAffordanceConfig() {} + homeControls = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + ) {} underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()), @@ -224,8 +229,14 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { ), KeyguardQuickAffordancePosition.BOTTOM_END to listOf( - object : FakeKeyguardQuickAffordanceConfig() {}, - object : FakeKeyguardQuickAffordanceConfig() {}, + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) {}, + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + ) {}, ), ), ), @@ -260,7 +271,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { } underTest.onQuickAffordanceClicked( - configKey = homeControls::class, + configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, expandable = expandable, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 65fd6e576650..1a1ee8aca099 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -22,12 +22,13 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker @@ -69,9 +70,21 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { repository = FakeKeyguardRepository() repository.setKeyguardShowing(true) - homeControls = object : FakeKeyguardQuickAffordanceConfig() {} - quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {} - qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {} + homeControls = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + ) {} + quickAccessWallet = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) {} + qrCodeScanner = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + ) {} underTest = KeyguardQuickAffordanceInteractor( @@ -99,7 +112,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Test fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest { - val configKey = homeControls::class + val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS homeControls.setState( KeyguardQuickAffordanceConfig.State.Visible( icon = ICON, @@ -130,7 +143,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Test fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest { - val configKey = quickAccessWallet::class + val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET quickAccessWallet.setState( KeyguardQuickAffordanceConfig.State.Visible( icon = ICON, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt index e68c43f4abd7..13e2768e1fd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt @@ -17,8 +17,8 @@ package com.android.systemui.keyguard.domain.quickaffordance -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import kotlin.reflect.KClass +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition /** Fake implementation of [FakeKeyguardQuickAffordanceRegistry], for tests. */ class FakeKeyguardQuickAffordanceRegistry( @@ -33,11 +33,8 @@ class FakeKeyguardQuickAffordanceRegistry( } override fun get( - configClass: KClass<out FakeKeyguardQuickAffordanceConfig> + key: String, ): FakeKeyguardQuickAffordanceConfig { - return configsByPosition.values - .flatten() - .associateBy { config -> config::class } - .getValue(configClass) + return configsByPosition.values.flatten().associateBy { config -> config.key }.getValue(key) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index d674c89c0e14..f9be067362d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -23,14 +23,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys +import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor -import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordancePosition -import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceConfig import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry -import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceConfig +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordanceToggleState import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker @@ -40,7 +41,6 @@ import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min -import kotlin.reflect.KClass import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runBlockingTest @@ -81,9 +81,21 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(burnInHelperWrapper.burnInOffset(anyInt(), any())) .thenReturn(RETURNED_BURN_IN_OFFSET) - homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {} - quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {} - qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {} + homeControlsQuickAffordanceConfig = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + ) {} + quickAccessWalletAffordanceConfig = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + ) {} + qrCodeScannerAffordanceConfig = + object : + FakeKeyguardQuickAffordanceConfig( + BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER + ) {} registry = FakeKeyguardQuickAffordanceRegistry( mapOf( @@ -489,7 +501,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { private suspend fun setUpQuickAffordanceModel( position: KeyguardQuickAffordancePosition, testConfig: TestConfig, - ): KClass<out FakeKeyguardQuickAffordanceConfig> { + ): String { val config = when (position) { KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig @@ -518,13 +530,13 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { KeyguardQuickAffordanceConfig.State.Hidden } config.setState(state) - return config::class + return config.key } private fun assertQuickAffordanceViewModel( viewModel: KeyguardQuickAffordanceViewModel?, testConfig: TestConfig, - configKey: KClass<out FakeKeyguardQuickAffordanceConfig>, + configKey: String, ) { checkNotNull(viewModel) assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible) diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt new file mode 100644 index 000000000000..f20c6a29b840 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.notetask + +import android.app.KeyguardManager +import android.content.Context +import android.content.Intent +import android.os.UserManager +import android.test.suitebuilder.annotation.SmallTest +import android.view.KeyEvent +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.floating.FloatingTasks +import java.util.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskController]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskControllerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskControllerTest : SysuiTestCase() { + + private val notesIntent = Intent(NOTES_ACTION) + + @Mock lateinit var context: Context + @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver + @Mock lateinit var floatingTasks: FloatingTasks + @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks> + @Mock lateinit var keyguardManager: KeyguardManager + @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager> + @Mock lateinit var optionalUserManager: Optional<UserManager> + @Mock lateinit var userManager: UserManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent) + whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks) + whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager) + whenever(optionalUserManager.orElse(null)).thenReturn(userManager) + whenever(userManager.isUserUnlocked).thenReturn(true) + } + + private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController { + return NoteTaskController( + context = context, + intentResolver = noteTaskIntentResolver, + optionalFloatingTasks = optionalFloatingTasks, + optionalKeyguardManager = optionalKeyguardManager, + optionalUserManager = optionalUserManager, + isEnabled = isEnabled, + ) + } + + @Test + fun handleSystemKey_keyguardIsLocked_shouldStartActivity() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(true) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_keyguardIsUnlocked_shouldStartFloatingTask() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(false) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(floatingTasks).showOrSetStashed(notesIntent) + verify(context, never()).startActivity(notesIntent) + } + + @Test + fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() { + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_floatingTasksIsNull_shouldDoNothing() { + whenever(optionalFloatingTasks.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() { + whenever(optionalKeyguardManager.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_userManagerIsNull_shouldDoNothing() { + whenever(optionalUserManager.orElse(null)).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() { + whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_flagDisabled_shouldDoNothing() { + createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } + + @Test + fun handleSystemKey_userIsLocked_shouldDoNothing() { + whenever(userManager.isUserUnlocked).thenReturn(false) + + createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1) + + verify(context, never()).startActivity(notesIntent) + verify(floatingTasks, never()).showOrSetStashed(notesIntent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt new file mode 100644 index 000000000000..f344c8d9eec4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.notetask + +import android.test.suitebuilder.annotation.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.floating.FloatingTasks +import java.util.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskController]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskInitializerTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskInitializerTest : SysuiTestCase() { + + @Mock lateinit var commandQueue: CommandQueue + @Mock lateinit var floatingTasks: FloatingTasks + @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks> + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(optionalFloatingTasks.isPresent).thenReturn(true) + whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks) + } + + private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer { + return NoteTaskInitializer( + optionalFloatingTasks = optionalFloatingTasks, + lazyNoteTaskController = mock(), + commandQueue = commandQueue, + isEnabled = isEnabled, + ) + } + + @Test + fun initialize_shouldAddCallbacks() { + createNoteTaskInitializer().initialize() + + verify(commandQueue).addCallback(any()) + } + + @Test + fun initialize_flagDisabled_shouldDoNothing() { + createNoteTaskInitializer(isEnabled = false).initialize() + + verify(commandQueue, never()).addCallback(any()) + } + + @Test + fun initialize_floatingTasksNotPresent_shouldDoNothing() { + whenever(optionalFloatingTasks.isPresent).thenReturn(false) + + createNoteTaskInitializer().initialize() + + verify(commandQueue, never()).addCallback(any()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt new file mode 100644 index 000000000000..dd2cc2ffc9db --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.notetask + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.test.suitebuilder.annotation.SmallTest +import androidx.test.runner.AndroidJUnit4 +import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.MockitoAnnotations + +/** + * Tests for [NoteTaskIntentResolver]. + * + * Build/Install/Run: + * - atest SystemUITests:NoteTaskIntentResolverTest + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +internal class NoteTaskIntentResolverTest : SysuiTestCase() { + + @Mock lateinit var packageManager: PackageManager + + private lateinit var resolver: NoteTaskIntentResolver + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + resolver = NoteTaskIntentResolver(packageManager) + } + + private fun createResolveInfo( + packageName: String = "PackageName", + activityInfo: ActivityInfo? = null, + ): ResolveInfo { + return ResolveInfo().apply { + serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo().apply { this.packageName = packageName } + } + this.activityInfo = activityInfo + } + } + + private fun createActivityInfo( + name: String? = "ActivityName", + exported: Boolean = true, + enabled: Boolean = true, + showWhenLocked: Boolean = true, + turnScreenOn: Boolean = true, + ): ActivityInfo { + return ActivityInfo().apply { + this.name = name + this.exported = exported + this.enabled = enabled + if (showWhenLocked) { + flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED + } + if (turnScreenOn) { + flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON + } + } + } + + private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) { + whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>())) + .thenReturn(block()) + } + + private fun givenResolveActivity(block: () -> ResolveInfo?) { + whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block()) + } + + @Test + fun resolveIntent_shouldReturnNotesIntent() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) } + + val actual = resolver.resolveIntent() + + val expected = + Intent(NOTES_ACTION) + .setComponent(ComponentName("PackageName", "ActivityName")) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // Compares the string representation of both intents, as they are different instances. + assertThat(actual.toString()).isEqualTo(expected.toString()) + } + + @Test + fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(enabled = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(exported = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { + createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false)) + } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityInfoIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { createResolveInfo(activityInfo = null) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_resolveActivityIsNull_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo()) } + givenResolveActivity { null } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_packageNameIsBlank_shouldReturnNull() { + givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } + + @Test + fun resolveIntent_activityNotFoundForAction_shouldReturnNull() { + givenQueryIntentActivities { emptyList() } + + val actual = resolver.resolveIntent() + + assertThat(actual).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 91aecd8cf753..dceb4ff48125 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -78,6 +78,7 @@ import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController import org.junit.Assert; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -89,6 +90,7 @@ import org.mockito.junit.MockitoRule; /** * Tests for {@link NotificationStackScrollLayout}. */ +@Ignore("b/255552856") @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt index e18dd3a3c846..7d5f06c890c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt @@ -140,6 +140,40 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { } @Test + fun testOnUnfold_hingeAngleDecreasesBeforeInnerScreenAvailable_emitsOnlyStartAndInnerScreenAvailableEvents() { + setFoldState(folded = true) + foldUpdates.clear() + + setFoldState(folded = false) + screenOnStatusProvider.notifyScreenTurningOn() + sendHingeAngleEvent(10) + sendHingeAngleEvent(20) + sendHingeAngleEvent(10) + screenOnStatusProvider.notifyScreenTurnedOn() + + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING, + FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) + } + + @Test + fun testOnUnfold_hingeAngleDecreasesAfterInnerScreenAvailable_emitsStartInnerScreenAvailableAndStartClosingEvents() { + setFoldState(folded = true) + foldUpdates.clear() + + setFoldState(folded = false) + screenOnStatusProvider.notifyScreenTurningOn() + sendHingeAngleEvent(10) + sendHingeAngleEvent(20) + screenOnStatusProvider.notifyScreenTurnedOn() + sendHingeAngleEvent(30) + sendHingeAngleEvent(40) + sendHingeAngleEvent(10) + + assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING, + FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_START_CLOSING) + } + + @Test fun testOnFolded_stopsHingeAngleProvider() { setFoldState(folded = true) @@ -237,7 +271,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { } @Test - fun startClosingEvent_afterTimeout_abortEmitted() { + fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() { sendHingeAngleEvent(90) sendHingeAngleEvent(80) @@ -269,7 +303,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { } @Test - fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() { + fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java index 76bff1d72141..7e8ffeb7f9e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java @@ -54,7 +54,7 @@ import java.util.concurrent.Executor; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class WallpaperColorExtractorTest extends SysuiTestCase { +public class WallpaperLocalColorExtractorTest extends SysuiTestCase { private static final int LOW_BMP_WIDTH = 128; private static final int LOW_BMP_HEIGHT = 128; private static final int HIGH_BMP_WIDTH = 3000; @@ -105,11 +105,11 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { return bitmap; } - private WallpaperColorExtractor getSpyWallpaperColorExtractor() { + private WallpaperLocalColorExtractor getSpyWallpaperLocalColorExtractor() { - WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor( + WallpaperLocalColorExtractor colorExtractor = new WallpaperLocalColorExtractor( mBackgroundExecutor, - new WallpaperColorExtractor.WallpaperColorExtractorCallback() { + new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() { @Override public void onColorsProcessed(List<RectF> regions, List<WallpaperColors> colors) { @@ -132,25 +132,25 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { mDeactivatedCount++; } }); - WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor); + WallpaperLocalColorExtractor spyColorExtractor = spy(colorExtractor); doAnswer(invocation -> { mMiniBitmapWidth = invocation.getArgument(1); mMiniBitmapHeight = invocation.getArgument(2); return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight); - }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); + }).when(spyColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); doAnswer(invocation -> getMockBitmap( invocation.getArgument(1), invocation.getArgument(2))) - .when(spyWallpaperColorExtractor) + .when(spyColorExtractor) .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt()); doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0))) - .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class)); + .when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class)); - return spyWallpaperColorExtractor; + return spyColorExtractor; } private RectF randomArea() { @@ -180,18 +180,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { */ @Test public void testMiniBitmapCreation() { - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH); int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT); Bitmap bitmap = getMockBitmap(width, height); - spyWallpaperColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onBitmapChanged(bitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight)) - .isAtMost(WallpaperColorExtractor.SMALL_SIDE); + .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE); } } @@ -201,18 +201,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { */ @Test public void testSmallMiniBitmapCreation() { - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH); int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT); Bitmap bitmap = getMockBitmap(width, height); - spyWallpaperColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onBitmapChanged(bitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight)) - .isAtMost(WallpaperColorExtractor.SMALL_SIDE); + .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE); } } @@ -228,15 +228,15 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS); int nPages = randomBetween(PAGES_LOW, PAGES_HIGH); List<Runnable> tasks = Arrays.asList( - () -> spyWallpaperColorExtractor.onPageChanged(nPages), - () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap), - () -> spyWallpaperColorExtractor.setDisplayDimensions( + () -> spyColorExtractor.onPageChanged(nPages), + () -> spyColorExtractor.onBitmapChanged(bitmap), + () -> spyColorExtractor.setDisplayDimensions( DISPLAY_WIDTH, DISPLAY_HEIGHT), - () -> spyWallpaperColorExtractor.addLocalColorsAreas( + () -> spyColorExtractor.addLocalColorsAreas( regions)); Collections.shuffle(tasks); tasks.forEach(Runnable::run); @@ -245,7 +245,7 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(mColorsProcessed).isEqualTo(regions.size()); - spyWallpaperColorExtractor.removeLocalColorAreas(regions); + spyColorExtractor.removeLocalColorAreas(regions); assertThat(mDeactivatedCount).isEqualTo(1); } } @@ -260,7 +260,7 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { int nSimulations = 10; for (int i = 0; i < nSimulations; i++) { resetCounters(); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions = new ArrayList<>(); @@ -268,20 +268,20 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { regions.addAll(regions2); int nPages = randomBetween(PAGES_LOW, PAGES_HIGH); List<Runnable> tasks = Arrays.asList( - () -> spyWallpaperColorExtractor.onPageChanged(nPages), - () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap), - () -> spyWallpaperColorExtractor.setDisplayDimensions( + () -> spyColorExtractor.onPageChanged(nPages), + () -> spyColorExtractor.onBitmapChanged(bitmap), + () -> spyColorExtractor.setDisplayDimensions( DISPLAY_WIDTH, DISPLAY_HEIGHT), - () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1)); + () -> spyColorExtractor.removeLocalColorAreas(regions1)); - spyWallpaperColorExtractor.addLocalColorsAreas(regions); + spyColorExtractor.addLocalColorsAreas(regions); assertThat(mActivatedCount).isEqualTo(1); Collections.shuffle(tasks); tasks.forEach(Runnable::run); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); assertThat(mDeactivatedCount).isEqualTo(0); - spyWallpaperColorExtractor.removeLocalColorAreas(regions2); + spyColorExtractor.removeLocalColorAreas(regions2); assertThat(mDeactivatedCount).isEqualTo(1); } } @@ -295,18 +295,18 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { @Test public void testRecomputeColorExtraction() { Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2); List<RectF> regions = new ArrayList<>(); regions.addAll(regions1); regions.addAll(regions2); - spyWallpaperColorExtractor.addLocalColorsAreas(regions); + spyColorExtractor.addLocalColorsAreas(regions); assertThat(mActivatedCount).isEqualTo(1); int nPages = PAGES_LOW; - spyWallpaperColorExtractor.onBitmapChanged(bitmap); - spyWallpaperColorExtractor.onPageChanged(nPages); - spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT); + spyColorExtractor.onBitmapChanged(bitmap); + spyColorExtractor.onPageChanged(nPages); + spyColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT); int nSimulations = 20; for (int i = 0; i < nSimulations; i++) { @@ -315,22 +315,22 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { // verify that if we remove some regions, they are not recomputed after other changes if (i == nSimulations / 2) { regions.removeAll(regions2); - spyWallpaperColorExtractor.removeLocalColorAreas(regions2); + spyColorExtractor.removeLocalColorAreas(regions2); } if (Math.random() >= 0.5) { int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH); if (nPagesNew == nPages) continue; nPages = nPagesNew; - spyWallpaperColorExtractor.onPageChanged(nPagesNew); + spyColorExtractor.onPageChanged(nPagesNew); } else { Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); - spyWallpaperColorExtractor.onBitmapChanged(newBitmap); + spyColorExtractor.onBitmapChanged(newBitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); } assertThat(mColorsProcessed).isEqualTo(regions.size()); } - spyWallpaperColorExtractor.removeLocalColorAreas(regions); + spyColorExtractor.removeLocalColorAreas(regions); assertThat(mDeactivatedCount).isEqualTo(1); } @@ -339,12 +339,12 @@ public class WallpaperColorExtractorTest extends SysuiTestCase { resetCounters(); Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT); doNothing().when(bitmap).recycle(); - WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor(); - spyWallpaperColorExtractor.onPageChanged(PAGES_LOW); - spyWallpaperColorExtractor.onBitmapChanged(bitmap); + WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor(); + spyColorExtractor.onPageChanged(PAGES_LOW); + spyColorExtractor.onBitmapChanged(bitmap); assertThat(mMiniBitmapUpdatedCount).isEqualTo(1); - spyWallpaperColorExtractor.cleanUp(); - spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS)); + spyColorExtractor.cleanUp(); + spyColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS)); assertThat(mColorsProcessed).isEqualTo(0); } } 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 7af66f641837..7ae47b41d5ae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java @@ -28,6 +28,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.model.SysUiState; +import com.android.systemui.notetask.NoteTaskInitializer; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -36,7 +37,6 @@ import com.android.systemui.tracing.ProtoTracer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.floating.FloatingTasks; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedEventCallback; import com.android.wm.shell.onehanded.OneHandedTransitionCallback; @@ -78,18 +78,31 @@ public class WMShellTest extends SysuiTestCase { @Mock ProtoTracer mProtoTracer; @Mock UserTracker mUserTracker; @Mock ShellExecutor mSysUiMainExecutor; - @Mock FloatingTasks mFloatingTasks; + @Mock NoteTaskInitializer mNoteTaskInitializer; @Mock DesktopMode mDesktopMode; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip), - Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks), + mWMShell = new WMShell( + mContext, + mShellInterface, + Optional.of(mPip), + Optional.of(mSplitScreen), + Optional.of(mOneHanded), Optional.of(mDesktopMode), - mCommandQueue, mConfigurationController, mKeyguardStateController, - mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer, - mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor); + mCommandQueue, + mConfigurationController, + mKeyguardStateController, + mKeyguardUpdateMonitor, + mScreenLifecycle, + mSysUiState, + mProtoTracer, + mWakefulnessLifecycle, + mUserTracker, + mNoteTaskInitializer, + mSysUiMainExecutor + ); } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt new file mode 100644 index 000000000000..96658c61109d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -0,0 +1,48 @@ +package com.android.systemui.biometrics.data.repository + +import android.hardware.biometrics.PromptInfo +import com.android.systemui.biometrics.data.model.PromptKind +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Fake implementation of [PromptRepository] for tests. */ +class FakePromptRepository : PromptRepository { + + private val _isShowing = MutableStateFlow(false) + override val isShowing = _isShowing.asStateFlow() + + private val _promptInfo = MutableStateFlow<PromptInfo?>(null) + override val promptInfo = _promptInfo.asStateFlow() + + private val _userId = MutableStateFlow<Int?>(null) + override val userId = _userId.asStateFlow() + + private var _challenge = MutableStateFlow<Long?>(null) + override val challenge = _challenge.asStateFlow() + + private val _kind = MutableStateFlow(PromptKind.ANY_BIOMETRIC) + override val kind = _kind.asStateFlow() + + override fun setPrompt( + promptInfo: PromptInfo, + userId: Int, + gatekeeperChallenge: Long?, + kind: PromptKind + ) { + _promptInfo.value = promptInfo + _userId.value = userId + _challenge.value = gatekeeperChallenge + _kind.value = kind + } + + override fun unsetPrompt() { + _promptInfo.value = null + _userId.value = null + _challenge.value = null + _kind.value = PromptKind.ANY_BIOMETRIC + } + + fun setIsShowing(showing: Boolean) { + _isShowing.value = showing + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt new file mode 100644 index 000000000000..fbe291ebaf5d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/FakeCredentialInteractor.kt @@ -0,0 +1,31 @@ +package com.android.systemui.biometrics.domain.interactor + +import com.android.internal.widget.LockscreenCredential +import com.android.systemui.biometrics.domain.model.BiometricPromptRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** Fake implementation of [CredentialInteractor] for tests. */ +class FakeCredentialInteractor : CredentialInteractor { + + /** Sets return value for [isStealthModeActive]. */ + var stealthMode: Boolean = false + + /** Sets return value for [getCredentialOwnerOrSelfId]. */ + var credentialOwnerId: Int? = null + + override fun isStealthModeActive(userId: Int): Boolean = stealthMode + + override fun getCredentialOwnerOrSelfId(userId: Int): Int = credentialOwnerId ?: userId + + override fun verifyCredential( + request: BiometricPromptRequest.Credential, + credential: LockscreenCredential, + ): Flow<CredentialStatus> = verifyCredentialResponse(credential) + + /** Sets the result value for [verifyCredential]. */ + var verifyCredentialResponse: (credential: LockscreenCredential) -> Flow<CredentialStatus> = + { _ -> + flowOf(CredentialStatus.Fail.Error("invalid")) + } +} diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt index 043aff659d6c..b56818693124 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.unfold.progress +import android.os.Trace import android.util.Log import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -117,6 +118,7 @@ class PhysicsBasedUnfoldTransitionProgressProvider( if (DEBUG) { Log.d(TAG, "onFoldUpdate = $update") + Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update) } } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 07473b30dd58..808128d16b7e 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -16,6 +16,7 @@ package com.android.systemui.unfold.updates import android.os.Handler +import android.os.Trace import android.util.Log import androidx.annotation.FloatRange import androidx.annotation.VisibleForTesting @@ -108,6 +109,7 @@ constructor( private fun onHingeAngle(angle: Float) { if (DEBUG) { Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle") + Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt()) } val isClosing = angle < lastHingeAngle @@ -115,8 +117,16 @@ constructor( val closingThresholdMet = closingThreshold == null || angle < closingThreshold val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING - - if (isClosing && closingThresholdMet && !closingEventDispatched && !isFullyOpened) { + val screenAvailableEventSent = isUnfoldHandled + + if (isClosing // hinge angle should be decreasing since last update + && closingThresholdMet // hinge angle is below certain threshold + && !closingEventDispatched // we haven't sent closing event already + && !isFullyOpened // do not send closing event if we are in fully opened hinge + // angle range as closing threshold could overlap this range + && screenAvailableEventSent // do not send closing event if we are still in + // the process of turning on the inner display + ) { notifyFoldUpdate(FOLD_UPDATE_START_CLOSING) } diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 1df382fbd76f..f35de17088d1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -176,6 +176,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private boolean mSendMotionEvents; + private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0); boolean mRequestFilterKeyEvents; boolean mRetrieveInteractiveWindows; @@ -2369,9 +2370,17 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) { + mServiceDetectsGestures.put(displayId, mode); mSystemSupport.setServiceDetectsGesturesEnabled(displayId, mode); } + public boolean isServiceDetectsGesturesEnabled(int displayId) { + if (mServiceDetectsGestures.contains(displayId)) { + return mServiceDetectsGestures.get(displayId); + } + return false; + } + public void requestTouchExploration(int displayId) { mSystemSupport.requestTouchExploration(displayId); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java index 75724bffabf8..d80117d8d8ba 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java @@ -176,6 +176,8 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo private int mEnabledFeatures; + // Display-specific features + private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(); private final SparseArray<EventStreamState> mMouseStreamStates = new SparseArray<>(0); private final SparseArray<EventStreamState> mTouchScreenStreamStates = new SparseArray<>(0); @@ -458,7 +460,9 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo final Context displayContext = mContext.createDisplayContext(display); final int displayId = display.getDisplayId(); - + if (!mServiceDetectsGestures.contains(displayId)) { + mServiceDetectsGestures.put(displayId, false); + } if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) { if (mAutoclickController == null) { mAutoclickController = new AutoclickController( @@ -481,6 +485,7 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if ((mEnabledFeatures & FLAG_SEND_MOTION_EVENTS) != 0) { explorer.setSendMotionEventsEnabled(true); } + explorer.setServiceDetectsGestures(mServiceDetectsGestures.get(displayId)); addFirstEventHandler(displayId, explorer); mTouchExplorer.put(displayId, explorer); } @@ -897,6 +902,11 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo if (mTouchExplorer.contains(displayId)) { mTouchExplorer.get(displayId).setServiceDetectsGestures(mode); } + mServiceDetectsGestures.put(displayId, mode); + } + + public void resetServiceDetectsGestures() { + mServiceDetectsGestures.clear(); } public void requestTouchExploration(int displayId) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 085a58909b6d..47b415630de8 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -1695,31 +1695,34 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } private boolean scheduleNotifyMotionEvent(MotionEvent event) { + boolean result = false; + int displayId = event.getDisplayId(); synchronized (mLock) { AccessibilityUserState state = getCurrentUserStateLocked(); for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { AccessibilityServiceConnection service = state.mBoundServices.get(i); - if (service.mRequestTouchExplorationMode) { + if (service.isServiceDetectsGesturesEnabled(displayId)) { service.notifyMotionEvent(event); - return true; + result = true; } } } - return false; + return result; } private boolean scheduleNotifyTouchState(int displayId, int touchState) { + boolean result = false; synchronized (mLock) { AccessibilityUserState state = getCurrentUserStateLocked(); for (int i = state.mBoundServices.size() - 1; i >= 0; i--) { AccessibilityServiceConnection service = state.mBoundServices.get(i); - if (service.mRequestTouchExplorationMode) { + if (service.isServiceDetectsGesturesEnabled(displayId)) { service.notifyTouchState(displayId, touchState); - return true; + result = true; } } } - return false; + return result; } private void notifyClearAccessibilityCacheLocked() { @@ -2292,8 +2295,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (!mHasInputFilter) { mHasInputFilter = true; if (mInputFilter == null) { - mInputFilter = new AccessibilityInputFilter(mContext, - AccessibilityManagerService.this); + mInputFilter = + new AccessibilityInputFilter( + mContext, AccessibilityManagerService.this); } inputFilter = mInputFilter; setInputFilter = true; @@ -2303,6 +2307,17 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (mHasInputFilter) { mHasInputFilter = false; mInputFilter.setUserAndEnabledFeatures(userState.mUserId, 0); + mInputFilter.resetServiceDetectsGestures(); + if (userState.isTouchExplorationEnabledLocked()) { + // Service gesture detection is turned on and off on a per-display + // basis. + final ArrayList<Display> displays = getValidDisplayList(); + for (Display display : displays) { + int displayId = display.getDisplayId(); + boolean mode = userState.isServiceDetectsGesturesEnabled(displayId); + mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode); + } + } inputFilter = null; setInputFilter = true; } @@ -2618,6 +2633,18 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Binder.restoreCallingIdentity(identity); } } + // Service gesture detection is turned on and off on a per-display + // basis. + userState.resetServiceDetectsGestures(); + final ArrayList<Display> displays = getValidDisplayList(); + for (AccessibilityServiceConnection service: userState.mBoundServices) { + for (Display display : displays) { + int displayId = display.getDisplayId(); + if (service.isServiceDetectsGesturesEnabled(displayId)) { + userState.setServiceDetectsGesturesEnabled(displayId, true); + } + } + } userState.setServiceHandlesDoubleTapLocked(serviceHandlesDoubleTapEnabled); userState.setMultiFingerGesturesLocked(requestMultiFingerGestures); userState.setTwoFingerPassthroughLocked(requestTwoFingerPassthrough); @@ -4342,6 +4369,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void setServiceDetectsGesturesInternal(int displayId, boolean mode) { synchronized (mLock) { + getCurrentUserStateLocked().setServiceDetectsGesturesEnabled(displayId, mode); if (mHasInputFilter && mInputFilter != null) { mInputFilter.setServiceDetectsGesturesEnabled(displayId, mode); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 0cb7209c187a..0db169fd76c3 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -44,6 +44,7 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseIntArray; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; @@ -118,6 +119,7 @@ class AccessibilityUserState { private boolean mRequestMultiFingerGestures; private boolean mRequestTwoFingerPassthrough; private boolean mSendMotionEventsEnabled; + private SparseArray<Boolean> mServiceDetectsGestures = new SparseArray<>(0); private int mUserInteractiveUiTimeout; private int mUserNonInteractiveUiTimeout; private int mNonInteractiveUiTimeout = 0; @@ -991,4 +993,19 @@ class AccessibilityUserState { mFocusStrokeWidth = strokeWidth; mFocusColor = color; } + + public void setServiceDetectsGesturesEnabled(int displayId, boolean mode) { + mServiceDetectsGestures.put(displayId, mode); + } + + public void resetServiceDetectsGestures() { + mServiceDetectsGestures.clear(); + } + + public boolean isServiceDetectsGesturesEnabled(int displayId) { + if (mServiceDetectsGestures.contains(displayId)) { + return mServiceDetectsGestures.get(displayId); + } + return false; + } } diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java index 933d2596aed8..e40f001f27d5 100644 --- a/services/core/java/com/android/server/SystemService.java +++ b/services/core/java/com/android/server/SystemService.java @@ -473,6 +473,18 @@ public abstract class SystemService { } /** + * The {@link UserManager#isUserVisible() user visibility} changed. + * + * <p>This callback is called before the user starts or is switched to (or after it stops), when + * its visibility changed because of that action. + * + * @hide + */ + // NOTE: change visible to int if this method becomes a @SystemApi + public void onUserVisibilityChanged(@NonNull TargetUser user, boolean visible) { + } + + /** * Called when an existing user is stopping, for system services to finalize any per-user * state they maintain for running users. This is called prior to sending the SHUTDOWN * broadcast to the user; it is a good place to stop making use of any resources of that diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index 1a8cf0b07cb6..83d86cdc05c6 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -75,13 +75,17 @@ public final class SystemServiceManager implements Dumpable { // Constants used on onUser(...) // NOTE: do not change their values, as they're used on Trace calls and changes might break // performance tests that rely on them. - private static final String USER_STARTING = "Start"; // Logged as onStartUser - private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUnlockingUser - private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUnlockedUser - private static final String USER_SWITCHING = "Switch"; // Logged as onSwitchUser - private static final String USER_STOPPING = "Stop"; // Logged as onStopUser - private static final String USER_STOPPED = "Cleanup"; // Logged as onCleanupUser - private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onCompletedEventUser + private static final String USER_STARTING = "Start"; // Logged as onUserStarting() + private static final String USER_UNLOCKING = "Unlocking"; // Logged as onUserUnlocking() + private static final String USER_UNLOCKED = "Unlocked"; // Logged as onUserUnlocked() + private static final String USER_SWITCHING = "Switch"; // Logged as onUserSwitching() + private static final String USER_STOPPING = "Stop"; // Logged as onUserStopping() + private static final String USER_STOPPED = "Cleanup"; // Logged as onUserStopped() + private static final String USER_COMPLETED_EVENT = "CompletedEvent"; // onUserCompletedEvent() + private static final String USER_VISIBLE = "Visible"; // Logged on onUserVisible() and + // onUserStarting() (when visible is true) + private static final String USER_INVISIBLE = "Invisible"; // Logged on onUserStopping() + // (when visibilityChanged is true) // The default number of threads to use if lifecycle thread pool is enabled. private static final int DEFAULT_MAX_USER_POOL_THREADS = 3; @@ -350,18 +354,41 @@ public final class SystemServiceManager implements Dumpable { /** * Starts the given user. */ - public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId) { - EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId); + public void onUserStarting(@NonNull TimingsTraceAndSlog t, @UserIdInt int userId, + boolean visible) { + EventLog.writeEvent(EventLogTags.SSM_USER_STARTING, userId, visible ? 1 : 0); final TargetUser targetUser = newTargetUser(userId); synchronized (mTargetUsers) { mTargetUsers.put(userId, targetUser); } + if (visible) { + // Must send the user visiiblity change first, for 2 reasons: + // 1. Automotive need to update the user-zone mapping ASAP and it's one of the few + // services listening to this event (OTOH, there are manyy listeners to USER_STARTING + // and some can take a while to process it) + // 2. When a user is switched from bg to fg, the onUserVisibilityChanged() callback is + // called onUserSwitching(), so calling it before onUserStarting() make it more + // consistent with that + onUser(t, USER_VISIBLE, /* prevUser= */ null, targetUser); + } onUser(t, USER_STARTING, /* prevUser= */ null, targetUser); } /** + * Updates the user visibility. + * + * <p><b>NOTE: </b>this method should only be called when a user that is already running become + * visible; if the user is starting visible, callers should call + * {@link #onUserStarting(TimingsTraceAndSlog, int, boolean)} instead + */ + public void onUserVisible(@UserIdInt int userId) { + EventLog.writeEvent(EventLogTags.SSM_USER_VISIBLE, userId); + onUser(USER_VISIBLE, userId); + } + + /** * Unlocks the given user. */ public void onUserUnlocking(@UserIdInt int userId) { @@ -408,9 +435,12 @@ public final class SystemServiceManager implements Dumpable { /** * Stops the given user. */ - public void onUserStopping(@UserIdInt int userId) { - EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId); + public void onUserStopping(@UserIdInt int userId, boolean visibilityChanged) { + EventLog.writeEvent(EventLogTags.SSM_USER_STOPPING, userId, visibilityChanged ? 1 : 0); onUser(USER_STOPPING, userId); + if (visibilityChanged) { + onUser(USER_INVISIBLE, userId); + } } /** @@ -456,13 +486,12 @@ public final class SystemServiceManager implements Dumpable { TargetUser targetUser = getTargetUser(userId); Preconditions.checkState(targetUser != null, "No TargetUser for " + userId); - onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, - targetUser); + onUser(TimingsTraceAndSlog.newAsyncLog(), onWhat, /* prevUser= */ null, targetUser); } private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat, @Nullable TargetUser prevUser, @NonNull TargetUser curUser) { - onUser(t, onWhat, prevUser, curUser, /* completedEventType=*/ null); + onUser(t, onWhat, prevUser, curUser, /* completedEventType= */ null); } private void onUser(@NonNull TimingsTraceAndSlog t, @NonNull String onWhat, @@ -534,6 +563,12 @@ public final class SystemServiceManager implements Dumpable { threadPool.submit(getOnUserCompletedEventRunnable( t, service, serviceName, curUser, completedEventType)); break; + case USER_VISIBLE: + service.onUserVisibilityChanged(curUser, /* visible= */ true); + break; + case USER_INVISIBLE: + service.onUserVisibilityChanged(curUser, /* visible= */ false); + break; default: throw new IllegalArgumentException(onWhat + " what?"); } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b166adc9a828..2cf3462554f5 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -3891,24 +3891,29 @@ public class ActivityManagerService extends IActivityManager.Stub finishForceStopPackageLocked(packageName, appInfo.uid); } } - final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED, - Uri.fromParts("package", packageName, null)); - intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND - | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); - intent.putExtra(Intent.EXTRA_UID, (appInfo != null) ? appInfo.uid : -1); - intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId); - final int[] visibilityAllowList = - mPackageManagerInt.getVisibilityAllowList(packageName, resolvedUserId); - if (isInstantApp) { - intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); - broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent, - null, null, null, 0, null, null, permission.ACCESS_INSTANT_APPS, - null, false, false, resolvedUserId, false, null, - visibilityAllowList); - } else { - broadcastIntentInPackage("android", null, SYSTEM_UID, uid, pid, intent, - null, null, null, 0, null, null, null, null, false, false, - resolvedUserId, false, null, visibilityAllowList); + + if (succeeded) { + final Intent intent = new Intent(Intent.ACTION_PACKAGE_DATA_CLEARED, + Uri.fromParts("package", packageName, null /* fragment */)); + intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND + | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + intent.putExtra(Intent.EXTRA_UID, + (appInfo != null) ? appInfo.uid : INVALID_UID); + intent.putExtra(Intent.EXTRA_USER_HANDLE, resolvedUserId); + if (isInstantApp) { + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); + } + final int[] visibilityAllowList = mPackageManagerInt.getVisibilityAllowList( + packageName, resolvedUserId); + + broadcastIntentInPackage("android", null /* featureId */, + SYSTEM_UID, uid, pid, intent, null /* resolvedType */, + null /* resultToApp */, null /* resultTo */, 0 /* resultCode */, + null /* resultData */, null /* resultExtras */, + isInstantApp ? permission.ACCESS_INSTANT_APPS : null, + null /* bOptions */, false /* serialized */, false /* sticky */, + resolvedUserId, false /* allowBackgroundActivityStarts */, + null /* backgroundActivityStartsToken */, visibilityAllowList); } if (observer != null) { @@ -8346,14 +8351,14 @@ public class ActivityManagerService extends IActivityManager.Stub mBatteryStatsService.noteEvent(BatteryStats.HistoryItem.EVENT_USER_FOREGROUND_START, Integer.toString(currentUserId), currentUserId); - // On Automotive, at this point the system user has already been started and unlocked, - // and some of the tasks we do here have already been done. So skip those in that case. - // TODO(b/132262830, b/203885241): this workdound shouldn't be necessary once we move the - // headless-user start logic to UserManager-land + // On Automotive / Headless System User Mode, at this point the system user has already been + // started and unlocked, and some of the tasks we do here have already been done. So skip + // those in that case. + // TODO(b/242195409): this workaround shouldn't be necessary once we move the headless-user + // start logic to UserManager-land final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM; - if (bootingSystemUser) { - mSystemServiceManager.onUserStarting(t, currentUserId); + mUserController.onSystemUserStarting(); } synchronized (this) { diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index 4590c859a909..417a0e5ede83 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -133,7 +133,7 @@ public class BroadcastConstants { */ public boolean MODERN_QUEUE_ENABLED = DEFAULT_MODERN_QUEUE_ENABLED; private static final String KEY_MODERN_QUEUE_ENABLED = "modern_queue_enabled"; - private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = false; + private static final boolean DEFAULT_MODERN_QUEUE_ENABLED = true; /** * For {@link BroadcastQueueModernImpl}: Maximum number of process queues to diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 5123517e272d..f7d24e9b8b4e 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -85,9 +85,16 @@ class BroadcastProcessQueue { @Nullable ProcessRecord app; /** - * Track name to use for {@link Trace} events. + * Track name to use for {@link Trace} events, defined as part of upgrading + * into a running slot. */ - @Nullable String traceTrackName; + @Nullable String runningTraceTrackName; + + /** + * Flag indicating if this process should be OOM adjusted, defined as part + * of upgrading into a running slot. + */ + boolean runningOomAdjusted; /** * Snapshotted value of {@link ProcessRecord#getCpuDelayTime()}, typically @@ -141,7 +148,8 @@ class BroadcastProcessQueue { private boolean mActiveViaColdStart; /** - * Count of {@link #mPending} broadcasts of these various flavors. + * Count of {@link #mPending} and {@link #mPendingUrgent} broadcasts of + * these various flavors. */ private int mCountForeground; private int mCountOrdered; @@ -150,6 +158,7 @@ class BroadcastProcessQueue { private int mCountInteractive; private int mCountResultTo; private int mCountInstrumented; + private int mCountManifest; private @UptimeMillisLong long mRunnableAt = Long.MAX_VALUE; private @Reason int mRunnableAtReason = REASON_EMPTY; @@ -206,7 +215,7 @@ class BroadcastProcessQueue { // with implicit responsiveness expectations. final ArrayDeque<SomeArgs> queue = record.isUrgent() ? mPendingUrgent : mPending; queue.addLast(newBroadcastArgs); - onBroadcastEnqueued(record); + onBroadcastEnqueued(record, recordIndex); } /** @@ -224,7 +233,8 @@ class BroadcastProcessQueue { while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord testRecord = (BroadcastRecord) args.arg1; - final Object testReceiver = testRecord.receivers.get(args.argi1); + final int testRecordIndex = args.argi1; + final Object testReceiver = testRecord.receivers.get(testRecordIndex); if ((record.callingUid == testRecord.callingUid) && (record.userId == testRecord.userId) && record.intent.filterEquals(testRecord.intent) @@ -233,8 +243,8 @@ class BroadcastProcessQueue { args.arg1 = record; args.argi1 = recordIndex; args.argi2 = blockedUntilTerminalCount; - onBroadcastDequeued(testRecord); - onBroadcastEnqueued(record); + onBroadcastDequeued(testRecord, testRecordIndex); + onBroadcastEnqueued(record, recordIndex); return true; } } @@ -284,13 +294,13 @@ class BroadcastProcessQueue { while (it.hasNext()) { final SomeArgs args = it.next(); final BroadcastRecord record = (BroadcastRecord) args.arg1; - final int index = args.argi1; - if (predicate.test(record, index)) { - consumer.accept(record, index); + final int recordIndex = args.argi1; + if (predicate.test(record, recordIndex)) { + consumer.accept(record, recordIndex); if (andRemove) { args.recycle(); it.remove(); - onBroadcastDequeued(record); + onBroadcastDequeued(record, recordIndex); } didSomething = true; } @@ -339,7 +349,7 @@ class BroadcastProcessQueue { * Return if we know of an actively running "warm" process for this queue. */ public boolean isProcessWarm() { - return (app != null) && (app.getThread() != null) && !app.isKilled(); + return (app != null) && (app.getOnewayThread() != null) && !app.isKilled(); } public int getPreferredSchedulingGroupLocked() { @@ -385,7 +395,7 @@ class BroadcastProcessQueue { mActiveCountSinceIdle++; mActiveViaColdStart = false; next.recycle(); - onBroadcastDequeued(mActive); + onBroadcastDequeued(mActive, mActiveIndex); } /** @@ -403,7 +413,7 @@ class BroadcastProcessQueue { /** * Update summary statistics when the given record has been enqueued. */ - private void onBroadcastEnqueued(@NonNull BroadcastRecord record) { + private void onBroadcastEnqueued(@NonNull BroadcastRecord record, int recordIndex) { if (record.isForeground()) { mCountForeground++; } @@ -425,13 +435,16 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented++; } + if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + mCountManifest++; + } invalidateRunnableAt(); } /** * Update summary statistics when the given record has been dequeued. */ - private void onBroadcastDequeued(@NonNull BroadcastRecord record) { + private void onBroadcastDequeued(@NonNull BroadcastRecord record, int recordIndex) { if (record.isForeground()) { mCountForeground--; } @@ -453,34 +466,37 @@ class BroadcastProcessQueue { if (record.callerInstrumented) { mCountInstrumented--; } + if (record.receivers.get(recordIndex) instanceof ResolveInfo) { + mCountManifest--; + } invalidateRunnableAt(); } public void traceProcessStartingBegin() { Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, toShortString() + " starting", hashCode()); + runningTraceTrackName, toShortString() + " starting", hashCode()); } public void traceProcessRunningBegin() { Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, toShortString() + " running", hashCode()); + runningTraceTrackName, toShortString() + " running", hashCode()); } public void traceProcessEnd() { Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, hashCode()); + runningTraceTrackName, hashCode()); } public void traceActiveBegin() { final int cookie = mActive.receivers.get(mActiveIndex).hashCode(); Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, mActive.toShortString() + " scheduled", cookie); + runningTraceTrackName, mActive.toShortString() + " scheduled", cookie); } public void traceActiveEnd() { final int cookie = mActive.receivers.get(mActiveIndex).hashCode(); Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, - traceTrackName, cookie); + runningTraceTrackName, cookie); } /** @@ -540,6 +556,14 @@ class BroadcastProcessQueue { } /** + * Quickly determine if this queue has broadcasts waiting to be delivered to + * manifest receivers, which indicates we should request an OOM adjust. + */ + public boolean isPendingManifest() { + return mCountManifest > 0; + } + + /** * Quickly determine if this queue has broadcasts that are still waiting to * be delivered at some point in the future. */ @@ -807,7 +831,7 @@ class BroadcastProcessQueue { @NeverCompile public void dumpLocked(@UptimeMillisLong long now, @NonNull IndentingPrintWriter pw) { - if ((mActive == null) && mPending.isEmpty()) return; + if ((mActive == null) && isEmpty()) return; pw.print(toShortString()); if (isRunnable()) { @@ -823,6 +847,10 @@ class BroadcastProcessQueue { if (mActive != null) { dumpRecord(now, pw, mActive, mActiveIndex, mActiveBlockedUntilTerminalCount); } + for (SomeArgs args : mPendingUrgent) { + final BroadcastRecord r = (BroadcastRecord) args.arg1; + dumpRecord(now, pw, r, args.argi1, args.argi2); + } for (SomeArgs args : mPending) { final BroadcastRecord r = (BroadcastRecord) args.arg1; dumpRecord(now, pw, r, args.argi1, args.argi2); diff --git a/services/core/java/com/android/server/am/BroadcastQueue.md b/services/core/java/com/android/server/am/BroadcastQueue.md new file mode 100644 index 000000000000..81317932ef9b --- /dev/null +++ b/services/core/java/com/android/server/am/BroadcastQueue.md @@ -0,0 +1,98 @@ +# Broadcast Queue Design + +Broadcast intents are one of the major building blocks of the Android platform, +generally intended for asynchronous notification of events. There are three +flavors of intents that can be broadcast: + +* **Normal** broadcast intents are dispatched to relevant receivers. +* **Ordered** broadcast intents are dispatched in a specific order to +receivers, where each receiver has the opportunity to influence the final +"result" of a broadcast, including aborting delivery to any remaining receivers. +* **Sticky** broadcast intents are dispatched to relevant receivers, and are +then retained internally for immediate dispatch to any future receivers. (This +capability has been deprecated and its use is discouraged due to its system +health impact.) + +And there are there two ways to receive these intents: + +* Registered receivers (via `Context.registerReceiver()` methods) are +dynamically requested by a running app to receive intents. These requests are +only maintained while the process is running, and are discarded at process +death. +* Manifest receivers (via the `<receiver>` tag in `AndroidManifest.xml`) are +statically requested by an app to receive intents. These requests are delivered +regardless of process running state, and have the ability to cold-start a +process that isn't currently running. + +## Per-process queues + +The design of `BroadcastQueueModernImpl` is centered around maintaining a +separate `BroadcastProcessQueue` instance for each potential process on the +device. At this level, a process refers to the `android:process` attributes +defined in `AndroidManifest.xml` files, which means it can be defined and +populated regardless of the process state. (For example, a given +`android:process` can have multiple `ProcessRecord`/PIDs defined as it's +launched, killed, and relaunched over long periods of time.) + +Each per-process queue has the concept of a _runnable at_ timestamp when it's +next eligible for execution, and that value can be influenced by a wide range +of policies, such as: + +* Which broadcasts are pending dispatch to a given process. For example, an +"urgent" broadcast typically results in an earlier _runnable at_ time, or a +"delayed" broadcast typically results in a later _runnable at_ time. +* Current state of the process or UID. For example, a "cached" process +typically results in a later _runnable at_ time, or an "instrumented" process +typically results in an earlier _runnable at_ time. +* Blocked waiting for an earlier receiver to complete. For example, an +"ordered" or "prioritized" broadcast typically results in a _not currently +runnable_ value. + +Each per-process queue represents a single remote `ApplicationThread`, and we +only dispatch a single broadcast at a time to each process to ensure developers +see consistent ordering of broadcast events. The flexible _runnable at_ +policies above mean that no inter-process ordering guarantees are provided, +except for those explicitly provided by "ordered" or "prioritized" broadcasts. + +## Parallel dispatch + +Given a collection of per-process queues with valid _runnable at_ timestamps, +BroadcastQueueModernImpl is then willing to promote those _runnable_ queues +into a _running_ state. We choose the next per-process queue to promote based +on the sorted ordering of the _runnable at_ timestamps, selecting the +longest-waiting process first, which aims to reduce overall broadcast dispatch +latency. + +To preserve system health, at most +`BroadcastConstants.MAX_RUNNING_PROCESS_QUEUES` processes are allowed to be in +the _running_ state at any given time, and at most one process is allowed to be +_cold started_ at any given time. (For background, _cold starting_ a process +by forking and specializing the zygote is a relatively heavy operation, so +limiting ourselves to a single pending _cold start_ reduces system-wide +resource contention.) + +After each broadcast is dispatched to a given process, we consider dispatching +any additional pending broadcasts to that process, aimed at batching dispatch +to better amortize the cost of OOM adjustments. + +## Starvation considerations + +Careful attention is given to several types of potential resource starvation, +along with the mechanisms of mitigation: + +* A per-process queue that has a delayed _runnable at_ policy applied can risk +growing very large. This is mitigated by +`BroadcastConstants.MAX_PENDING_BROADCASTS` bypassing any delays when the queue +grows too large. +* A per-process queue that has a large number of pending broadcasts can risk +monopolizing one of the limited _runnable_ slots. This is mitigated by +`BroadcastConstants.MAX_RUNNING_ACTIVE_BROADCASTS` being used to temporarily +"retire" a running process to give other processes a chance to run. +* An "urgent" broadcast dispatched to a process with a large backlog of +"non-urgent" broadcasts can risk large dispatch latencies. This is mitigated +by maintaining a separate `mPendingUrgent` queue of urgent events, which we +prefer to dispatch before the normal `mPending` queue. +* A process with a scheduled broadcast desires to execute, but heavy CPU +contention can risk the process not receiving enough resources before an ANR +timeout is triggered. This is mitigated by extending the "soft" ANR timeout by +up to double the original timeout length. diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 4c831bd47ee4..9e9eb71db4e5 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -58,6 +58,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.os.BundleMerger; import android.os.Handler; import android.os.Message; import android.os.Process; @@ -364,6 +365,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue { BroadcastProcessQueue nextQueue = queue.runnableAtNext; final long runnableAt = queue.getRunnableAt(); + // When broadcasts are skipped or failed during list traversal, we + // might encounter a queue that is no longer runnable; skip it + if (!queue.isRunnable()) { + queue = nextQueue; + continue; + } + // If queues beyond this point aren't ready to run yet, schedule // another pass when they'll be runnable if (runnableAt > now && !waitingFor) { @@ -401,7 +409,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mRunnableHead = removeFromRunnableList(mRunnableHead, queue); // Emit all trace events for this process into a consistent track - queue.traceTrackName = TAG + ".mRunning[" + queueIndex + "]"; + queue.runningTraceTrackName = TAG + ".mRunning[" + queueIndex + "]"; + queue.runningOomAdjusted = queue.isPendingManifest(); // If we're already warm, schedule next pending broadcast now; // otherwise we'll wait for the cold start to circle back around @@ -415,9 +424,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { scheduleReceiverColdLocked(queue); } - // We've moved at least one process into running state above, so we - // need to kick off an OOM adjustment pass - updateOomAdj = true; + // Only kick off an OOM adjustment pass if needed + updateOomAdj |= queue.runningOomAdjusted; // Move to considering next runnable queue queue = nextQueue; @@ -543,16 +551,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { }, mBroadcastConsumerSkipAndCanceled, true); } - final int policy = (r.options != null) - ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; - if (policy == BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) { - forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { - // We only allow caller to remove broadcasts they enqueued - return (r.callingUid == testRecord.callingUid) - && (r.userId == testRecord.userId) - && r.matchesDeliveryGroup(testRecord); - }, mBroadcastConsumerSkipAndCanceled, true); - } + applyDeliveryGroupPolicy(r); if (r.isReplacePending()) { // Leave the skipped broadcasts intact in queue, so that we can @@ -609,6 +608,41 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } } + private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) { + final int policy = (r.options != null) + ? r.options.getDeliveryGroupPolicy() : BroadcastOptions.DELIVERY_GROUP_POLICY_ALL; + final BroadcastConsumer broadcastConsumer; + switch (policy) { + case BroadcastOptions.DELIVERY_GROUP_POLICY_ALL: + // Older broadcasts need to be left as is in this case, so nothing more to do. + return; + case BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT: + broadcastConsumer = mBroadcastConsumerSkipAndCanceled; + break; + case BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED: + final BundleMerger extrasMerger = r.options.getDeliveryGroupExtrasMerger(); + if (extrasMerger == null) { + // Extras merger is required to be able to merge the extras. So, if it's not + // supplied, then ignore the delivery group policy. + return; + } + broadcastConsumer = (record, recordIndex) -> { + r.intent.mergeExtras(record.intent, extrasMerger); + mBroadcastConsumerSkipAndCanceled.accept(record, recordIndex); + }; + break; + default: + logw("Unknown delivery group policy: " + policy); + return; + } + forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { + // We only allow caller to remove broadcasts they enqueued + return (r.callingUid == testRecord.callingUid) + && (r.userId == testRecord.userId) + && r.matchesDeliveryGroup(testRecord); + }, broadcastConsumer, true); + } + /** * Schedule the currently active broadcast on the given queue when we know * the process is cold. This kicks off a cold start and will eventually call @@ -736,7 +770,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app); setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED); - final IApplicationThread thread = app.getThread(); + final IApplicationThread thread = app.getOnewayThread(); if (thread != null) { try { if (receiver instanceof BroadcastFilter) { @@ -777,7 +811,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private void scheduleResultTo(@NonNull BroadcastRecord r) { if ((r.resultToApp == null) || (r.resultTo == null)) return; final ProcessRecord app = r.resultToApp; - final IApplicationThread thread = app.getThread(); + final IApplicationThread thread = app.getOnewayThread(); if (thread != null) { mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily( app, OOM_ADJ_REASON_FINISH_RECEIVER); @@ -1245,8 +1279,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (queue.app != null) { queue.app.mReceivers.incrementCurReceivers(); - queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER); - // Don't bump its LRU position if it's in the background restricted. if (mService.mInternal.getRestrictionLevel( queue.uid) < ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET) { @@ -1256,7 +1288,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(queue.app, OOM_ADJ_REASON_START_RECEIVER); - mService.enqueueOomAdjTargetLocked(queue.app); + if (queue.runningOomAdjusted) { + queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER); + mService.enqueueOomAdjTargetLocked(queue.app); + } } } @@ -1266,10 +1301,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { */ private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) { if (queue.app != null) { - // Update during our next pass; no need for an immediate update - mService.enqueueOomAdjTargetLocked(queue.app); - queue.app.mReceivers.decrementCurReceivers(); + + if (queue.runningOomAdjusted) { + mService.enqueueOomAdjTargetLocked(queue.app); + } } } diff --git a/services/core/java/com/android/server/am/EventLogTags.logtags b/services/core/java/com/android/server/am/EventLogTags.logtags index d080036733a5..dec8b62de2ec 100644 --- a/services/core/java/com/android/server/am/EventLogTags.logtags +++ b/services/core/java/com/android/server/am/EventLogTags.logtags @@ -101,7 +101,7 @@ option java_package com.android.server.am 30073 uc_finish_user_stopping (userId|1|5) 30074 uc_finish_user_stopped (userId|1|5) 30075 uc_switch_user (userId|1|5) -30076 uc_start_user_internal (userId|1|5) +30076 uc_start_user_internal (userId|1|5),(foreground|1),(displayId|1|5) 30077 uc_unlock_user (userId|1|5) 30078 uc_finish_user_boot (userId|1|5) 30079 uc_dispatch_user_switch (oldUserId|1|5),(newUserId|1|5) @@ -109,13 +109,14 @@ option java_package com.android.server.am 30081 uc_send_user_broadcast (userId|1|5),(IntentAction|3) # Tags below are used by SystemServiceManager - although it's technically part of am, these are # also user switch events and useful to be analyzed together with events above. -30082 ssm_user_starting (userId|1|5) +30082 ssm_user_starting (userId|1|5),(visible|1) 30083 ssm_user_switching (oldUserId|1|5),(newUserId|1|5) 30084 ssm_user_unlocking (userId|1|5) 30085 ssm_user_unlocked (userId|1|5) -30086 ssm_user_stopping (userId|1|5) +30086 ssm_user_stopping (userId|1|5),(visibilityChanged|1) 30087 ssm_user_stopped (userId|1|5) 30088 ssm_user_completed_event (userId|1|5),(eventFlag|1|5) +30089 ssm_user_visible (userId|1|5) # Foreground service start/stop events. 30100 am_foreground_service_start (User|1|5),(Component Name|3),(allowWhileInUse|1),(startReasonCode|3),(targetSdk|1|1),(callerTargetSdk|1|1),(notificationWasDeferred|1),(notificationShown|1),(durationMs|1|3),(startForegroundCount|1|1),(stopReason|3) diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 3b04dbb1da98..0a8c6400a6fd 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -54,6 +54,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.procstats.ProcessState; import com.android.internal.app.procstats.ProcessStats; import com.android.internal.os.Zygote; +import com.android.server.FgThread; import com.android.server.wm.WindowProcessController; import com.android.server.wm.WindowProcessListener; @@ -143,6 +144,13 @@ class ProcessRecord implements WindowProcessListener { private IApplicationThread mThread; /** + * Instance of {@link #mThread} that will always meet the {@code oneway} + * contract, possibly by using {@link SameProcessApplicationThread}. + */ + @CompositeRWLock({"mService", "mProcLock"}) + private IApplicationThread mOnewayThread; + + /** * Always keep this application running? */ private volatile boolean mPersistent; @@ -603,16 +611,27 @@ class ProcessRecord implements WindowProcessListener { return mThread; } + @GuardedBy(anyOf = {"mService", "mProcLock"}) + IApplicationThread getOnewayThread() { + return mOnewayThread; + } + @GuardedBy({"mService", "mProcLock"}) public void makeActive(IApplicationThread thread, ProcessStatsService tracker) { mProfile.onProcessActive(thread, tracker); mThread = thread; + if (mPid == Process.myPid()) { + mOnewayThread = new SameProcessApplicationThread(thread, FgThread.getHandler()); + } else { + mOnewayThread = thread; + } mWindowProcessController.setThread(thread); } @GuardedBy({"mService", "mProcLock"}) public void makeInactive(ProcessStatsService tracker) { mThread = null; + mOnewayThread = null; mWindowProcessController.setThread(null); mProfile.onProcessInactive(tracker); } diff --git a/services/core/java/com/android/server/am/SameProcessApplicationThread.java b/services/core/java/com/android/server/am/SameProcessApplicationThread.java new file mode 100644 index 000000000000..a3c011188539 --- /dev/null +++ b/services/core/java/com/android/server/am/SameProcessApplicationThread.java @@ -0,0 +1,73 @@ +/* + * 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.am; + +import android.annotation.NonNull; +import android.app.IApplicationThread; +import android.content.IIntentReceiver; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.res.CompatibilityInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; + +import java.util.Objects; + +/** + * Wrapper around an {@link IApplicationThread} that delegates selected calls + * through a {@link Handler} so they meet the {@code oneway} contract of + * returning immediately after dispatch. + */ +public class SameProcessApplicationThread extends IApplicationThread.Default { + private final IApplicationThread mWrapped; + private final Handler mHandler; + + public SameProcessApplicationThread(@NonNull IApplicationThread wrapped, + @NonNull Handler handler) { + mWrapped = Objects.requireNonNull(wrapped); + mHandler = Objects.requireNonNull(handler); + } + + @Override + public void scheduleReceiver(Intent intent, ActivityInfo info, CompatibilityInfo compatInfo, + int resultCode, String data, Bundle extras, boolean sync, int sendingUser, + int processState) { + mHandler.post(() -> { + try { + mWrapped.scheduleReceiver(intent, info, compatInfo, resultCode, data, extras, sync, + sendingUser, processState); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent, int resultCode, + String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser, + int processState) { + mHandler.post(() -> { + try { + mWrapped.scheduleRegisteredReceiver(receiver, intent, resultCode, data, extras, + ordered, sticky, sendingUser, processState); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 3c26116e8ad2..dcc7a8ea4e44 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -100,6 +100,7 @@ import android.util.EventLog; import android.util.IntArray; import android.util.Pair; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; import android.view.Display; @@ -174,6 +175,9 @@ class UserController implements Handler.Callback { static final int START_USER_SWITCH_FG_MSG = 120; static final int COMPLETE_USER_SWITCH_MSG = 130; static final int USER_COMPLETED_EVENT_MSG = 140; + static final int USER_VISIBLE_MSG = 150; + + private static final int NO_ARG2 = 0; // Message constant to clear {@link UserJourneySession} from {@link mUserIdToUserJourneyMap} if // the user journey, defined in the UserLifecycleJourneyReported atom for statsd, is not @@ -421,6 +425,17 @@ class UserController implements Handler.Callback { /** @see #getLastUserUnlockingUptime */ private volatile long mLastUserUnlockingUptime = 0; + /** + * List of visible users (as defined by {@link UserManager#isUserVisible()}). + * + * <p>It's only used to call {@link SystemServiceManager} when the visibility is changed upon + * the user starting or stopping. + * + * <p>Note: only the key is used, not the value. + */ + @GuardedBy("mLock") + private final SparseBooleanArray mVisibleUsers = new SparseBooleanArray(); + UserController(ActivityManagerService service) { this(new Injector(service)); } @@ -1050,11 +1065,27 @@ class UserController implements Handler.Callback { // instead. userManagerInternal.unassignUserFromDisplay(userId); + final boolean visibilityChanged; + boolean visibleBefore; + synchronized (mLock) { + visibleBefore = mVisibleUsers.get(userId); + if (visibleBefore) { + if (DEBUG_MU) { + Slogf.d(TAG, "Removing %d from mVisibleUsers", userId); + } + mVisibleUsers.delete(userId); + visibilityChanged = true; + } else { + visibilityChanged = false; + } + } + updateStartedUserArrayLU(); final boolean allowDelayedLockingCopied = allowDelayedLocking; Runnable finishUserStoppingAsync = () -> - mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied)); + mHandler.post(() -> finishUserStopping(userId, uss, allowDelayedLockingCopied, + visibilityChanged)); if (mInjector.getUserManager().isPreCreated(userId)) { finishUserStoppingAsync.run(); @@ -1092,7 +1123,7 @@ class UserController implements Handler.Callback { } private void finishUserStopping(final int userId, final UserState uss, - final boolean allowDelayedLocking) { + final boolean allowDelayedLocking, final boolean visibilityChanged) { EventLog.writeEvent(EventLogTags.UC_FINISH_USER_STOPPING, userId); synchronized (mLock) { if (uss.state != UserState.STATE_STOPPING) { @@ -1109,7 +1140,7 @@ class UserController implements Handler.Callback { mInjector.batteryStatsServiceNoteEvent( BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH, Integer.toString(userId), userId); - mInjector.getSystemServiceManager().onUserStopping(userId); + mInjector.getSystemServiceManager().onUserStopping(userId, visibilityChanged); Runnable finishUserStoppedAsync = () -> mHandler.post(() -> finishUserStopped(uss, allowDelayedLocking)); @@ -1513,16 +1544,17 @@ class UserController implements Handler.Callback { private boolean startUserInternal(@UserIdInt int userId, int displayId, boolean foreground, @Nullable IProgressListener unlockListener, @NonNull TimingsTraceAndSlog t) { if (DEBUG_MU) { - Slogf.i(TAG, "Starting user %d on display %d %s", userId, displayId, + Slogf.i(TAG, "Starting user %d on display %d%s", userId, displayId, foreground ? " in foreground" : ""); } - if (displayId != Display.DEFAULT_DISPLAY) { + boolean onSecondaryDisplay = displayId != Display.DEFAULT_DISPLAY; + if (onSecondaryDisplay) { Preconditions.checkArgument(!foreground, "Cannot start user %d in foreground AND " + "on secondary display (%d)", userId, displayId); } - // TODO(b/239982558): log display id (or use a new event) - EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId); + EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId, foreground ? 1 : 0, + displayId); final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); @@ -1571,8 +1603,9 @@ class UserController implements Handler.Callback { return false; } - if (foreground && userInfo.preCreated) { - Slogf.w(TAG, "Cannot start pre-created user #" + userId + " as foreground"); + if ((foreground || onSecondaryDisplay) && userInfo.preCreated) { + Slogf.w(TAG, "Cannot start pre-created user #" + userId + " in foreground or on " + + "secondary display"); return false; } @@ -1656,6 +1689,28 @@ class UserController implements Handler.Callback { } t.traceEnd(); + // Need to call UM when user is on background, as there are some cases where the user + // cannot be started in background on a secondary display (for example, if user is a + // profile). + // TODO(b/253103846): it's also explicitly checking if the user is the USER_SYSTEM, as + // the UM call would return true during boot (when CarService / BootUserInitializer + // calls AM.startUserInBackground() because the system user is still the current user. + // TODO(b/244644281): another fragility of this check is that it must wait to call + // UMI.isUserVisible() until the user state is check, as that method checks if the + // profile of the current user is started. We should fix that dependency so the logic + // belongs to just one place (like UserDisplayAssigner) + boolean visible = foreground + || userId != UserHandle.USER_SYSTEM + && mInjector.getUserManagerInternal().isUserVisible(userId); + if (visible) { + synchronized (mLock) { + if (DEBUG_MU) { + Slogf.d(TAG, "Adding %d to mVisibleUsers", userId); + } + mVisibleUsers.put(userId, true); + } + } + // Make sure user is in the started state. If it is currently // stopping, we need to knock that off. if (uss.state == UserState.STATE_STOPPING) { @@ -1692,8 +1747,15 @@ class UserController implements Handler.Callback { // Booting up a new user, need to tell system services about it. // Note that this is on the same handler as scheduling of broadcasts, // which is important because it needs to go first. - mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, 0)); + mHandler.sendMessage(mHandler.obtainMessage(USER_START_MSG, userId, + visible ? 1 : 0)); t.traceEnd(); + } else if (visible) { + // User was already running and became visible (for example, when switching to a + // user that was started in the background before), so it's necessary to explicitly + // notify the services (while when the user starts from BOOTING, USER_START_MSG + // takes care of that. + mHandler.sendMessage(mHandler.obtainMessage(USER_VISIBLE_MSG, userId, NO_ARG2)); } t.traceBegin("sendMessages"); @@ -2110,6 +2172,11 @@ class UserController implements Handler.Callback { mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG, newUserId, 0)); stopGuestOrEphemeralUserIfBackground(oldUserId); stopUserOnSwitchIfEnforced(oldUserId); + if (oldUserId == UserHandle.USER_SYSTEM) { + // System user is never stopped, but its visibility is changed (as it is brought to the + // background) + updateSystemUserVisibility(/* visible= */ false); + } t.traceEnd(); // end continueUserSwitch } @@ -2413,9 +2480,7 @@ class UserController implements Handler.Callback { void setAllowUserUnlocking(boolean allowed) { mAllowUserUnlocking = allowed; if (DEBUG_MU) { - // TODO(b/245335748): use Slogf.d instead - // Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed); - android.util.Slog.d(TAG, "setAllowUserUnlocking():" + allowed, new Exception()); + Slogf.d(TAG, new Exception(), "setAllowUserUnlocking(%b)", allowed); } } @@ -2457,10 +2522,34 @@ class UserController implements Handler.Callback { } void onSystemReady() { + if (DEBUG_MU) { + Slogf.d(TAG, "onSystemReady()"); + + } updateCurrentProfileIds(); mInjector.reportCurWakefulnessUsageEvent(); } + // TODO(b/242195409): remove this method if initial system user boot logic is refactored? + void onSystemUserStarting() { + updateSystemUserVisibility(/* visible= */ !UserManager.isHeadlessSystemUserMode()); + } + + private void updateSystemUserVisibility(boolean visible) { + if (DEBUG_MU) { + Slogf.d(TAG, "updateSystemUserVisibility(): visible=%b", visible); + } + int userId = UserHandle.USER_SYSTEM; + synchronized (mLock) { + if (visible) { + mVisibleUsers.put(userId, true); + } else { + mVisibleUsers.delete(userId); + } + } + mInjector.onUserStarting(userId, visible); + } + /** * Refreshes the list of users related to the current user when either a * user switch happens or when a new related user is started in the @@ -2846,6 +2935,9 @@ class UserController implements Handler.Callback { proto.end(uToken); } } + for (int i = 0; i < mVisibleUsers.size(); i++) { + proto.write(UserControllerProto.VISIBLE_USERS_ARRAY, mVisibleUsers.keyAt(i)); + } proto.end(token); } } @@ -2899,7 +2991,8 @@ class UserController implements Handler.Callback { if (mSwitchingToSystemUserMessage != null) { pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage); } - pw.println(" mLastUserUnlockingUptime:" + mLastUserUnlockingUptime); + pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime); + pw.println(" mVisibleUsers: " + mVisibleUsers); } } @@ -2936,8 +3029,7 @@ class UserController implements Handler.Callback { logUserLifecycleEvent(msg.arg1, USER_LIFECYCLE_EVENT_START_USER, USER_LIFECYCLE_EVENT_STATE_BEGIN); - mInjector.getSystemServiceManager().onUserStarting( - TimingsTraceAndSlog.newAsyncLog(), msg.arg1); + mInjector.onUserStarting(/* userId= */ msg.arg1, /* visible= */ msg.arg2 == 1); scheduleOnUserCompletedEvent(msg.arg1, UserCompletedEventType.EVENT_TYPE_USER_STARTING, USER_COMPLETED_EVENT_DELAY_MS); @@ -3018,6 +3110,9 @@ class UserController implements Handler.Callback { case COMPLETE_USER_SWITCH_MSG: completeUserSwitch(msg.arg1); break; + case USER_VISIBLE_MSG: + mInjector.getSystemServiceManager().onUserVisible(/* userId= */ msg.arg1); + break; } return false; } @@ -3539,5 +3634,10 @@ class UserController implements Handler.Callback { boolean isUsersOnSecondaryDisplaysEnabled() { return UserManager.isUsersOnSecondaryDisplaysEnabled(); } + + void onUserStarting(int userId, boolean visible) { + getSystemServiceManager().onUserStarting(TimingsTraceAndSlog.newAsyncLog(), userId, + visible); + } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 2761ec04aa7e..7a5b58413014 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -600,8 +600,9 @@ public class FaceService extends SystemService { } try { final SensorProps[] props = face.getSensorProps(); - final FaceProvider provider = new FaceProvider(getContext(), props, instance, - mLockoutResetDispatcher, BiometricContext.getInstance(getContext())); + final FaceProvider provider = new FaceProvider(getContext(), + mBiometricStateCallback, props, instance, mLockoutResetDispatcher, + BiometricContext.getInstance(getContext())); providers.add(provider); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); @@ -612,14 +613,14 @@ public class FaceService extends SystemService { } @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) - @Override // Binder call public void registerAuthenticators( @NonNull List<FaceSensorPropertiesInternal> hidlSensors) { mRegistry.registerAll(() -> { final List<ServiceProvider> providers = new ArrayList<>(); for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) { providers.add( - Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher)); + Face10.newInstance(getContext(), mBiometricStateCallback, + hidlSensor, mLockoutResetDispatcher)); } providers.addAll(getAidlProviders()); return providers; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java index 73c272f7a779..cfbb5dce4c2b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java @@ -16,8 +16,6 @@ package com.android.server.biometrics.sensors.face.aidl; -import static android.Manifest.permission.TEST_BIOMETRIC; - import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.ITestSession; @@ -33,7 +31,6 @@ import android.os.RemoteException; import android.util.Slog; import com.android.server.biometrics.HardwareAuthTokenUtils; -import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.face.FaceUtils; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index c12994c993e6..6488185c727d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -52,9 +52,11 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; import com.android.server.biometrics.sensors.BaseClientMonitor; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.InvalidationRequesterClient; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.PerformanceTracker; @@ -81,6 +83,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { private boolean mTestHalEnabled; @NonNull private final Context mContext; + @NonNull private final BiometricStateCallback mBiometricStateCallback; @NonNull private final String mHalInstanceName; @NonNull @VisibleForTesting final SparseArray<Sensor> mSensors; // Map of sensors that this HAL supports @@ -122,11 +125,14 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { } } - public FaceProvider(@NonNull Context context, @NonNull SensorProps[] props, + public FaceProvider(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, + @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext) { mContext = context; + mBiometricStateCallback = biometricStateCallback; mHalInstanceName = halInstanceName; mSensors = new SparseArray<>(); mHandler = new Handler(Looper.getMainLooper()); @@ -363,16 +369,18 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_ENROLL, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, maxTemplatesPerUser, debugConsent); - scheduleForSensor(sensorId, client, new ClientMonitorCallback() { - @Override - public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, - boolean success) { - if (success) { - scheduleLoadAuthenticatorIdsForUser(sensorId, userId); - scheduleInvalidationRequest(sensorId, userId); - } - } - }); + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( + mBiometricStateCallback, new ClientMonitorCallback() { + @Override + public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, + boolean success) { + ClientMonitorCallback.super.onClientFinished(clientMonitor, success); + if (success) { + scheduleLoadAuthenticatorIdsForUser(sensorId, userId); + scheduleInvalidationRequest(sensorId, userId); + } + } + })); }); return id; } @@ -396,7 +404,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { token, id, callback, userId, opPackageName, sensorId, createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric); - scheduleForSensor(sensorId, client); + scheduleForSensor(sensorId, client, mBiometricStateCallback); }); return id; @@ -424,7 +432,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mBiometricContext, isStrongBiometric, mUsageStats, mSensors.get(sensorId).getLockoutCache(), allowBackgroundAuthentication, isKeyguardBypassEnabled, biometricStrength); - scheduleForSensor(sensorId, client); + scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -479,7 +487,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mSensors.get(sensorId).getAuthenticatorIds()); - scheduleForSensor(sensorId, client); + scheduleForSensor(sensorId, client, mBiometricStateCallback); }); } @@ -568,7 +576,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { if (favorHalEnrollments) { client.setFavorHalEnrollments(); } - scheduleForSensor(sensorId, client, callback); + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback(callback, + mBiometricStateCallback)); }); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java index 14af216a9dc5..7a6a274f8dd7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/BiometricTestSessionImpl.java @@ -16,8 +16,6 @@ package com.android.server.biometrics.sensors.face.hidl; -import static android.Manifest.permission.TEST_BIOMETRIC; - import android.annotation.NonNull; import android.content.Context; import android.hardware.biometrics.ITestSession; @@ -30,7 +28,6 @@ import android.os.Binder; import android.os.RemoteException; import android.util.Slog; -import com.android.server.biometrics.Utils; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.face.FaceUtils; @@ -53,6 +50,7 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { @NonNull private final Set<Integer> mEnrollmentIds; @NonNull private final Random mRandom; + private final IFaceServiceReceiver mReceiver = new IFaceServiceReceiver.Stub() { @Override public void onEnrollResult(Face face, int remaining) { @@ -116,7 +114,8 @@ public class BiometricTestSessionImpl extends ITestSession.Stub { }; BiometricTestSessionImpl(@NonNull Context context, int sensorId, - @NonNull ITestSessionCallback callback, @NonNull Face10 face10, + @NonNull ITestSessionCallback callback, + @NonNull Face10 face10, @NonNull Face10.HalResultController halResultController) { mContext = context; mSensorId = sensorId; diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index c0a119ff5f1e..0e0ee1966024 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -62,8 +62,10 @@ import com.android.server.biometrics.sensors.AuthenticationConsumer; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricNotificationUtils; import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; +import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; import com.android.server.biometrics.sensors.EnumerateConsumer; import com.android.server.biometrics.sensors.ErrorConsumer; import com.android.server.biometrics.sensors.LockoutResetDispatcher; @@ -110,6 +112,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { private boolean mTestHalEnabled; @NonNull private final FaceSensorPropertiesInternal mSensorProperties; + @NonNull private final BiometricStateCallback mBiometricStateCallback; @NonNull private final Context mContext; @NonNull private final BiometricScheduler mScheduler; @NonNull private final Handler mHandler; @@ -336,6 +339,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @VisibleForTesting Face10(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull Handler handler, @@ -343,6 +347,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull BiometricContext biometricContext) { mSensorProperties = sensorProps; mContext = context; + mBiometricStateCallback = biometricStateCallback; mSensorId = sensorProps.sensorId; mScheduler = scheduler; mHandler = handler; @@ -366,11 +371,12 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { } public static Face10 newInstance(@NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher) { final Handler handler = new Handler(Looper.getMainLooper()); - return new Face10(context, sensorProps, lockoutResetDispatcher, handler, - new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, + return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher, + handler, new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityTracker */), BiometricContext.getInstance(context)); } @@ -615,8 +621,19 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mScheduler.scheduleClientMonitor(client, new ClientMonitorCallback() { @Override + public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { + mBiometricStateCallback.onClientStarted(clientMonitor); + } + + @Override + public void onBiometricAction(int action) { + mBiometricStateCallback.onBiometricAction(action); + } + + @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { + mBiometricStateCallback.onClientFinished(clientMonitor, success); if (success) { // Update authenticatorIds scheduleUpdateActiveUserWithoutHandler(client.getTargetUserId()); @@ -661,7 +678,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient), mBiometricContext, isStrongBiometric, mLockoutTracker, mUsageStats, allowBackgroundAuthentication, isKeyguardBypassEnabled); - mScheduler.scheduleClientMonitor(client); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); } @@ -696,7 +713,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mAuthenticatorIds); - mScheduler.scheduleClientMonitor(client); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); } @@ -714,7 +731,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_REMOVE, BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, mAuthenticatorIds); - mScheduler.scheduleClientMonitor(client); + mScheduler.scheduleClientMonitor(client, mBiometricStateCallback); }); } @@ -806,14 +823,15 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { BiometricsProtoEnums.CLIENT_UNKNOWN), mBiometricContext, enrolledList, FaceUtils.getLegacyInstance(mSensorId), mAuthenticatorIds); - mScheduler.scheduleClientMonitor(client, callback); + mScheduler.scheduleClientMonitor(client, new ClientMonitorCompositeCallback(callback, + mBiometricStateCallback)); }); } @Override public void scheduleInternalCleanup(int sensorId, int userId, @Nullable ClientMonitorCallback callback) { - scheduleInternalCleanup(userId, callback); + scheduleInternalCleanup(userId, mBiometricStateCallback); } @Override @@ -1011,7 +1029,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @Override public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback, @NonNull String opPackageName) { - return new BiometricTestSessionImpl(mContext, mSensorId, callback, this, - mHalResultController); + return new BiometricTestSessionImpl(mContext, mSensorId, callback, + this, mHalResultController); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 17ba07f2c2bd..628c16afed5c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -384,28 +384,18 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi mBiometricContext, mSensors.get(sensorId).getSensorProperties(), mUdfpsOverlayController, mSidefpsController, maxTemplatesPerUser, enrollReason); - scheduleForSensor(sensorId, client, new ClientMonitorCallback() { - - @Override - public void onClientStarted(@NonNull BaseClientMonitor clientMonitor) { - mBiometricStateCallback.onClientStarted(clientMonitor); - } - - @Override - public void onBiometricAction(int action) { - mBiometricStateCallback.onBiometricAction(action); - } - + scheduleForSensor(sensorId, client, new ClientMonitorCompositeCallback( + mBiometricStateCallback, new ClientMonitorCallback() { @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { - mBiometricStateCallback.onClientFinished(clientMonitor, success); + ClientMonitorCallback.super.onClientFinished(clientMonitor, success); if (success) { scheduleLoadAuthenticatorIdsForUser(sensorId, userId); scheduleInvalidationRequest(sensorId, userId); } } - }); + })); }); return id; } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 6795b6b4158d..45b0f0a6d04a 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -79,6 +79,7 @@ import android.net.NetworkScore; import android.net.RouteInfo; import android.net.UidRangeParcel; import android.net.UnderlyingNetworkInfo; +import android.net.Uri; import android.net.VpnManager; import android.net.VpnProfileState; import android.net.VpnService; @@ -226,6 +227,16 @@ public class Vpn { private static final int VPN_DEFAULT_SCORE = 101; /** + * The reset session timer for data stall. If a session has not successfully revalidated after + * the delay, the session will be torn down and restarted in an attempt to recover. Delay + * counter is reset on successful validation only. + * + * <p>If retries have exceeded the length of this array, the last entry in the array will be + * used as a repeating interval. + */ + private static final long[] DATA_STALL_RESET_DELAYS_SEC = {30L, 60L, 120L, 240L, 480L, 960L}; + + /** * The initial token value of IKE session. */ private static final int STARTING_TOKEN = -1; @@ -271,6 +282,7 @@ public class Vpn { private final UserManager mUserManager; private final VpnProfileStore mVpnProfileStore; + protected boolean mDataStallSuspected = false; @VisibleForTesting VpnProfileStore getVpnProfileStore() { @@ -522,12 +534,30 @@ public class Vpn { @NonNull LinkProperties lp, @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, - @Nullable NetworkProvider provider) { + @Nullable NetworkProvider provider, + @Nullable ValidationStatusCallback callback) { return new VpnNetworkAgentWrapper( - context, looper, logTag, nc, lp, score, config, provider); + context, looper, logTag, nc, lp, score, config, provider, callback); + } + + /** + * Get the length of time to wait before resetting the ike session when a data stall is + * suspected. + */ + public long getDataStallResetSessionSeconds(int count) { + if (count >= DATA_STALL_RESET_DELAYS_SEC.length) { + return DATA_STALL_RESET_DELAYS_SEC[DATA_STALL_RESET_DELAYS_SEC.length - 1]; + } else { + return DATA_STALL_RESET_DELAYS_SEC[count]; + } } } + @VisibleForTesting + interface ValidationStatusCallback { + void onValidationStatus(int status); + } + public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd, @UserIdInt int userId, VpnProfileStore vpnProfileStore) { this(looper, context, new Dependencies(), netService, netd, userId, vpnProfileStore, @@ -1460,6 +1490,11 @@ public class Vpn { @GuardedBy("this") private void agentConnect() { + agentConnect(null /* validationCallback */); + } + + @GuardedBy("this") + private void agentConnect(@Nullable ValidationStatusCallback validationCallback) { LinkProperties lp = makeLinkProperties(); // VPN either provide a default route (IPv4 or IPv6 or both), or they are a split tunnel @@ -1507,7 +1542,7 @@ public class Vpn { mNetworkAgent = mDeps.newNetworkAgent(mContext, mLooper, NETWORKTYPE /* logtag */, mNetworkCapabilities, lp, new NetworkScore.Builder().setLegacyInt(VPN_DEFAULT_SCORE).build(), - networkAgentConfig, mNetworkProvider); + networkAgentConfig, mNetworkProvider, validationCallback); final long token = Binder.clearCallingIdentity(); try { mNetworkAgent.register(); @@ -2723,7 +2758,7 @@ public class Vpn { @Nullable private ScheduledFuture<?> mScheduledHandleNetworkLostFuture; @Nullable private ScheduledFuture<?> mScheduledHandleRetryIkeSessionFuture; - + @Nullable private ScheduledFuture<?> mScheduledHandleDataStallFuture; /** Signal to ensure shutdown is honored even if a new Network is connected. */ private boolean mIsRunning = true; @@ -2750,6 +2785,14 @@ public class Vpn { private boolean mMobikeEnabled = false; /** + * The number of attempts to reset the IKE session since the last successful connection. + * + * <p>This variable controls the retry delay, and is reset when the VPN pass network + * validation. + */ + private int mDataStallRetryCount = 0; + + /** * The number of attempts since the last successful connection. * * <p>This variable controls the retry delay, and is reset when a new IKE session is @@ -2931,7 +2974,7 @@ public class Vpn { if (isSettingsVpnLocked()) { prepareStatusIntent(); } - agentConnect(); + agentConnect(this::onValidationStatus); return; // Link properties are already sent. } else { // Underlying networks also set in agentConnect() @@ -3200,18 +3243,52 @@ public class Vpn { // Ignore stale runner. if (mVpnRunner != Vpn.IkeV2VpnRunner.this) return; - // Handle the report only for current VPN network. + // Handle the report only for current VPN network. If data stall is already + // reported, ignoring the other reports. It means that the stall is not + // recovered by MOBIKE and should be on the way to reset the ike session. if (mNetworkAgent != null - && mNetworkAgent.getNetwork().equals(report.getNetwork())) { + && mNetworkAgent.getNetwork().equals(report.getNetwork()) + && !mDataStallSuspected) { Log.d(TAG, "Data stall suspected"); // Trigger MOBIKE. maybeMigrateIkeSession(mActiveNetwork); + mDataStallSuspected = true; } } } } + public void onValidationStatus(int status) { + if (status == NetworkAgent.VALIDATION_STATUS_VALID) { + // No data stall now. Reset it. + mExecutor.execute(() -> { + mDataStallSuspected = false; + mDataStallRetryCount = 0; + if (mScheduledHandleDataStallFuture != null) { + Log.d(TAG, "Recovered from stall. Cancel pending reset action."); + mScheduledHandleDataStallFuture.cancel(false /* mayInterruptIfRunning */); + mScheduledHandleDataStallFuture = null; + } + }); + } else { + // Skip other invalid status if the scheduled recovery exists. + if (mScheduledHandleDataStallFuture != null) return; + + mScheduledHandleDataStallFuture = mExecutor.schedule(() -> { + if (mDataStallSuspected) { + Log.d(TAG, "Reset session to recover stalled network"); + // This will reset old state if it exists. + startIkeSession(mActiveNetwork); + } + + // Reset mScheduledHandleDataStallFuture since it's already run on executor + // thread. + mScheduledHandleDataStallFuture = null; + }, mDeps.getDataStallResetSessionSeconds(mDataStallRetryCount++), TimeUnit.SECONDS); + } + } + /** * Handles loss of the default underlying network * @@ -4339,6 +4416,7 @@ public class Vpn { // un-finalized. @VisibleForTesting public static class VpnNetworkAgentWrapper extends NetworkAgent { + private final ValidationStatusCallback mCallback; /** Create an VpnNetworkAgentWrapper */ public VpnNetworkAgentWrapper( @NonNull Context context, @@ -4348,8 +4426,10 @@ public class Vpn { @NonNull LinkProperties lp, @NonNull NetworkScore score, @NonNull NetworkAgentConfig config, - @Nullable NetworkProvider provider) { + @Nullable NetworkProvider provider, + @Nullable ValidationStatusCallback callback) { super(context, looper, logTag, nc, lp, score, config, provider); + mCallback = callback; } /** Update the LinkProperties */ @@ -4371,6 +4451,13 @@ public class Vpn { public void onNetworkUnwanted() { // We are user controlled, not driven by NetworkRequest. } + + @Override + public void onValidationStatus(int status, Uri redirectUri) { + if (mCallback != null) { + mCallback.onValidationStatus(status); + } + } } /** diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 7e80b7d5b0ac..e907ebfa6471 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -127,6 +127,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.util.DumpUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.IndentingPrintWriter; import com.android.server.AnimationThread; import com.android.server.DisplayThread; @@ -151,6 +152,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; + /** * Manages attached displays. * <p> @@ -1900,6 +1902,14 @@ public final class DisplayManagerService extends SystemService { if (displayDevice == null) { return; } + if (mLogicalDisplayMapper.getDisplayLocked(displayDevice) + .getDisplayInfoLocked().type == Display.TYPE_INTERNAL) { + FrameworkStatsLog.write(FrameworkStatsLog.BRIGHTNESS_CONFIGURATION_UPDATED, + c.getCurve().first, + c.getCurve().second, + // should not be logged for virtual displays + uniqueId); + } mPersistentDataStore.setBrightnessConfigurationForDisplayLocked(c, displayDevice, userSerial, packageName); } finally { diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS index 8534fabb5576..84324f2524fc 100644 --- a/services/core/java/com/android/server/pm/OWNERS +++ b/services/core/java/com/android/server/pm/OWNERS @@ -3,8 +3,6 @@ hackbod@google.com jsharkey@android.com jsharkey@google.com narayan@google.com -svetoslavganov@android.com -svetoslavganov@google.com include /PACKAGE_MANAGER_OWNERS # apex support @@ -26,16 +24,10 @@ per-file PackageManagerServiceCompilerMapping.java = file:dex/OWNERS per-file PackageUsage.java = file:dex/OWNERS # multi user / cross profile -per-file CrossProfileAppsServiceImpl.java = omakoto@google.com, yamasani@google.com -per-file CrossProfileAppsService.java = omakoto@google.com, yamasani@google.com -per-file CrossProfileIntentFilter.java = omakoto@google.com, yamasani@google.com -per-file CrossProfileIntentResolver.java = omakoto@google.com, yamasani@google.com +per-file CrossProfile* = file:MULTIUSER_AND_ENTERPRISE_OWNERS per-file RestrictionsSet.java = file:MULTIUSER_AND_ENTERPRISE_OWNERS -per-file UserManager* = file:/MULTIUSER_OWNERS per-file UserRestriction* = file:MULTIUSER_AND_ENTERPRISE_OWNERS -per-file UserSystemPackageInstaller* = file:/MULTIUSER_OWNERS -per-file UserTypeDetails.java = file:/MULTIUSER_OWNERS -per-file UserTypeFactory.java = file:/MULTIUSER_OWNERS +per-file User* = file:/MULTIUSER_OWNERS # security per-file KeySetHandle.java = cbrubaker@google.com, nnk@google.com diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index a6fac4d60fe7..c4e122d4497d 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4131,6 +4131,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_DEMO_APP_2: case KeyEvent.KEYCODE_DEMO_APP_3: case KeyEvent.KEYCODE_DEMO_APP_4: { + // TODO(b/254604589): Dispatch KeyEvent to System UI. + sendSystemKeyToStatusBarAsync(keyCode); + // Just drop if keys are not intercepted for direct key. result &= ~ACTION_PASS_TO_USER; break; diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index ba414cb593ef..5b7b8f4ca21f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; import android.annotation.NonNull; @@ -43,6 +44,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.os.Bundle; +import android.os.BundleMerger; import android.os.HandlerThread; import android.os.UserHandle; import android.provider.Settings; @@ -57,12 +59,15 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; +import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.List; @SmallTest @RunWith(MockitoJUnitRunner.class) public class BroadcastQueueModernImplTest { private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID; + private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1; @Mock ActivityManagerService mAms; @Mock ProcessRecord mProcess; @@ -87,6 +92,10 @@ public class BroadcastQueueModernImplTest { mHandlerThread.start(); mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS); + mConstants.DELAY_URGENT_MILLIS = -120_000; + mConstants.DELAY_NORMAL_MILLIS = 10_000; + mConstants.DELAY_CACHED_MILLIS = 120_000; + mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), mConstants, mConstants); @@ -467,6 +476,62 @@ public class BroadcastQueueModernImplTest { List.of(musicVolumeChanged, alarmVolumeChanged, timeTick)); } + /** + * Verify that sending a broadcast with DELIVERY_GROUP_POLICY_MERGED works as expected. + */ + @Test + public void testDeliveryGroupPolicy_merged() { + final BundleMerger extrasMerger = new BundleMerger(); + extrasMerger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + BundleMerger.STRATEGY_ARRAY_APPEND); + + final Intent packageChangedForUid = createPackageChangedIntent(TEST_UID, + List.of("com.testuid.component1")); + final BroadcastOptions optionsPackageChangedForUid = BroadcastOptions.makeBasic(); + optionsPackageChangedForUid.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID)); + optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger); + + final Intent secondPackageChangedForUid = createPackageChangedIntent(TEST_UID, + List.of("com.testuid.component2", "com.testuid.component3")); + + final Intent packageChangedForUid2 = createPackageChangedIntent(TEST_UID2, + List.of("com.testuid2.component1")); + final BroadcastOptions optionsPackageChangedForUid2 = BroadcastOptions.makeBasic(); + optionsPackageChangedForUid.setDeliveryGroupPolicy( + BroadcastOptions.DELIVERY_GROUP_POLICY_MERGED); + optionsPackageChangedForUid.setDeliveryGroupKey("package", String.valueOf(TEST_UID2)); + optionsPackageChangedForUid.setDeliveryGroupExtrasMerger(extrasMerger); + + // Halt all processing so that we get a consistent view + mHandlerThread.getLooper().getQueue().postSyncBarrier(); + + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid, + optionsPackageChangedForUid)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(packageChangedForUid2, + optionsPackageChangedForUid2)); + mImpl.enqueueBroadcastLocked(makeBroadcastRecord(secondPackageChangedForUid, + optionsPackageChangedForUid)); + + final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN, + getUidForPackage(PACKAGE_GREEN)); + final Intent expectedPackageChangedForUid = createPackageChangedIntent(TEST_UID, + List.of("com.testuid.component2", "com.testuid.component3", + "com.testuid.component1")); + // Verify that packageChangedForUid and secondPackageChangedForUid broadcasts + // have been merged. + verifyPendingRecords(queue, List.of(packageChangedForUid2, expectedPackageChangedForUid)); + } + + private Intent createPackageChangedIntent(int uid, List<String> componentNameList) { + final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); + packageChangedIntent.putExtra(Intent.EXTRA_UID, uid); + packageChangedIntent.putExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, + componentNameList.toArray()); + return packageChangedIntent; + } + private void verifyPendingRecords(BroadcastProcessQueue queue, List<Intent> intents) { for (int i = 0; i < intents.size(); i++) { @@ -477,9 +542,45 @@ public class BroadcastQueueModernImplTest { + ", actual_extras=" + actualIntent.getExtras() + ", expected_extras=" + expectedIntent.getExtras(); assertTrue(errMsg, actualIntent.filterEquals(expectedIntent)); - assertTrue(errMsg, Bundle.kindofEquals( - actualIntent.getExtras(), expectedIntent.getExtras())); + assertBundleEquals(expectedIntent.getExtras(), actualIntent.getExtras()); } assertTrue(queue.isEmpty()); } + + private void assertBundleEquals(Bundle expected, Bundle actual) { + final String errMsg = "expected=" + expected + ", actual=" + actual; + if (expected == actual) { + return; + } else if (expected == null || actual == null) { + fail(errMsg); + } + if (!expected.keySet().equals(actual.keySet())) { + fail(errMsg); + } + for (String key : expected.keySet()) { + final Object expectedValue = expected.get(key); + final Object actualValue = actual.get(key); + if (expectedValue == actualValue) { + continue; + } else if (expectedValue == null || actualValue == null) { + fail(errMsg); + } + assertEquals(errMsg, expectedValue.getClass(), actualValue.getClass()); + if (expectedValue.getClass().isArray()) { + assertEquals(errMsg, Array.getLength(expectedValue), Array.getLength(actualValue)); + for (int i = 0; i < Array.getLength(expectedValue); ++i) { + assertEquals(errMsg, Array.get(expectedValue, i), Array.get(actualValue, i)); + } + } else if (expectedValue instanceof ArrayList) { + final ArrayList<?> expectedList = (ArrayList<?>) expectedValue; + final ArrayList<?> actualList = (ArrayList<?>) actualValue; + assertEquals(errMsg, expectedList.size(), actualList.size()); + for (int i = 0; i < expectedList.size(); ++i) { + assertEquals(errMsg, expectedList.get(i), actualList.get(i)); + } + } else { + assertEquals(errMsg, expectedValue, actualValue); + } + } + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index d9a26c68f3ed..e1a4c1dd7256 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -888,7 +888,7 @@ public class BroadcastQueueTest { }) { // Confirm expected OOM adjustments; we were invoked once to upgrade // and once to downgrade - assertEquals(ActivityManager.PROCESS_STATE_RECEIVER, + assertEquals(String.valueOf(receiverApp), ActivityManager.PROCESS_STATE_RECEIVER, receiverApp.mState.getReportedProcState()); verify(mAms, times(2)).enqueueOomAdjTargetLocked(eq(receiverApp)); @@ -897,8 +897,8 @@ public class BroadcastQueueTest { // cold-started apps to be thawed, but the modern stack does } else { // Confirm that app was thawed - verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(receiverApp), - eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER)); + verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce()).unfreezeTemporarily( + eq(receiverApp), eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER)); // Confirm that we added package to process verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName), @@ -1599,4 +1599,39 @@ public class BroadcastQueueTest { assertTrue(mQueue.isBeyondBarrierLocked(afterFirst)); assertTrue(mQueue.isBeyondBarrierLocked(afterSecond)); } + + /** + * Verify that we OOM adjust for manifest receivers. + */ + @Test + public void testOomAdjust_Manifest() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), + makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE), + makeManifestReceiver(PACKAGE_GREEN, CLASS_RED)))); + + waitForIdle(); + verify(mAms, atLeastOnce()).enqueueOomAdjTargetLocked(any()); + } + + /** + * Verify that we never OOM adjust for registered receivers. + */ + @Test + public void testOomAdjust_Registered() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); + + final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, + List.of(makeRegisteredReceiver(receiverApp), + makeRegisteredReceiver(receiverApp), + makeRegisteredReceiver(receiverApp)))); + + waitForIdle(); + verify(mAms, never()).enqueueOomAdjTargetLocked(any()); + } } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 0b776a3e6642..fe92a1dbdac1 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -24,6 +24,7 @@ import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static android.os.UserHandle.USER_SYSTEM; import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; @@ -246,7 +247,7 @@ public class UserControllerTest { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ false, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); - mUserController.startUser(TEST_USER_ID, true /* foreground */); + mUserController.startUser(TEST_USER_ID, /* foreground= */ true); verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt()); verify(mInjector.getWindowManager(), never()).stopFreezingScreen(); verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean()); @@ -258,6 +259,8 @@ public class UserControllerTest { assertFalse(mUserController.startUser(TEST_PRE_CREATED_USER_ID, /* foreground= */ true)); // Make sure no intents have been fired for pre-created users. assertTrue(mInjector.mSentIntents.isEmpty()); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -280,6 +283,8 @@ public class UserControllerTest { // binder calls, but their side effects (in this case, that the user is stopped right away) assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes()) .containsExactly(USER_START_MSG); + + verifyUserAssignedToDisplay(TEST_PRE_CREATED_USER_ID, Display.DEFAULT_DISPLAY); } private void startUserAssertions( @@ -303,6 +308,7 @@ public class UserControllerTest { assertEquals("User must be in STATE_BOOTING", UserState.STATE_BOOTING, userState.state); assertEquals("Unexpected old user id", 0, reportMsg.arg1); assertEquals("Unexpected new user id", TEST_USER_ID, reportMsg.arg2); + verifyUserAssignedToDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY); } @Test @@ -313,6 +319,8 @@ public class UserControllerTest { mUserController.startUserInForeground(NONEXIST_USER_ID); verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean()); verify(mInjector.getWindowManager()).setSwitchingUser(false); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -395,6 +403,7 @@ public class UserControllerTest { verify(mInjector, times(0)).dismissKeyguard(any(), anyString()); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); continueUserSwitchAssertions(TEST_USER_ID, false); + verifyOnUserStarting(USER_SYSTEM, /* visible= */ false); } @Test @@ -403,7 +412,7 @@ public class UserControllerTest { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, true); + mUserController.startUser(TEST_USER_ID, /* foreground=*/ true); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); assertNotNull(reportMsg); UserState userState = (UserState) reportMsg.obj; @@ -415,6 +424,7 @@ public class UserControllerTest { verify(mInjector, times(1)).dismissKeyguard(any(), anyString()); verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen(); continueUserSwitchAssertions(TEST_USER_ID, false); + verifyOnUserStarting(USER_SYSTEM, /* visible= */ false); } @Test @@ -423,7 +433,7 @@ public class UserControllerTest { /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false); // Start user -- this will update state of mUserController - mUserController.startUser(TEST_USER_ID, true); + mUserController.startUser(TEST_USER_ID, /* foreground=*/ true); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); assertNotNull(reportMsg); UserState userState = (UserState) reportMsg.obj; @@ -521,6 +531,7 @@ public class UserControllerTest { assertFalse(mUserController.canStartMoreUsers()); assertEquals(Arrays.asList(new Integer[] {0, TEST_USER_ID1, TEST_USER_ID2}), mUserController.getRunningUsersLU()); + verifyOnUserStarting(USER_SYSTEM, /* visible= */ false); } /** @@ -530,7 +541,7 @@ public class UserControllerTest { */ @Test public void testUserLockingFromUserSwitchingForMultipleUsersDelayedLockingMode() - throws InterruptedException, RemoteException { + throws Exception { mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true, /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true); @@ -645,6 +656,8 @@ public class UserControllerTest { setUpUser(TEST_USER_ID1, 0); assertThrows(IllegalArgumentException.class, () -> mUserController.startProfile(TEST_USER_ID1)); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -660,6 +673,8 @@ public class UserControllerTest { setUpUser(TEST_USER_ID1, UserInfo.FLAG_PROFILE | UserInfo.FLAG_DISABLED, /* preCreated= */ false, UserManager.USER_TYPE_PROFILE_MANAGED); assertThat(mUserController.startProfile(TEST_USER_ID1)).isFalse(); + + verifyUserNeverAssignedToDisplay(); } @Test @@ -949,6 +964,10 @@ public class UserControllerTest { verify(mInjector.getUserManagerInternal(), never()).unassignUserFromDisplay(userId); } + private void verifyOnUserStarting(@UserIdInt int userId, boolean visible) { + verify(mInjector).onUserStarting(userId, visible); + } + // Should be public to allow mocking private static class TestInjector extends UserController.Injector { public final TestHandler mHandler; @@ -1084,6 +1103,11 @@ public class UserControllerTest { protected LockPatternUtils getLockPatternUtils() { return mLockPatternUtilsMock; } + + @Override + void onUserStarting(@UserIdInt int userId, boolean visible) { + Log.i(TAG, "onUserStarting(" + userId + ", " + visible + ")"); + } } private static class TestHandler extends Handler { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java index 12b8264fc20c..41f743367aeb 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java @@ -39,6 +39,7 @@ import androidx.test.filters.SmallTest; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.HalClientMonitor; import com.android.server.biometrics.sensors.LockoutResetDispatcher; @@ -63,6 +64,8 @@ public class FaceProviderTest { private IFace mDaemon; @Mock private BiometricContext mBiometricContext; + @Mock + private BiometricStateCallback mBiometricStateCallback; private SensorProps[] mSensorProps; private LockoutResetDispatcher mLockoutResetDispatcher; @@ -91,8 +94,8 @@ public class FaceProviderTest { mLockoutResetDispatcher = new LockoutResetDispatcher(mContext); - mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG, - mLockoutResetDispatcher, mBiometricContext); + mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mBiometricStateCallback, + mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext); } @SuppressWarnings("rawtypes") @@ -140,11 +143,13 @@ public class FaceProviderTest { TestableFaceProvider(@NonNull IFace daemon, @NonNull Context context, + @NonNull BiometricStateCallback biometricStateCallback, @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext) { - super(context, props, halInstanceName, lockoutResetDispatcher, biometricContext); + super(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher, + biometricContext); mDaemon = daemon; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java index 116d2d5a66a0..a2cade7ad797 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java @@ -43,6 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.sensors.BiometricScheduler; +import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import org.junit.Before; @@ -73,6 +74,8 @@ public class Face10Test { private BiometricScheduler mScheduler; @Mock private BiometricContext mBiometricContext; + @Mock + private BiometricStateCallback mBiometricStateCallback; private final Handler mHandler = new Handler(Looper.getMainLooper()); private LockoutResetDispatcher mLockoutResetDispatcher; @@ -103,8 +106,8 @@ public class Face10Test { resetLockoutRequiresChallenge); Face10.sSystemClock = Clock.fixed(Instant.ofEpochMilli(100), ZoneId.of("PST")); - mFace10 = new Face10(mContext, sensorProps, mLockoutResetDispatcher, mHandler, mScheduler, - mBiometricContext); + mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps, + mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext); mBinder = new Binder(); } diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 439eaa69e771..ef693b5278a0 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -3942,6 +3942,10 @@ public class SubscriptionManager { * may provide one. Or, a carrier may decide to provide the phone number via source * {@link #PHONE_NUMBER_SOURCE_CARRIER carrier} if neither source UICC nor IMS is available. * + * <p>The availability and correctness of the phone number depends on the underlying source + * and the network etc. Additional verification is needed to use this number for + * security-related or other sensitive scenarios. + * * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID} * for the default one. * @param source the source of the phone number, one of the PHONE_NUMBER_SOURCE_* constants. @@ -4175,18 +4179,18 @@ public class SubscriptionManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) - public void setUserHandle(int subscriptionId, @Nullable UserHandle userHandle) { + public void setSubscriptionUserHandle(int subscriptionId, @Nullable UserHandle userHandle) { if (!isValidSubscriptionId(subscriptionId)) { - throw new IllegalArgumentException("[setUserHandle]: Invalid subscriptionId: " - + subscriptionId); + throw new IllegalArgumentException("[setSubscriptionUserHandle]: " + + "Invalid subscriptionId: " + subscriptionId); } try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - iSub.setUserHandle(userHandle, subscriptionId, mContext.getOpPackageName()); + iSub.setSubscriptionUserHandle(userHandle, subscriptionId); } else { - throw new IllegalStateException("[setUserHandle]: " + throw new IllegalStateException("[setSubscriptionUserHandle]: " + "subscription service unavailable"); } } catch (RemoteException ex) { @@ -4211,18 +4215,18 @@ public class SubscriptionManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION) - public @Nullable UserHandle getUserHandle(int subscriptionId) { + public @Nullable UserHandle getSubscriptionUserHandle(int subscriptionId) { if (!isValidSubscriptionId(subscriptionId)) { - throw new IllegalArgumentException("[getUserHandle]: Invalid subscriptionId: " - + subscriptionId); + throw new IllegalArgumentException("[getSubscriptionUserHandle]: " + + "Invalid subscriptionId: " + subscriptionId); } try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - return iSub.getUserHandle(subscriptionId, mContext.getOpPackageName()); + return iSub.getSubscriptionUserHandle(subscriptionId); } else { - throw new IllegalStateException("[getUserHandle]: " + throw new IllegalStateException("[getSubscriptionUserHandle]: " + "subscription service unavailable"); } } catch (RemoteException ex) { diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 0211a7f5c5c5..4752cca8bd6c 100755 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -323,22 +323,20 @@ interface ISub { * * @param userHandle the user handle for this subscription * @param subId the unique SubscriptionInfo index in database - * @param callingPackage The package making the IPC. * * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION * @throws IllegalArgumentException if subId is invalid. */ - int setUserHandle(in UserHandle userHandle, int subId, String callingPackage); + int setSubscriptionUserHandle(in UserHandle userHandle, int subId); /** * Get UserHandle for this subscription * * @param subId the unique SubscriptionInfo index in database - * @param callingPackage the package making the IPC * @return userHandle associated with this subscription. * - * @throws SecurityException if doesn't have SMANAGE_SUBSCRIPTION_USER_ASSOCIATION + * @throws SecurityException if doesn't have MANAGE_SUBSCRIPTION_USER_ASSOCIATION * @throws IllegalArgumentException if subId is invalid. */ - UserHandle getUserHandle(int subId, String callingPackage); + UserHandle getSubscriptionUserHandle(int subId); } |