diff options
122 files changed, 3592 insertions, 1798 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 1c9785f34b79..b924ac802812 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -22,6 +22,7 @@ aconfig_srcjars = [ ":android.os.flags-aconfig-java{.generated_srcjars}", ":android.os.vibrator.flags-aconfig-java{.generated_srcjars}", ":android.security.flags-aconfig-java{.generated_srcjars}", + ":android.service.notification.flags-aconfig-java{.generated_srcjars}", ":android.view.flags-aconfig-java{.generated_srcjars}", ":android.view.accessibility.flags-aconfig-java{.generated_srcjars}", ":camera_platform_flags_core_java_lib{.generated_srcjars}", @@ -544,3 +545,16 @@ cc_aconfig_library { name: "device_policy_aconfig_flags_c_lib", aconfig_declarations: "device_policy_aconfig_flags", } + +// Notifications +aconfig_declarations { + name: "android.service.notification.flags-aconfig", + package: "android.service.notification", + srcs: ["core/java/android/service/notification/flags.aconfig"], +} + +java_aconfig_library { + name: "android.service.notification.flags-aconfig-java", + aconfig_declarations: "android.service.notification.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/core/api/current.txt b/core/api/current.txt index c250d3c31669..51cdfc538663 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -668,6 +668,7 @@ package android { field public static final int debuggable = 16842767; // 0x101000f field public static final int defaultFocusHighlightEnabled = 16844130; // 0x1010562 field public static final int defaultHeight = 16844021; // 0x10104f5 + field @FlaggedApi("android.content.res.default_locale") public static final int defaultLocale; field public static final int defaultToDeviceProtectedStorage = 16844036; // 0x1010504 field public static final int defaultValue = 16843245; // 0x10101ed field public static final int defaultWidth = 16844020; // 0x10104f4 @@ -6188,6 +6189,7 @@ package android.app { ctor public LocaleConfig(@NonNull android.os.LocaleList); method public int describeContents(); method @NonNull public static android.app.LocaleConfig fromContextIgnoringOverride(@NonNull android.content.Context); + method @FlaggedApi("android.content.res.default_locale") @Nullable public java.util.Locale getDefaultLocale(); method public int getStatus(); method @Nullable public android.os.LocaleList getSupportedLocales(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -9530,7 +9532,7 @@ package android.companion { method @Nullable public CharSequence getDisplayName(); method public int getId(); method public int getSystemDataSyncFlags(); - method @Nullable public String getTag(); + method @FlaggedApi("android.companion.association_tag") @Nullable public String getTag(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.companion.AssociationInfo> CREATOR; } @@ -9600,7 +9602,7 @@ package android.companion { method @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public void attachSystemDataTransport(int, @NonNull java.io.InputStream, @NonNull java.io.OutputStream) throws android.companion.DeviceNotAssociatedException; method @Nullable public android.content.IntentSender buildAssociationCancellationIntent(); method @Nullable public android.content.IntentSender buildPermissionTransferUserConsentIntent(int) throws android.companion.DeviceNotAssociatedException; - method public void clearAssociationTag(int); + method @FlaggedApi("android.companion.association_tag") public void clearAssociationTag(int); method @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES) public void detachSystemDataTransport(int) throws android.companion.DeviceNotAssociatedException; method public void disableSystemDataSyncForTypes(int, int); method @Deprecated public void disassociate(@NonNull String); @@ -9610,7 +9612,7 @@ package android.companion { method @NonNull public java.util.List<android.companion.AssociationInfo> getMyAssociations(); method @Deprecated public boolean hasNotificationAccess(android.content.ComponentName); method public void requestNotificationAccess(android.content.ComponentName); - method public void setAssociationTag(int, @NonNull String); + method @FlaggedApi("android.companion.association_tag") public void setAssociationTag(int, @NonNull String); method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; method public void startSystemDataTransfer(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.companion.CompanionException>) throws android.companion.DeviceNotAssociatedException; method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; @@ -11719,7 +11721,7 @@ package android.content.om { method @NonNull public void setResourceValue(@NonNull String, @IntRange(from=android.util.TypedValue.TYPE_FIRST_INT, to=android.util.TypedValue.TYPE_LAST_INT) int, int, @Nullable String); method @NonNull public void setResourceValue(@NonNull String, int, @NonNull String, @Nullable String); method @NonNull public void setResourceValue(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @Nullable String); - method @NonNull public void setResourceValue(@NonNull String, @NonNull android.content.res.AssetFileDescriptor, @Nullable String); + method @FlaggedApi("android.content.res.asset_file_descriptor_frro") @NonNull public void setResourceValue(@NonNull String, @NonNull android.content.res.AssetFileDescriptor, @Nullable String); method public void setTargetOverlayable(@Nullable String); } @@ -12675,6 +12677,7 @@ package android.content.pm { method public boolean isDeviceUpgrading(); method public abstract boolean isInstantApp(); method public abstract boolean isInstantApp(@NonNull String); + method @FlaggedApi("android.content.pm.quarantined_enabled") public boolean isPackageQuarantined(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.stay_stopped") public boolean isPackageStopped(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method public boolean isPackageSuspended(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method public boolean isPackageSuspended(); @@ -12913,6 +12916,7 @@ package android.content.pm { field public static final int MATCH_DIRECT_BOOT_UNAWARE = 262144; // 0x40000 field public static final int MATCH_DISABLED_COMPONENTS = 512; // 0x200 field public static final int MATCH_DISABLED_UNTIL_USED_COMPONENTS = 32768; // 0x8000 + field @FlaggedApi("android.content.pm.quarantined_enabled") public static final long MATCH_QUARANTINED_COMPONENTS = 4294967296L; // 0x100000000L field public static final int MATCH_SYSTEM_ONLY = 1048576; // 0x100000 field public static final int MATCH_UNINSTALLED_PACKAGES = 8192; // 0x2000 field public static final long MAXIMUM_VERIFICATION_TIMEOUT = 3600000L; // 0x36ee80L @@ -18547,12 +18551,12 @@ package android.hardware.biometrics { ctor @Deprecated public BiometricPrompt.CryptoObject(@NonNull android.security.identity.IdentityCredential); ctor public BiometricPrompt.CryptoObject(@NonNull android.security.identity.PresentationSession); ctor @FlaggedApi("android.hardware.biometrics.add_key_agreement_crypto_object") public BiometricPrompt.CryptoObject(@NonNull javax.crypto.KeyAgreement); - method public javax.crypto.Cipher getCipher(); + method @Nullable public javax.crypto.Cipher getCipher(); method @Deprecated @Nullable public android.security.identity.IdentityCredential getIdentityCredential(); method @FlaggedApi("android.hardware.biometrics.add_key_agreement_crypto_object") @Nullable public javax.crypto.KeyAgreement getKeyAgreement(); - method public javax.crypto.Mac getMac(); + method @Nullable public javax.crypto.Mac getMac(); method @Nullable public android.security.identity.PresentationSession getPresentationSession(); - method public java.security.Signature getSignature(); + method @Nullable public java.security.Signature getSignature(); } } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 70f5e2f231c9..c3c63b5a1778 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9641,6 +9641,7 @@ package android.nfc { method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void registerControllerAlwaysOnListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean removeNfcUnlockHandler(android.nfc.NfcAdapter.NfcUnlockHandler); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean); + method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderMode(boolean); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener); field public static final int TAG_INTENT_APP_PREF_RESULT_PACKAGE_NOT_FOUND = -1; // 0xffffffff diff --git a/core/api/test-current.txt b/core/api/test-current.txt index bf39b03244ce..03a58bed3389 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -851,7 +851,7 @@ package android.companion { method @NonNull public android.companion.AssociationInfo.Builder setRevoked(boolean); method @NonNull public android.companion.AssociationInfo.Builder setSelfManaged(boolean); method @NonNull public android.companion.AssociationInfo.Builder setSystemDataSyncFlags(int); - method @NonNull public android.companion.AssociationInfo.Builder setTag(@Nullable String); + method @FlaggedApi("android.companion.association_tag") @NonNull public android.companion.AssociationInfo.Builder setTag(@Nullable String); method @NonNull public android.companion.AssociationInfo.Builder setTimeApproved(long); } diff --git a/core/java/Android.bp b/core/java/Android.bp index 0293f66061c1..ddb221f422d9 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -14,6 +14,15 @@ aidl_library { hdrs: ["android/hardware/HardwareBuffer.aidl"], } +// TODO (b/303286040): Remove this once |ENABLE_NFC_MAINLINE_FLAG| is rolled out +filegroup { + name: "framework-core-nfc-infcadapter-sources", + srcs: [ + "android/nfc/INfcAdapter.aidl", + ], + visibility: ["//frameworks/base/services/core"], +} + filegroup { name: "framework-core-sources", srcs: [ diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index a538247998e6..08c18c8b7448 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -3483,12 +3483,10 @@ class ContextImpl extends Context { // only do this if the user already has more than one preferred locale if (r.getConfiguration().getLocales().size() > 1) { - LocaleConfig lc = LocaleConfig.fromContextIgnoringOverride(this); - mResourcesManager.setLocaleList(lc != null - && lc.getSupportedLocales() != null - && !lc.getSupportedLocales().isEmpty() - ? lc.getSupportedLocales() - : null); + LocaleConfig lc = getUserId() < 0 + ? LocaleConfig.fromContextIgnoringOverride(this) + : new LocaleConfig(this); + mResourcesManager.setLocaleConfig(lc); } } diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java index 1fdc51687433..369a78144fd3 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -16,9 +16,11 @@ package android.app; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -30,6 +32,7 @@ import android.util.AttributeSet; import android.util.Slog; import android.util.Xml; +import com.android.internal.R; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParserException; @@ -39,7 +42,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashSet; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -66,6 +69,8 @@ public class LocaleConfig implements Parcelable { public static final String TAG_LOCALE_CONFIG = "locale-config"; public static final String TAG_LOCALE = "locale"; private LocaleList mLocales; + + private Locale mDefaultLocale; private int mStatus = STATUS_NOT_SPECIFIED; /** @@ -193,8 +198,17 @@ public class LocaleConfig implements Parcelable { XmlUtils.beginDocument(parser, TAG_LOCALE_CONFIG); int outerDepth = parser.getDepth(); AttributeSet attrs = Xml.asAttributeSet(parser); - // LinkedHashSet to preserve insertion order - Set<String> localeNames = new LinkedHashSet<>(); + + String defaultLocale = null; + if (android.content.res.Flags.defaultLocale()) { + TypedArray att = res.obtainAttributes( + attrs, com.android.internal.R.styleable.LocaleConfig); + defaultLocale = att.getString( + R.styleable.LocaleConfig_defaultLocale); + att.recycle(); + } + + Set<String> localeNames = new HashSet<>(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_LOCALE.equals(parser.getName())) { final TypedArray attributes = res.obtainAttributes( @@ -209,6 +223,15 @@ public class LocaleConfig implements Parcelable { } mStatus = STATUS_SUCCESS; mLocales = LocaleList.forLanguageTags(String.join(",", localeNames)); + if (defaultLocale != null) { + if (localeNames.contains(defaultLocale)) { + mDefaultLocale = Locale.forLanguageTag(defaultLocale); + } else { + Slog.w(TAG, "Default locale specified that is not contained in the list: " + + defaultLocale); + mStatus = STATUS_PARSING_FAILED; + } + } } /** @@ -224,6 +247,17 @@ public class LocaleConfig implements Parcelable { } /** + * Returns the default locale if specified, otherwise null + * + * @return The default Locale or null + */ + @SuppressLint("UseIcu") + @FlaggedApi(android.content.res.Flags.FLAG_DEFAULT_LOCALE) + public @Nullable Locale getDefaultLocale() { + return mDefaultLocale; + } + + /** * Get the status of reading the resource file where the LocaleConfig was stored. * * <p>Distinguish "the application didn't provide the resource file" from "the application diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 1ecb5d33fba2..6009c29ae53c 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -120,9 +120,9 @@ public class ResourcesManager { private final ReferenceQueue<Resources> mResourcesReferencesQueue = new ReferenceQueue<>(); /** - * The list of locales the app declares it supports. + * The localeConfig of the app. */ - private LocaleList mLocaleList = LocaleList.getEmptyLocaleList(); + private LocaleConfig mLocaleConfig = new LocaleConfig(LocaleList.getEmptyLocaleList()); private static class ApkKey { public final String path; @@ -1612,18 +1612,19 @@ public class ResourcesManager { } /** - * Returns the LocaleList current set + * Returns the LocaleConfig current set */ - public LocaleList getLocaleList() { - return mLocaleList; + public LocaleConfig getLocaleConfig() { + return mLocaleConfig; } /** - * Sets the LocaleList of app's supported locales + * Sets the LocaleConfig of the app */ - public void setLocaleList(LocaleList localeList) { - if ((localeList != null) && !localeList.isEmpty()) { - mLocaleList = localeList; + public void setLocaleConfig(LocaleConfig localeConfig) { + if ((localeConfig != null) && (localeConfig.getSupportedLocales() != null) + && !localeConfig.getSupportedLocales().isEmpty()) { + mLocaleConfig = localeConfig; } } diff --git a/core/java/android/app/servertransaction/ActivityLifecycleItem.java b/core/java/android/app/servertransaction/ActivityLifecycleItem.java index b34f6788fb60..06bff5df490a 100644 --- a/core/java/android/app/servertransaction/ActivityLifecycleItem.java +++ b/core/java/android/app/servertransaction/ActivityLifecycleItem.java @@ -58,6 +58,11 @@ public abstract class ActivityLifecycleItem extends ActivityTransactionItem { super(in); } + @Override + boolean isActivityLifecycleItem() { + return true; + } + /** A final lifecycle state that an activity should reach. */ @LifecycleState public abstract int getTargetState(); diff --git a/core/java/android/app/servertransaction/ClientTransaction.java b/core/java/android/app/servertransaction/ClientTransaction.java index 8617386516af..f7f901b26646 100644 --- a/core/java/android/app/servertransaction/ClientTransaction.java +++ b/core/java/android/app/servertransaction/ClientTransaction.java @@ -45,6 +45,13 @@ import java.util.Objects; */ public class ClientTransaction implements Parcelable, ObjectPoolItem { + /** + * List of transaction items that should be executed in order. Including both + * {@link ActivityLifecycleItem} and other {@link ClientTransactionItem}. + */ + @Nullable + private List<ClientTransactionItem> mTransactionItems; + /** A list of individual callbacks to a client. */ @UnsupportedAppUsage private List<ClientTransactionItem> mActivityCallbacks; @@ -64,9 +71,32 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { } /** - * Add a message to the end of the sequence of callbacks. + * Adds a message to the end of the sequence of transaction items. + * @param item A single message that can contain a client activity/window request/callback. + * TODO(b/260873529): replace both {@link #addCallback} and {@link #setLifecycleStateRequest}. + */ + public void addTransactionItem(@NonNull ClientTransactionItem item) { + if (mTransactionItems == null) { + mTransactionItems = new ArrayList<>(); + } + mTransactionItems.add(item); + } + + /** + * Gets the list of client window requests/callbacks. + * TODO(b/260873529): must be non null after remove the deprecated methods. + */ + @Nullable + public List<ClientTransactionItem> getTransactionItems() { + return mTransactionItems; + } + + /** + * Adds a message to the end of the sequence of callbacks. * @param activityCallback A single message that can contain a lifecycle request/callback. + * @deprecated use {@link #addTransactionItem(ClientTransactionItem)} instead. */ + @Deprecated public void addCallback(@NonNull ClientTransactionItem activityCallback) { if (mActivityCallbacks == null) { mActivityCallbacks = new ArrayList<>(); @@ -74,25 +104,35 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { mActivityCallbacks.add(activityCallback); } - /** Get the list of callbacks. */ + /** + * Gets the list of callbacks. + * @deprecated use {@link #getTransactionItems()} instead. + */ @Nullable @VisibleForTesting @UnsupportedAppUsage + @Deprecated public List<ClientTransactionItem> getCallbacks() { return mActivityCallbacks; } - /** Get the target state lifecycle request. */ + /** + * Gets the target state lifecycle request. + * @deprecated use {@link #getTransactionItems()} instead. + */ @VisibleForTesting(visibility = PACKAGE) @UnsupportedAppUsage + @Deprecated public ActivityLifecycleItem getLifecycleStateRequest() { return mLifecycleStateRequest; } /** - * Set the lifecycle state in which the client should be after executing the transaction. + * Sets the lifecycle state in which the client should be after executing the transaction. * @param stateRequest A lifecycle request initialized with right parameters. + * @deprecated use {@link #addTransactionItem(ClientTransactionItem)} instead. */ + @Deprecated public void setLifecycleStateRequest(@NonNull ActivityLifecycleItem stateRequest) { mLifecycleStateRequest = stateRequest; } @@ -103,6 +143,14 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { * requested by transaction items. */ public void preExecute(@NonNull ClientTransactionHandler clientTransactionHandler) { + if (mTransactionItems != null) { + final int size = mTransactionItems.size(); + for (int i = 0; i < size; ++i) { + mTransactionItems.get(i).preExecute(clientTransactionHandler); + } + return; + } + if (mActivityCallbacks != null) { final int size = mActivityCallbacks.size(); for (int i = 0; i < size; ++i) { @@ -147,12 +195,19 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { @Override public void recycle() { + if (mTransactionItems != null) { + int size = mTransactionItems.size(); + for (int i = 0; i < size; i++) { + mTransactionItems.get(i).recycle(); + } + mTransactionItems = null; + } if (mActivityCallbacks != null) { int size = mActivityCallbacks.size(); for (int i = 0; i < size; i++) { mActivityCallbacks.get(i).recycle(); } - mActivityCallbacks.clear(); + mActivityCallbacks = null; } if (mLifecycleStateRequest != null) { mLifecycleStateRequest.recycle(); @@ -165,8 +220,15 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { // Parcelable implementation /** Write to Parcel. */ + @SuppressWarnings("AndroidFrameworkEfficientParcelable") // Item class is not final. @Override public void writeToParcel(@NonNull Parcel dest, int flags) { + final boolean writeTransactionItems = mTransactionItems != null; + dest.writeBoolean(writeTransactionItems); + if (writeTransactionItems) { + dest.writeParcelableList(mTransactionItems, flags); + } + dest.writeParcelable(mLifecycleStateRequest, flags); final boolean writeActivityCallbacks = mActivityCallbacks != null; dest.writeBoolean(writeActivityCallbacks); @@ -177,11 +239,20 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { /** Read from Parcel. */ private ClientTransaction(@NonNull Parcel in) { - mLifecycleStateRequest = in.readParcelable(getClass().getClassLoader(), android.app.servertransaction.ActivityLifecycleItem.class); + final boolean readTransactionItems = in.readBoolean(); + if (readTransactionItems) { + mTransactionItems = new ArrayList<>(); + in.readParcelableList(mTransactionItems, getClass().getClassLoader(), + ClientTransactionItem.class); + } + + mLifecycleStateRequest = in.readParcelable(getClass().getClassLoader(), + ActivityLifecycleItem.class); final boolean readActivityCallbacks = in.readBoolean(); if (readActivityCallbacks) { mActivityCallbacks = new ArrayList<>(); - in.readParcelableList(mActivityCallbacks, getClass().getClassLoader(), android.app.servertransaction.ClientTransactionItem.class); + in.readParcelableList(mActivityCallbacks, getClass().getClassLoader(), + ClientTransactionItem.class); } } @@ -209,7 +280,8 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { return false; } final ClientTransaction other = (ClientTransaction) o; - return Objects.equals(mActivityCallbacks, other.mActivityCallbacks) + return Objects.equals(mTransactionItems, other.mTransactionItems) + && Objects.equals(mActivityCallbacks, other.mActivityCallbacks) && Objects.equals(mLifecycleStateRequest, other.mLifecycleStateRequest) && mClient == other.mClient; } @@ -217,6 +289,7 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { @Override public int hashCode() { int result = 17; + result = 31 * result + Objects.hashCode(mTransactionItems); result = 31 * result + Objects.hashCode(mActivityCallbacks); result = 31 * result + Objects.hashCode(mLifecycleStateRequest); result = 31 * result + Objects.hashCode(mClient); @@ -227,6 +300,22 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem { void dump(@NonNull String prefix, @NonNull PrintWriter pw, @NonNull ClientTransactionHandler transactionHandler) { pw.append(prefix).println("ClientTransaction{"); + if (mTransactionItems != null) { + pw.append(prefix).print(" transactionItems=["); + final String itemPrefix = prefix + " "; + final int size = mTransactionItems.size(); + if (size > 0) { + pw.println(); + for (int i = 0; i < size; i++) { + mTransactionItems.get(i).dump(itemPrefix, pw, transactionHandler); + } + pw.append(prefix).println(" ]"); + } else { + pw.println("]"); + } + pw.append(prefix).println("}"); + return; + } pw.append(prefix).print(" callbacks=["); final String itemPrefix = prefix + " "; final int size = mActivityCallbacks != null ? mActivityCallbacks.size() : 0; diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java index 07e5a7dc5f02..f94e22de06e5 100644 --- a/core/java/android/app/servertransaction/ClientTransactionItem.java +++ b/core/java/android/app/servertransaction/ClientTransactionItem.java @@ -72,6 +72,13 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel return null; } + /** + * Whether this is a {@link ActivityLifecycleItem}. + */ + boolean isActivityLifecycleItem() { + return false; + } + /** Dumps this transaction item. */ void dump(@NonNull String prefix, @NonNull PrintWriter pw, @NonNull ClientTransactionHandler transactionHandler) { diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index 066f9fe84970..9f5e0dc14cca 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -28,6 +28,7 @@ import static android.app.servertransaction.ActivityLifecycleItem.UNDEFINED; import static android.app.servertransaction.TransactionExecutorHelper.getShortActivityName; import static android.app.servertransaction.TransactionExecutorHelper.getStateName; import static android.app.servertransaction.TransactionExecutorHelper.lastCallbackRequestingState; +import static android.app.servertransaction.TransactionExecutorHelper.shouldExcludeLastLifecycleState; import static android.app.servertransaction.TransactionExecutorHelper.tId; import static android.app.servertransaction.TransactionExecutorHelper.transactionToString; @@ -61,6 +62,9 @@ public class TransactionExecutor { private final PendingTransactionActions mPendingActions = new PendingTransactionActions(); private final TransactionExecutorHelper mHelper = new TransactionExecutorHelper(); + /** Keeps track of display ids whose Configuration got updated within a transaction. */ + private final ArraySet<Integer> mConfigUpdatedDisplayIds = new ArraySet<>(); + /** Initialize an instance with transaction handler, that will execute all requested actions. */ public TransactionExecutor(@NonNull ClientTransactionHandler clientTransactionHandler) { mTransactionHandler = clientTransactionHandler; @@ -79,15 +83,52 @@ public class TransactionExecutor { Slog.d(TAG, transactionToString(transaction, mTransactionHandler)); } - executeCallbacks(transaction); - executeLifecycleState(transaction); + if (transaction.getTransactionItems() != null) { + executeTransactionItems(transaction); + } else { + // TODO(b/260873529): cleanup after launch. + executeCallbacks(transaction); + executeLifecycleState(transaction); + } + + if (!mConfigUpdatedDisplayIds.isEmpty()) { + // Whether this transaction should trigger DisplayListener#onDisplayChanged. + final ClientTransactionListenerController controller = + ClientTransactionListenerController.getInstance(); + final int displayCount = mConfigUpdatedDisplayIds.size(); + for (int i = 0; i < displayCount; i++) { + final int displayId = mConfigUpdatedDisplayIds.valueAt(i); + controller.onDisplayChanged(displayId); + } + mConfigUpdatedDisplayIds.clear(); + } mPendingActions.clear(); if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "End resolving transaction"); } - /** Cycle through all states requested by callbacks and execute them at proper times. */ + /** Cycles through all transaction items and execute them at proper times. */ @VisibleForTesting + public void executeTransactionItems(@NonNull ClientTransaction transaction) { + final List<ClientTransactionItem> items = transaction.getTransactionItems(); + final int size = items.size(); + for (int i = 0; i < size; i++) { + final ClientTransactionItem item = items.get(i); + if (item.isActivityLifecycleItem()) { + executeLifecycleItem(transaction, (ActivityLifecycleItem) item); + } else { + executeNonLifecycleItem(transaction, item, + shouldExcludeLastLifecycleState(items, i)); + } + } + } + + /** + * Cycle through all states requested by callbacks and execute them at proper times. + * @deprecated use {@link #executeTransactionItems} instead. + */ + @VisibleForTesting + @Deprecated public void executeCallbacks(@NonNull ClientTransaction transaction) { final List<ClientTransactionItem> callbacks = transaction.getCallbacks(); if (callbacks == null || callbacks.isEmpty()) { @@ -105,83 +146,78 @@ public class TransactionExecutor { // Index of the last callback that requests some post-execution state. final int lastCallbackRequestingState = lastCallbackRequestingState(transaction); - // Keep track of display ids whose Configuration got updated with this transaction. - ArraySet<Integer> configUpdatedDisplays = null; - final int size = callbacks.size(); for (int i = 0; i < size; ++i) { final ClientTransactionItem item = callbacks.get(i); - final IBinder token = item.getActivityToken(); - ActivityClientRecord r = mTransactionHandler.getActivityClient(token); - - if (token != null && r == null - && mTransactionHandler.getActivitiesToBeDestroyed().containsKey(token)) { - // The activity has not been created but has been requested to destroy, so all - // transactions for the token are just like being cancelled. - Slog.w(TAG, "Skip pre-destroyed transaction item:\n" + item); - continue; - } - if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item); + // Skip the very last transition and perform it by explicit state request instead. final int postExecutionState = item.getPostExecutionState(); + final boolean shouldExcludeLastLifecycleState = postExecutionState != UNDEFINED + && i == lastCallbackRequestingState && finalState == postExecutionState; + executeNonLifecycleItem(transaction, item, shouldExcludeLastLifecycleState); + } + } - if (item.shouldHaveDefinedPreExecutionState()) { - final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r, - item.getPostExecutionState()); - if (closestPreExecutionState != UNDEFINED) { - cycleToPath(r, closestPreExecutionState, transaction); - } - } + private void executeNonLifecycleItem(@NonNull ClientTransaction transaction, + @NonNull ClientTransactionItem item, boolean shouldExcludeLastLifecycleState) { + final IBinder token = item.getActivityToken(); + ActivityClientRecord r = mTransactionHandler.getActivityClient(token); - // Can't read flag from isolated process. - final boolean isSyncWindowConfigUpdateFlagEnabled = !Process.isIsolated() - && syncWindowConfigUpdateFlag(); - final Context configUpdatedContext = isSyncWindowConfigUpdateFlagEnabled - ? item.getContextToUpdate(mTransactionHandler) - : null; - final Configuration preExecutedConfig = configUpdatedContext != null - ? new Configuration(configUpdatedContext.getResources().getConfiguration()) - : null; - - item.execute(mTransactionHandler, mPendingActions); - - if (configUpdatedContext != null) { - final Configuration postExecutedConfig = configUpdatedContext.getResources() - .getConfiguration(); - if (!areConfigurationsEqualForDisplay(postExecutedConfig, preExecutedConfig)) { - if (configUpdatedDisplays == null) { - configUpdatedDisplays = new ArraySet<>(); - } - configUpdatedDisplays.add(configUpdatedContext.getDisplayId()); - } - } + if (token != null && r == null + && mTransactionHandler.getActivitiesToBeDestroyed().containsKey(token)) { + // The activity has not been created but has been requested to destroy, so all + // transactions for the token are just like being cancelled. + Slog.w(TAG, "Skip pre-destroyed transaction item:\n" + item); + return; + } - item.postExecute(mTransactionHandler, mPendingActions); - if (r == null) { - // Launch activity request will create an activity record. - r = mTransactionHandler.getActivityClient(token); - } + if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item); + final int postExecutionState = item.getPostExecutionState(); - if (postExecutionState != UNDEFINED && r != null) { - // Skip the very last transition and perform it by explicit state request instead. - final boolean shouldExcludeLastTransition = - i == lastCallbackRequestingState && finalState == postExecutionState; - cycleToPath(r, postExecutionState, shouldExcludeLastTransition, transaction); + if (item.shouldHaveDefinedPreExecutionState()) { + final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r, + postExecutionState); + if (closestPreExecutionState != UNDEFINED) { + cycleToPath(r, closestPreExecutionState, transaction); } } - if (configUpdatedDisplays != null) { - final ClientTransactionListenerController controller = - ClientTransactionListenerController.getInstance(); - final int displayCount = configUpdatedDisplays.size(); - for (int i = 0; i < displayCount; i++) { - final int displayId = configUpdatedDisplays.valueAt(i); - controller.onDisplayChanged(displayId); + // Can't read flag from isolated process. + final boolean isSyncWindowConfigUpdateFlagEnabled = !Process.isIsolated() + && syncWindowConfigUpdateFlag(); + final Context configUpdatedContext = isSyncWindowConfigUpdateFlagEnabled + ? item.getContextToUpdate(mTransactionHandler) + : null; + final Configuration preExecutedConfig = configUpdatedContext != null + ? new Configuration(configUpdatedContext.getResources().getConfiguration()) + : null; + + item.execute(mTransactionHandler, mPendingActions); + + if (configUpdatedContext != null) { + final Configuration postExecutedConfig = configUpdatedContext.getResources() + .getConfiguration(); + if (!areConfigurationsEqualForDisplay(postExecutedConfig, preExecutedConfig)) { + mConfigUpdatedDisplayIds.add(configUpdatedContext.getDisplayId()); } } + + item.postExecute(mTransactionHandler, mPendingActions); + if (r == null) { + // Launch activity request will create an activity record. + r = mTransactionHandler.getActivityClient(token); + } + + if (postExecutionState != UNDEFINED && r != null) { + cycleToPath(r, postExecutionState, shouldExcludeLastLifecycleState, transaction); + } } - /** Transition to the final state if requested by the transaction. */ + /** + * Transition to the final state if requested by the transaction. + * @deprecated use {@link #executeTransactionItems} instead + */ + @Deprecated private void executeLifecycleState(@NonNull ClientTransaction transaction) { final ActivityLifecycleItem lifecycleItem = transaction.getLifecycleStateRequest(); if (lifecycleItem == null) { @@ -189,6 +225,11 @@ public class TransactionExecutor { return; } + executeLifecycleItem(transaction, lifecycleItem); + } + + private void executeLifecycleItem(@NonNull ClientTransaction transaction, + @NonNull ActivityLifecycleItem lifecycleItem) { final IBinder token = lifecycleItem.getActivityToken(); final ActivityClientRecord r = mTransactionHandler.getActivityClient(token); if (DEBUG_RESOLVER) { diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java index 7e89a5b45a2d..dfbccb41d045 100644 --- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java +++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java @@ -236,21 +236,39 @@ public class TransactionExecutorHelper { * index 1 will be returned, because ActivityResult request on position 1 will be the last * request that moves activity to the RESUMED state where it will eventually end. */ - static int lastCallbackRequestingState(ClientTransaction transaction) { + static int lastCallbackRequestingState(@NonNull ClientTransaction transaction) { final List<ClientTransactionItem> callbacks = transaction.getCallbacks(); - if (callbacks == null || callbacks.size() == 0) { + if (callbacks == null || callbacks.isEmpty() + || transaction.getLifecycleStateRequest() == null) { return -1; } + return lastCallbackRequestingStateIndex(callbacks, 0, callbacks.size() - 1, + transaction.getLifecycleStateRequest().getActivityToken()); + } + /** + * Returns the index of the last callback between the start index and last index that requests + * the state for the given activity token in which that activity will be after execution. + * If there is a group of callbacks in the end that requests the same specific state or doesn't + * request any - we will find the first one from such group. + * + * E.g. ActivityResult requests RESUMED post-execution state, Configuration does not request any + * specific state. If there is a sequence + * Configuration - ActivityResult - Configuration - ActivityResult + * index 1 will be returned, because ActivityResult request on position 1 will be the last + * request that moves activity to the RESUMED state where it will eventually end. + */ + private static int lastCallbackRequestingStateIndex(@NonNull List<ClientTransactionItem> items, + int startIndex, int lastIndex, @NonNull IBinder activityToken) { // Go from the back of the list to front, look for the request closes to the beginning that // requests the state in which activity will end after all callbacks are executed. int lastRequestedState = UNDEFINED; int lastRequestingCallback = -1; - for (int i = callbacks.size() - 1; i >= 0; i--) { - final ClientTransactionItem callback = callbacks.get(i); - final int postExecutionState = callback.getPostExecutionState(); - if (postExecutionState != UNDEFINED) { - // Found a callback that requests some post-execution state. + for (int i = lastIndex; i >= startIndex; i--) { + final ClientTransactionItem item = items.get(i); + final int postExecutionState = item.getPostExecutionState(); + if (postExecutionState != UNDEFINED && activityToken.equals(item.getActivityToken())) { + // Found a callback that requests some post-execution state for the given activity. if (lastRequestedState == UNDEFINED || lastRequestedState == postExecutionState) { // It's either a first-from-end callback that requests state or it requests // the same state as the last one. In both cases, we will use it as the new @@ -266,6 +284,53 @@ public class TransactionExecutorHelper { return lastRequestingCallback; } + /** + * For the transaction item at {@code currentIndex}, if it is requesting post execution state, + * whether or not to exclude the last state. This only returns {@code true} when there is a + * following explicit {@link ActivityLifecycleItem} requesting the same state for the same + * activity, so that last state will be covered by the following {@link ActivityLifecycleItem}. + */ + static boolean shouldExcludeLastLifecycleState(@NonNull List<ClientTransactionItem> items, + int currentIndex) { + final ClientTransactionItem item = items.get(currentIndex); + final IBinder activityToken = item.getActivityToken(); + final int postExecutionState = item.getPostExecutionState(); + if (activityToken == null || postExecutionState == UNDEFINED) { + // Not a transaction item requesting post execution state. + return false; + } + final int nextLifecycleItemIndex = findNextLifecycleItemIndex(items, currentIndex + 1, + activityToken); + if (nextLifecycleItemIndex == -1) { + // No following ActivityLifecycleItem for this activity token. + return false; + } + final ActivityLifecycleItem lifecycleItem = + (ActivityLifecycleItem) items.get(nextLifecycleItemIndex); + if (postExecutionState != lifecycleItem.getTargetState()) { + // The explicit ActivityLifecycleItem is not requesting the same state. + return false; + } + // Only exclude for the first non-lifecycle item that requests the same specific state. + return currentIndex == lastCallbackRequestingStateIndex(items, currentIndex, + nextLifecycleItemIndex - 1, activityToken); + } + + /** + * Finds the index of the next {@link ActivityLifecycleItem} for the given activity token. + */ + private static int findNextLifecycleItemIndex(@NonNull List<ClientTransactionItem> items, + int startIndex, @NonNull IBinder activityToken) { + final int size = items.size(); + for (int i = startIndex; i < size; i++) { + final ClientTransactionItem item = items.get(i); + if (item.isActivityLifecycleItem() && item.getActivityToken().equals(activityToken)) { + return i; + } + } + return -1; + } + /** Dump transaction to string. */ static String transactionToString(@NonNull ClientTransaction transaction, @NonNull ClientTransactionHandler transactionHandler) { diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 6393c456bdcd..8b09bdf2ab17 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -144,6 +144,7 @@ public final class AssociationInfo implements Parcelable { * @return the tag of this association. * @see CompanionDeviceManager#setAssociationTag(int, String) */ + @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @Nullable public String getTag() { return mTag; @@ -459,6 +460,7 @@ public final class AssociationInfo implements Parcelable { } /** @hide */ + @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @TestApi @NonNull public Builder setTag(@Nullable String tag) { diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index dbc67fc5a3a3..70811bb329ec 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -1435,6 +1435,7 @@ public final class CompanionDeviceManager { * of the companion device recorded by CompanionDeviceManager * @param tag the tag of this association */ + @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @UserHandleAware public void setAssociationTag(int associationId, @NonNull String tag) { Objects.requireNonNull(tag, "tag cannot be null"); @@ -1459,6 +1460,7 @@ public final class CompanionDeviceManager { * of the companion device recorded by CompanionDeviceManager * @see CompanionDeviceManager#setAssociationTag(int, String) */ + @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG) @UserHandleAware public void clearAssociationTag(int associationId) { try { diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig index 1b4234b5205c..4f9c849865fc 100644 --- a/core/java/android/companion/flags.aconfig +++ b/core/java/android/companion/flags.aconfig @@ -12,4 +12,11 @@ flag { namespace: "companion" description: "Grants access to the companion transport apis." bug: "288297505" +} + +flag { + name: "association_tag" + namespace: "companion" + description: "Enable Association tag APIs " + bug: "289241123" }
\ No newline at end of file diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java index c4547b8acc2b..df2d7e70880f 100644 --- a/core/java/android/content/om/FabricatedOverlay.java +++ b/core/java/android/content/om/FabricatedOverlay.java @@ -16,6 +16,7 @@ package android.content.om; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -546,6 +547,7 @@ public class FabricatedOverlay { * @param configuration The string representation of the config this overlay is enabled for */ @NonNull + @FlaggedApi(android.content.res.Flags.FLAG_ASSET_FILE_DESCRIPTOR_FRRO) public void setResourceValue( @NonNull String resourceName, @NonNull AssetFileDescriptor value, diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 1b60f8ed904f..b15c9e4fa15b 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1256,8 +1256,10 @@ public abstract class PackageManager { public static final long MATCH_ARCHIVED_PACKAGES = 1L << 32; /** - * @hide + * Querying flag: always match components of packages in quarantined state. + * @see #isPackageQuarantined */ + @FlaggedApi(android.content.pm.Flags.FLAG_QUARANTINED_ENABLED) public static final long MATCH_QUARANTINED_COMPONENTS = 0x100000000L; /** @@ -9902,12 +9904,16 @@ public abstract class PackageManager { /** * Query if an app is currently quarantined. + * A misbehaving app can be quarantined by e.g. a system of another privileged entity. + * Quarantined apps are similar to disabled, but still visible in e.g. Launcher. + * Only activities of such apps can still be queried, but not services etc. + * Quarantined apps can't be bound to, and won't receive broadcasts. + * They can't be resolved, unless {@link #MATCH_QUARANTINED_COMPONENTS} specified. * * @return {@code true} if the given package is quarantined, {@code false} otherwise * @throws NameNotFoundException if the package could not be found. - * - * @hide */ + @FlaggedApi(android.content.pm.Flags.FLAG_QUARANTINED_ENABLED) public boolean isPackageQuarantined(@NonNull String packageName) throws NameNotFoundException { throw new UnsupportedOperationException("isPackageQuarantined not implemented"); } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 5cc3b92da305..c7790bd96c62 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -27,6 +27,8 @@ import android.annotation.PluralsRes; import android.annotation.RawRes; import android.annotation.StyleRes; import android.annotation.StyleableRes; +import android.app.LocaleConfig; +import android.app.ResourcesManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; @@ -426,38 +428,59 @@ public class ResourcesImpl { String[] selectedLocales = null; String defaultLocale = null; + LocaleConfig lc = ResourcesManager.getInstance().getLocaleConfig(); if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) { if (locales.size() > 1) { - String[] availableLocales; - // The LocaleList has changed. We must query the AssetManager's - // available Locales and figure out the best matching Locale in the new - // LocaleList. - availableLocales = mAssets.getNonSystemLocales(); - if (LocaleList.isPseudoLocalesOnly(availableLocales)) { - // No app defined locales, so grab the system locales. - availableLocales = mAssets.getLocales(); + if (Flags.defaultLocale() && (lc.getDefaultLocale() != null)) { + Locale[] intersection = + locales.getIntersection(lc.getSupportedLocales()); + mConfiguration.setLocales(new LocaleList(intersection)); + selectedLocales = new String[intersection.length]; + for (int i = 0; i < intersection.length; i++) { + selectedLocales[i] = + adjustLanguageTag(intersection[i].toLanguageTag()); + } + defaultLocale = + adjustLanguageTag(lc.getDefaultLocale().toLanguageTag()); + } else { + String[] availableLocales; + // The LocaleList has changed. We must query the AssetManager's + // available Locales and figure out the best matching Locale in the new + // LocaleList. + availableLocales = mAssets.getNonSystemLocales(); if (LocaleList.isPseudoLocalesOnly(availableLocales)) { - availableLocales = null; + // No app defined locales, so grab the system locales. + availableLocales = mAssets.getLocales(); + if (LocaleList.isPseudoLocalesOnly(availableLocales)) { + availableLocales = null; + } } - } - if (availableLocales != null) { - final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( - availableLocales); - if (bestLocale != null) { - selectedLocales = new String[]{ - adjustLanguageTag(bestLocale.toLanguageTag())}; - if (!bestLocale.equals(locales.get(0))) { - mConfiguration.setLocales( - new LocaleList(bestLocale, locales)); + if (availableLocales != null) { + final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( + availableLocales); + if (bestLocale != null) { + selectedLocales = new String[]{ + adjustLanguageTag(bestLocale.toLanguageTag())}; + if (!bestLocale.equals(locales.get(0))) { + mConfiguration.setLocales( + new LocaleList(bestLocale, locales)); + } } } } } } if (selectedLocales == null) { - selectedLocales = new String[]{ - adjustLanguageTag(locales.get(0).toLanguageTag())}; + if (Flags.defaultLocale() && (lc.getDefaultLocale() != null)) { + selectedLocales = new String[locales.size()]; + for (int i = 0; i < locales.size(); i++) { + selectedLocales[i] = adjustLanguageTag(locales.get(i).toLanguageTag()); + } + } else { + selectedLocales = new String[]{ + adjustLanguageTag(locales.get(0).toLanguageTag())}; + } } if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) { diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index 0c2c0f494257..1b8eb0748737 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -8,3 +8,10 @@ flag { # fixed_read_only or device wont boot because of permission issues accessing flags during boot is_fixed_read_only: true } + +flag { + name: "asset_file_descriptor_frro" + namespace: "resource_manager" + description: "Feature flag for passing in an AssetFileDescriptor to create an frro" + bug: "304478666" +} diff --git a/core/java/android/hardware/HardwareBuffer.aidl b/core/java/android/hardware/HardwareBuffer.aidl index 1333f0da725f..a9742cb6c084 100644 --- a/core/java/android/hardware/HardwareBuffer.aidl +++ b/core/java/android/hardware/HardwareBuffer.aidl @@ -16,4 +16,4 @@ package android.hardware; -@JavaOnlyStableParcelable @NdkOnlyStableParcelable parcelable HardwareBuffer ndk_header "android/hardware_buffer_aidl.h"; +@JavaOnlyStableParcelable @NdkOnlyStableParcelable @RustOnlyStableParcelable parcelable HardwareBuffer ndk_header "android/hardware_buffer_aidl.h" rust_type "nativewindow::HardwareBuffer"; diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 490ff640885e..7a43286692c4 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -805,7 +805,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Get {@link Signature} object. * @return {@link Signature} object or null if this doesn't contain one. */ - public Signature getSignature() { + public @Nullable Signature getSignature() { return super.getSignature(); } @@ -813,7 +813,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Get {@link Cipher} object. * @return {@link Cipher} object or null if this doesn't contain one. */ - public Cipher getCipher() { + public @Nullable Cipher getCipher() { return super.getCipher(); } @@ -821,7 +821,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * Get {@link Mac} object. * @return {@link Mac} object or null if this doesn't contain one. */ - public Mac getMac() { + public @Nullable Mac getMac() { return super.getMac(); } diff --git a/core/java/android/hardware/biometrics/CryptoObject.java b/core/java/android/hardware/biometrics/CryptoObject.java index 6ac1efb49839..39fbe83b6abb 100644 --- a/core/java/android/hardware/biometrics/CryptoObject.java +++ b/core/java/android/hardware/biometrics/CryptoObject.java @@ -20,6 +20,7 @@ import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OB import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.Nullable; import android.security.identity.IdentityCredential; import android.security.identity.PresentationSession; import android.security.keystore2.AndroidKeyStoreProvider; @@ -33,20 +34,35 @@ import javax.crypto.Mac; /** * A wrapper class for the crypto objects supported by BiometricPrompt and FingerprintManager. * Currently the framework supports {@link Signature}, {@link Cipher}, {@link Mac}, - * {@link IdentityCredential}, and {@link PresentationSession} objects. + * {@link KeyAgreement}, {@link IdentityCredential}, and {@link PresentationSession} objects. * @hide */ public class CryptoObject { private final Object mCrypto; + /** + * Create from a {@link Signature} object. + * + * @param signature a {@link Signature} object. + */ public CryptoObject(@NonNull Signature signature) { mCrypto = signature; } + /** + * Create from a {@link Cipher} object. + * + * @param cipher a {@link Cipher} object. + */ public CryptoObject(@NonNull Cipher cipher) { mCrypto = cipher; } + /** + * Create from a {@link Mac} object. + * + * @param mac a {@link Mac} object. + */ public CryptoObject(@NonNull Mac mac) { mCrypto = mac; } @@ -62,10 +78,20 @@ public class CryptoObject { mCrypto = credential; } + /** + * Create from a {@link PresentationSession} object. + * + * @param session a {@link PresentationSession} object. + */ public CryptoObject(@NonNull PresentationSession session) { mCrypto = session; } + /** + * Create from a {@link KeyAgreement} object. + * + * @param keyAgreement a {@link KeyAgreement} object. + */ @FlaggedApi(FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT) public CryptoObject(@NonNull KeyAgreement keyAgreement) { mCrypto = keyAgreement; @@ -75,7 +101,7 @@ public class CryptoObject { * Get {@link Signature} object. * @return {@link Signature} object or null if this doesn't contain one. */ - public Signature getSignature() { + public @Nullable Signature getSignature() { return mCrypto instanceof Signature ? (Signature) mCrypto : null; } @@ -83,7 +109,7 @@ public class CryptoObject { * Get {@link Cipher} object. * @return {@link Cipher} object or null if this doesn't contain one. */ - public Cipher getCipher() { + public @Nullable Cipher getCipher() { return mCrypto instanceof Cipher ? (Cipher) mCrypto : null; } @@ -91,7 +117,7 @@ public class CryptoObject { * Get {@link Mac} object. * @return {@link Mac} object or null if this doesn't contain one. */ - public Mac getMac() { + public @Nullable Mac getMac() { return mCrypto instanceof Mac ? (Mac) mCrypto : null; } @@ -101,7 +127,7 @@ public class CryptoObject { * @deprecated Use {@link PresentationSession} instead of {@link IdentityCredential}. */ @Deprecated - public IdentityCredential getIdentityCredential() { + public @Nullable IdentityCredential getIdentityCredential() { return mCrypto instanceof IdentityCredential ? (IdentityCredential) mCrypto : null; } @@ -109,16 +135,18 @@ public class CryptoObject { * Get {@link PresentationSession} object. * @return {@link PresentationSession} object or null if this doesn't contain one. */ - public PresentationSession getPresentationSession() { + public @Nullable PresentationSession getPresentationSession() { return mCrypto instanceof PresentationSession ? (PresentationSession) mCrypto : null; } /** - * Get {@link KeyAgreement} object. + * Get {@link KeyAgreement} object. A key-agreement protocol is a protocol whereby + * two or more parties can agree on a shared secret using public key cryptography. + * * @return {@link KeyAgreement} object or null if this doesn't contain one. */ @FlaggedApi(FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT) - public KeyAgreement getKeyAgreement() { + public @Nullable KeyAgreement getKeyAgreement() { return mCrypto instanceof KeyAgreement ? (KeyAgreement) mCrypto : null; } diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 8decd50664b3..2b5f5ee35a26 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -220,7 +220,7 @@ public final class DisplayManagerGlobal { registerCallbackIfNeededLocked(); - if (DEBUG || extraLogging()) { + if (DEBUG) { Log.d(TAG, "getDisplayInfo: displayId=" + displayId + ", info=" + info); } return info; @@ -402,7 +402,7 @@ public final class DisplayManagerGlobal { } private void maybeLogAllDisplayListeners() { - if (!sExtraDisplayListenerLogging) { + if (!extraLogging()) { return; } @@ -1222,7 +1222,7 @@ public final class DisplayManagerGlobal { private void handleMessage(Message msg) { if (extraLogging()) { - Slog.i(TAG, "DisplayListenerDelegate(" + eventToString(msg.what) + Slog.i(TAG, "DLD(" + eventToString(msg.what) + ", display=" + msg.arg1 + ", mEventsMask=" + Long.toBinaryString(mEventsMask) + ", mPackageName=" + mPackageName @@ -1231,9 +1231,10 @@ public final class DisplayManagerGlobal { } if (DEBUG) { Trace.beginSection( - "DisplayListenerDelegate(" + eventToString(msg.what) + TextUtils.trimToSize( + "DLD(" + eventToString(msg.what) + ", display=" + msg.arg1 - + ", listener=" + mListener.getClass() + ")"); + + ", listener=" + mListener.getClass() + ")", 127)); } switch (msg.what) { case EVENT_DISPLAY_ADDED: @@ -1422,11 +1423,12 @@ public final class DisplayManagerGlobal { sExtraDisplayListenerLogging = !TextUtils.isEmpty(EXTRA_LOGGING_PACKAGE_NAME) && EXTRA_LOGGING_PACKAGE_NAME.equals(sCurrentPackageName); } - return sExtraDisplayListenerLogging; + // TODO: b/306170135 - return sExtraDisplayListenerLogging instead + return true; } private static boolean extraLogging() { - return sExtraDisplayListenerLogging && EXTRA_LOGGING_PACKAGE_NAME.equals( - sCurrentPackageName); + // TODO: b/306170135 - return sExtraDisplayListenerLogging & package name check instead + return true; } } diff --git a/core/java/android/nfc/Constants.java b/core/java/android/nfc/Constants.java new file mode 100644 index 000000000000..f76833063605 --- /dev/null +++ b/core/java/android/nfc/Constants.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.nfc; + +/** + * @hide + * TODO(b/303286040): Holds @hide API constants. Formalize these APIs. + */ +public final class Constants { + private Constants() { } + + public static final String SETTINGS_SECURE_NFC_PAYMENT_FOREGROUND = "nfc_payment_foreground"; + public static final String SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component"; + public static final String FEATURE_NFC_ANY = "android.hardware.nfc.any"; +} diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 46586308e3cf..4a7bd3f29458 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -24,6 +24,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.UserIdInt; import android.app.Activity; @@ -37,6 +38,7 @@ import android.nfc.tech.MifareClassic; import android.nfc.tech.Ndef; import android.nfc.tech.NfcA; import android.nfc.tech.NfcF; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -1594,6 +1596,40 @@ public final class NfcAdapter { mNfcActivityManager.disableReaderMode(activity); } + // Flags arguments to NFC adapter to enable/disable NFC + private static final int DISABLE_POLLING_FLAGS = 0x1000; + private static final int ENABLE_POLLING_FLAGS = 0x0000; + + /** + * Privileged API to enable disable reader polling. + * Note: Use with caution! The app is responsible for ensuring that the polling state is + * returned to normal. + * + * @see #enableReaderMode(Activity, ReaderCallback, int, Bundle) for more detailed + * documentation. + * + * @param enablePolling whether to enable or disable polling. + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @FlaggedApi(Flags.FLAG_ENABLE_NFC_MAINLINE) + @SuppressLint("VisiblySynchronized") + public void setReaderMode(boolean enablePolling) { + synchronized (NfcAdapter.class) { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + } + Binder token = new Binder(); + int flags = enablePolling ? ENABLE_POLLING_FLAGS : DISABLE_POLLING_FLAGS; + try { + NfcAdapter.sService.setReaderMode(token, null, flags, null); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + /** * Manually invoke Android Beam to share data. * diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java index 4909b0830eeb..32c2a1b40530 100644 --- a/core/java/android/nfc/cardemulation/CardEmulation.java +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -26,6 +26,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.nfc.Constants; import android.nfc.INfcCardEmulation; import android.nfc.NfcAdapter; import android.os.RemoteException; @@ -274,7 +275,7 @@ public final class CardEmulation { try { preferForeground = Settings.Secure.getInt( contextAsUser.getContentResolver(), - Settings.Secure.NFC_PAYMENT_FOREGROUND) != 0; + Constants.SETTINGS_SECURE_NFC_PAYMENT_FOREGROUND) != 0; } catch (SettingNotFoundException e) { } return preferForeground; diff --git a/core/java/android/os/LocaleList.java b/core/java/android/os/LocaleList.java index 82cdd280a0f3..d7e440b66e13 100644 --- a/core/java/android/os/LocaleList.java +++ b/core/java/android/os/LocaleList.java @@ -153,21 +153,21 @@ public final class LocaleList implements Parcelable { /** * Find the intersection between this LocaleList and another - * @return a String array of the Locales in both LocaleLists + * @return an array of the Locales in both LocaleLists * {@hide} */ @NonNull - public String[] getIntersection(@NonNull LocaleList other) { - List<String> intersection = new ArrayList<>(); + public Locale[] getIntersection(@NonNull LocaleList other) { + List<Locale> intersection = new ArrayList<>(); for (Locale l1 : mList) { for (Locale l2 : other.mList) { if (matchesLanguageAndScript(l2, l1)) { - intersection.add(l1.toLanguageTag()); + intersection.add(l1); break; } } } - return intersection.toArray(new String[0]); + return intersection.toArray(new Locale[0]); } /** diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java index c82a4cabaddd..2a4cbaf79a75 100644 --- a/core/java/android/service/notification/NotificationRankingUpdate.java +++ b/core/java/android/service/notification/NotificationRankingUpdate.java @@ -28,8 +28,6 @@ import android.system.OsConstants; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -59,8 +57,7 @@ public class NotificationRankingUpdate implements Parcelable { * @hide */ public NotificationRankingUpdate(Parcel in) { - if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( - SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + if (Flags.rankingUpdateAshmem()) { // Recover the ranking map from the SharedMemory and store it in mapParcel. final Parcel mapParcel = Parcel.obtain(); ByteBuffer buffer = null; @@ -176,8 +173,7 @@ public class NotificationRankingUpdate implements Parcelable { */ @Override public void writeToParcel(@NonNull Parcel out, int flags) { - if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( - SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + if (Flags.rankingUpdateAshmem()) { final Parcel mapParcel = Parcel.obtain(); ArrayList<NotificationListenerService.Ranking> marshalableRankings = new ArrayList<>(); Bundle smartActionsBundle = new Bundle(); diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig new file mode 100644 index 000000000000..293143595b5e --- /dev/null +++ b/core/java/android/service/notification/flags.aconfig @@ -0,0 +1,9 @@ +package: "android.service.notification" + +flag { + name: "ranking_update_ashmem" + namespace: "systemui" + description: "This flag controls moving ranking update contents into ashmem" + bug: "284297289" +} + diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 52b7cb18259f..a7b3c10edd44 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -661,6 +661,10 @@ public final class ViewRootImpl implements ViewParent, */ private boolean mCheckIfCanDraw = false; + private boolean mWasLastDrawCanceled; + private boolean mLastTraversalWasVisible = true; + private boolean mLastDrawScreenOff; + private boolean mDrewOnceForSync = false; int mSyncSeqId = 0; @@ -1051,7 +1055,8 @@ public final class ViewRootImpl implements ViewParent, mDisplay = display; mBasePackageName = context.getBasePackageName(); final String name = DisplayProperties.debug_vri_package().orElse(null); - mExtraDisplayListenerLogging = !TextUtils.isEmpty(name) && name.equals(mBasePackageName); + // TODO: b/306170135 - return to using textutils check on package name. + mExtraDisplayListenerLogging = true; mThread = Thread.currentThread(); mLocation = new WindowLeaked(null); mLocation.fillInStackTrace(); @@ -1925,12 +1930,19 @@ public final class ViewRootImpl implements ViewParent, } void handleAppVisibility(boolean visible) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + Trace.instant(Trace.TRACE_TAG_VIEW, TextUtils.formatSimple( + "%s visibilityChanged oldVisibility=%b newVisibility=%b", mTag, + mAppVisible, visible)); + } if (mAppVisible != visible) { final boolean previousVisible = getHostVisibility() == View.VISIBLE; mAppVisible = visible; final boolean currentVisible = getHostVisibility() == View.VISIBLE; // Root view only cares about whether it is visible or not. if (previousVisible != currentVisible) { + Log.d(mTag, "visibilityChanged oldVisibility=" + previousVisible + " newVisibility=" + + currentVisible); mAppVisibilityChanged = true; scheduleTraversals(); } @@ -2038,6 +2050,10 @@ public final class ViewRootImpl implements ViewParent, Slog.i(mTag, "DisplayState - old: " + oldDisplayState + ", new: " + newDisplayState); } + if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) { + Trace.traceCounter(Trace.TRACE_TAG_WINDOW_MANAGER, + "vri#screenState[" + mTag + "] state=", newDisplayState); + } if (oldDisplayState != newDisplayState) { mAttachInfo.mDisplayState = newDisplayState; pokeDrawLockIfNeeded(); @@ -3287,8 +3303,8 @@ public final class ViewRootImpl implements ViewParent, || mForceNextWindowRelayout) { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, - TextUtils.formatSimple("relayoutWindow#" - + "first=%b/resize=%b/vis=%b/params=%b/force=%b", + TextUtils.formatSimple("%s-relayoutWindow#" + + "first=%b/resize=%b/vis=%b/params=%b/force=%b", mTag, mFirst, windowShouldResize, viewVisibilityChanged, params != null, mForceNextWindowRelayout)); } @@ -3877,11 +3893,7 @@ public final class ViewRootImpl implements ViewParent, boolean cancelDueToPreDrawListener = mAttachInfo.mTreeObserver.dispatchOnPreDraw(); boolean cancelAndRedraw = cancelDueToPreDrawListener || (cancelDraw && mDrewOnceForSync); - if (cancelAndRedraw) { - Log.d(mTag, "Cancelling draw." - + " cancelDueToPreDrawListener=" + cancelDueToPreDrawListener - + " cancelDueToSync=" + (cancelDraw && mDrewOnceForSync)); - } + if (!cancelAndRedraw) { // A sync was already requested before the WMS requested sync. This means we need to // sync the buffer, regardless if WMS wants to sync the buffer. @@ -3905,6 +3917,9 @@ public final class ViewRootImpl implements ViewParent, } if (!isViewVisible) { + if (mLastTraversalWasVisible) { + logAndTrace("Not drawing due to not visible"); + } mLastPerformTraversalsSkipDrawReason = "view_not_visible"; if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { @@ -3916,12 +3931,23 @@ public final class ViewRootImpl implements ViewParent, handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions, mPendingTransaction, "view not visible"); } else if (cancelAndRedraw) { + if (!mWasLastDrawCanceled) { + logAndTrace("Canceling draw." + + " cancelDueToPreDrawListener=" + cancelDueToPreDrawListener + + " cancelDueToSync=" + (cancelDraw && mDrewOnceForSync)); + } mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener ? "predraw_" + mAttachInfo.mTreeObserver.getLastDispatchOnPreDrawCanceledReason() : "cancel_" + cancelReason; // Try again scheduleTraversals(); } else { + if (mWasLastDrawCanceled) { + logAndTrace("Draw frame after cancel"); + } + if (!mLastTraversalWasVisible) { + logAndTrace("Start draw after previous draw not visible"); + } if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); @@ -3933,6 +3959,8 @@ public final class ViewRootImpl implements ViewParent, mPendingTransaction, mLastPerformDrawSkippedReason); } } + mWasLastDrawCanceled = cancelAndRedraw; + mLastTraversalWasVisible = isViewVisible; if (mAttachInfo.mContentCaptureEvents != null) { notifyContentCaptureEvents(); @@ -4728,10 +4756,7 @@ public final class ViewRootImpl implements ViewParent, return didProduceBuffer -> { if (!didProduceBuffer) { - Trace.instant(Trace.TRACE_TAG_VIEW, - "Transaction not synced due to no frame drawn-" + mTag); - Log.d(mTag, "Pending transaction will not be applied in sync with a draw " - + "because there was nothing new to draw"); + logAndTrace("Transaction not synced due to no frame drawn"); mBlastBufferQueue.applyPendingTransactions(frame); } }; @@ -4748,17 +4773,26 @@ public final class ViewRootImpl implements ViewParent, mLastPerformDrawSkippedReason = null; if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { mLastPerformDrawSkippedReason = "screen_off"; + if (!mLastDrawScreenOff) { + logAndTrace("Not drawing due to screen off"); + } + mLastDrawScreenOff = true; return false; } else if (mView == null) { mLastPerformDrawSkippedReason = "no_root_view"; return false; } + if (mLastDrawScreenOff) { + logAndTrace("Resumed drawing after screen turned on"); + mLastDrawScreenOff = false; + } + final boolean fullRedrawNeeded = mFullRedrawNeeded || surfaceSyncGroup != null; mFullRedrawNeeded = false; mIsDrawing = true; - Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); + Trace.traceBegin(Trace.TRACE_TAG_VIEW, mTag + "-draw"); addFrameCommitCallbackIfNeeded(); @@ -11512,8 +11546,7 @@ public final class ViewRootImpl implements ViewParent, @Override public boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t) { if (mRemoved || !isHardwareEnabled()) { - Trace.instant(Trace.TRACE_TAG_VIEW, "applyTransactionOnDraw applyImmediately-" + mTag); - Log.d(mTag, "applyTransactionOnDraw: Applying transaction immediately"); + logAndTrace("applyTransactionOnDraw applyImmediately"); t.apply(); } else { Trace.instant(Trace.TRACE_TAG_VIEW, "applyTransactionOnDraw-" + mTag); @@ -11990,4 +12023,11 @@ public final class ViewRootImpl implements ViewParent, public float getPreferredFrameRate() { return mPreferredFrameRate; } + + private void logAndTrace(String msg) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + Trace.instant(Trace.TRACE_TAG_VIEW, mTag + "-" + msg); + } + Log.d(mTag, msg); + } } diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index cc612ed93b2f..6888b50bb744 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -1,10 +1,12 @@ package: "android.view.accessibility" +# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. + flag { + name: "a11y_overlay_callbacks" namespace: "accessibility" - name: "force_invert_color" - description: "Enable force force-dark for smart inversion and dark theme everywhere" - bug: "282821643" + description: "Whether to allow the passing of result callbacks when attaching a11y overlays." + bug: "304478691" } flag { @@ -15,8 +17,8 @@ flag { } flag { - name: "a11y_overlay_callbacks" namespace: "accessibility" - description: "Whether to allow the passing of result callbacks when attaching a11y overlays." - bug: "304478691" + name: "force_invert_color" + description: "Enable force force-dark for smart inversion and dark theme everywhere" + bug: "282821643" } diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index 77e150239803..b1d22e069d9d 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -81,6 +81,11 @@ public class SystemUiSystemPropertiesFlags { public static final Flag PROPAGATE_CHANNEL_UPDATES_TO_CONVERSATIONS = releasedFlag( "persist.sysui.notification.propagate_channel_updates_to_conversations"); + // TODO: b/291907312 - remove feature flags + /** Gating the NMS->NotificationAttentionHelper buzzBeepBlink refactor */ + public static final Flag ENABLE_ATTENTION_HELPER_REFACTOR = devFlag( + "persist.debug.sysui.notification.enable_attention_helper_refactor"); + // TODO b/291899544: for released flags, use resource config values /** Value used by polite notif. feature */ public static final Flag NOTIF_COOLDOWN_T1 = devFlag( diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 04fd70a96201..3496994fe173 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -10090,6 +10090,15 @@ <!-- Perceptual luminance of a color, in accessibility friendly color space. From 0 to 100. --> <attr name="lStar" format="float"/> + <!-- The attributes of the {@code <locale-config>} tag. --> + <!-- @FlaggedApi("android.content.res.default_locale") --> + <declare-styleable name="LocaleConfig"> + <!-- The <a href="https://www.rfc-editor.org/rfc/bcp/bcp47.txt">IETF BCP47 language tag</a> + the strings in values/strings.xml (the default strings in the directory with no locale + qualifier) are in. --> + <attr name="defaultLocale" format="string"/> + </declare-styleable> + <!-- The attributes of the {@code <locale>} tag within {@code <locale-config>}. --> <declare-styleable name="LocaleConfig_Locale"> <!-- The <a href="https://www.rfc-editor.org/rfc/bcp/bcp47.txt">IETF BCP47 language tag</a> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index adc7fe0922aa..4cd4f638e191 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -110,6 +110,8 @@ <eat-comment/> <staging-public-group type="attr" first-id="0x01bd0000"> + <!-- @FlaggedApi("android.content.res.default_locale") --> + <public name="defaultLocale"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java index 531404bffd50..d10cf1691408 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionTests.java @@ -62,4 +62,24 @@ public class ClientTransactionTests { verify(callback2, times(1)).preExecute(clientTransactionHandler); verify(stateRequest, times(1)).preExecute(clientTransactionHandler); } + + @Test + public void testPreExecuteTransactionItems() { + final ClientTransactionItem callback1 = mock(ClientTransactionItem.class); + final ClientTransactionItem callback2 = mock(ClientTransactionItem.class); + final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class); + final ClientTransactionHandler clientTransactionHandler = + mock(ClientTransactionHandler.class); + + final ClientTransaction transaction = ClientTransaction.obtain(null /* client */); + transaction.addTransactionItem(callback1); + transaction.addTransactionItem(callback2); + transaction.addTransactionItem(stateRequest); + + transaction.preExecute(clientTransactionHandler); + + verify(callback1, times(1)).preExecute(clientTransactionHandler); + verify(callback2, times(1)).preExecute(clientTransactionHandler); + verify(stateRequest, times(1)).preExecute(clientTransactionHandler); + } } diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java index 44a4d580dbc0..f2b0f2e622b8 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionExecutorTests.java @@ -247,6 +247,31 @@ public class TransactionExecutorTests { } @Test + public void testExecuteTransactionItems_transactionResolution() { + ClientTransactionItem callback1 = mock(ClientTransactionItem.class); + when(callback1.getPostExecutionState()).thenReturn(UNDEFINED); + ClientTransactionItem callback2 = mock(ClientTransactionItem.class); + when(callback2.getPostExecutionState()).thenReturn(UNDEFINED); + ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class); + IBinder token = mock(IBinder.class); + when(stateRequest.getActivityToken()).thenReturn(token); + when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class)); + + ClientTransaction transaction = ClientTransaction.obtain(null /* client */); + transaction.addTransactionItem(callback1); + transaction.addTransactionItem(callback2); + transaction.addTransactionItem(stateRequest); + + transaction.preExecute(mTransactionHandler); + mExecutor.execute(transaction); + + InOrder inOrder = inOrder(mTransactionHandler, callback1, callback2, stateRequest); + inOrder.verify(callback1).execute(eq(mTransactionHandler), any()); + inOrder.verify(callback2).execute(eq(mTransactionHandler), any()); + inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any()); + } + + @Test public void testDoNotLaunchDestroyedActivity() { final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>(); when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed); @@ -279,12 +304,43 @@ public class TransactionExecutorTests { } @Test + public void testExecuteTransactionItems_doNotLaunchDestroyedActivity() { + final Map<IBinder, DestroyActivityItem> activitiesToBeDestroyed = new ArrayMap<>(); + when(mTransactionHandler.getActivitiesToBeDestroyed()).thenReturn(activitiesToBeDestroyed); + // Assume launch transaction is still in queue, so there is no client record. + when(mTransactionHandler.getActivityClient(any())).thenReturn(null); + + // An incoming destroy transaction enters binder thread (preExecute). + final IBinder token = mock(IBinder.class); + final ClientTransaction destroyTransaction = ClientTransaction.obtain(null /* client */); + destroyTransaction.addTransactionItem( + DestroyActivityItem.obtain(token, false /* finished */, 0 /* configChanges */)); + destroyTransaction.preExecute(mTransactionHandler); + // The activity should be added to to-be-destroyed container. + assertEquals(1, activitiesToBeDestroyed.size()); + + // A previous queued launch transaction runs on main thread (execute). + final ClientTransaction launchTransaction = ClientTransaction.obtain(null /* client */); + final LaunchActivityItem launchItem = + spy(new LaunchActivityItemBuilder().setActivityToken(token).build()); + launchTransaction.addTransactionItem(launchItem); + mExecutor.execute(launchTransaction); + + // The launch transaction should not be executed because its token is in the + // to-be-destroyed container. + verify(launchItem, never()).execute(any(), any()); + + // After the destroy transaction has been executed, the token should be removed. + mExecutor.execute(destroyTransaction); + assertTrue(activitiesToBeDestroyed.isEmpty()); + } + + @Test public void testActivityResultRequiredStateResolution() { when(mTransactionHandler.getActivity(any())).thenReturn(mock(Activity.class)); PostExecItem postExecItem = new PostExecItem(ON_RESUME); - IBinder token = mock(IBinder.class); ClientTransaction transaction = ClientTransaction.obtain(null /* client */); transaction.addCallback(postExecItem); @@ -300,6 +356,26 @@ public class TransactionExecutorTests { } @Test + public void testExecuteTransactionItems_activityResultRequiredStateResolution() { + when(mTransactionHandler.getActivity(any())).thenReturn(mock(Activity.class)); + + PostExecItem postExecItem = new PostExecItem(ON_RESUME); + + ClientTransaction transaction = ClientTransaction.obtain(null /* client */); + transaction.addTransactionItem(postExecItem); + + // Verify resolution that should get to onPause + mClientRecord.setState(ON_RESUME); + mExecutor.executeTransactionItems(transaction); + verify(mExecutor).cycleToPath(eq(mClientRecord), eq(ON_PAUSE), eq(transaction)); + + // Verify resolution that should get to onStart + mClientRecord.setState(ON_STOP); + mExecutor.executeTransactionItems(transaction); + verify(mExecutor).cycleToPath(eq(mClientRecord), eq(ON_START), eq(transaction)); + } + + @Test public void testClosestStateResolutionForSameState() { final int[] allStates = new int[] { ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY}; @@ -444,6 +520,18 @@ public class TransactionExecutorTests { mExecutor.executeCallbacks(transaction); } + @Test(expected = IllegalArgumentException.class) + public void testExecuteTransactionItems_activityItemNullRecordThrowsException() { + final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class); + when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED); + final IBinder token = mock(IBinder.class); + final ClientTransaction transaction = ClientTransaction.obtain(null /* client */); + transaction.addTransactionItem(activityItem); + when(mTransactionHandler.getActivityClient(token)).thenReturn(null); + + mExecutor.executeTransactionItems(transaction); + } + @Test public void testActivityItemExecute() { final IBinder token = mock(IBinder.class); @@ -464,6 +552,26 @@ public class TransactionExecutorTests { inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any()); } + @Test + public void testExecuteTransactionItems_activityItemExecute() { + final IBinder token = mock(IBinder.class); + final ClientTransaction transaction = ClientTransaction.obtain(null /* client */); + final ActivityTransactionItem activityItem = mock(ActivityTransactionItem.class); + when(activityItem.getPostExecutionState()).thenReturn(UNDEFINED); + when(activityItem.getActivityToken()).thenReturn(token); + transaction.addTransactionItem(activityItem); + final ActivityLifecycleItem stateRequest = mock(ActivityLifecycleItem.class); + transaction.addTransactionItem(stateRequest); + when(stateRequest.getActivityToken()).thenReturn(token); + when(mTransactionHandler.getActivity(token)).thenReturn(mock(Activity.class)); + + mExecutor.execute(transaction); + + final InOrder inOrder = inOrder(activityItem, stateRequest); + inOrder.verify(activityItem).execute(eq(mTransactionHandler), eq(mClientRecord), any()); + inOrder.verify(stateRequest).execute(eq(mTransactionHandler), eq(mClientRecord), any()); + } + private static int[] shuffledArray(int[] inputArray) { final List<Integer> list = Arrays.stream(inputArray).boxed().collect(Collectors.toList()); Collections.shuffle(list); diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java index 7d047c93520f..4aa62c503a41 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java @@ -285,6 +285,10 @@ public class TransactionParcelTests { 78 /* configChanges */); ClientTransaction transaction = ClientTransaction.obtain(null /* client */); + transaction.addTransactionItem(callback1); + transaction.addTransactionItem(callback2); + transaction.addTransactionItem(lifecycleRequest); + transaction.addCallback(callback1); transaction.addCallback(callback2); transaction.setLifecycleStateRequest(lifecycleRequest); diff --git a/core/tests/coretests/src/android/os/LocaleListTest.java b/core/tests/coretests/src/android/os/LocaleListTest.java index 1f00a7a12c54..88fc8267f5b2 100644 --- a/core/tests/coretests/src/android/os/LocaleListTest.java +++ b/core/tests/coretests/src/android/os/LocaleListTest.java @@ -81,4 +81,49 @@ public class LocaleListTest extends TestCase { // restore the original values LocaleList.setDefault(originalLocaleList, originalLocaleIndex); } + + @SmallTest + public void testIntersection() { + LocaleList localesWithN = new LocaleList( + Locale.ENGLISH, + Locale.FRENCH, + Locale.GERMAN, + Locale.ITALIAN, + Locale.JAPANESE, + Locale.KOREAN, + Locale.CHINESE, + Locale.SIMPLIFIED_CHINESE, + Locale.TRADITIONAL_CHINESE, + Locale.FRANCE, + Locale.GERMANY, + Locale.JAPAN, + Locale.CANADA, + Locale.CANADA_FRENCH); + LocaleList localesWithE = new LocaleList( + Locale.ENGLISH, + Locale.FRENCH, + Locale.GERMAN, + Locale.JAPANESE, + Locale.KOREAN, + Locale.CHINESE, + Locale.SIMPLIFIED_CHINESE, + Locale.TRADITIONAL_CHINESE, + Locale.FRANCE, + Locale.GERMANY, + Locale.CANADA_FRENCH); + LocaleList localesWithNAndE = new LocaleList( + Locale.ENGLISH, + Locale.FRENCH, + Locale.GERMAN, + Locale.JAPANESE, + Locale.KOREAN, + Locale.CHINESE, + Locale.SIMPLIFIED_CHINESE, + Locale.TRADITIONAL_CHINESE, + Locale.FRANCE, + Locale.GERMANY, + Locale.CANADA_FRENCH); + + assertEquals(localesWithNAndE, new LocaleList(localesWithE.getIntersection(localesWithN))); + } } diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java index 517aeae53784..0855268411eb 100644 --- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java +++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java @@ -20,8 +20,6 @@ import static android.service.notification.NotificationListenerService.Ranking.U import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE; -import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM; - import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; @@ -42,16 +40,12 @@ import android.content.pm.ShortcutInfo; import android.os.Bundle; import android.os.Parcel; import android.os.SharedMemory; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableContext; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; -import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag; -import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver; - -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -71,6 +65,9 @@ public class NotificationRankingUpdateTest { private NotificationChannel mNotificationChannel; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + // TODO(b/284297289): remove this flag set once resolved. @Parameterized.Parameters(name = "rankingUpdateAshmem={0}") public static Boolean[] getRankingUpdateAshmem() { @@ -424,30 +421,11 @@ public class NotificationRankingUpdateTest { mNotificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "test channel", NotificationManager.IMPORTANCE_DEFAULT); - SystemUiSystemPropertiesFlags.TEST_RESOLVER = new FlagResolver() { - @Override - public boolean isEnabled(Flag flag) { - if (flag.mSysPropKey.equals(RANKING_UPDATE_ASHMEM.mSysPropKey)) { - return mRankingUpdateAshmem; - } - return new SystemUiSystemPropertiesFlags.DebugResolver().isEnabled(flag); - } - - @Override - public int getIntValue(Flag flag) { - return 0; - } - - @Override - public String getStringValue(Flag flag) { - return null; - } - }; - } - - @After - public void tearDown() { - SystemUiSystemPropertiesFlags.TEST_RESOLVER = null; + if (mRankingUpdateAshmem) { + mSetFlagsRule.enableFlags(Flags.FLAG_RANKING_UPDATE_ASHMEM); + } else { + mSetFlagsRule.disableFlags(Flags.FLAG_RANKING_UPDATE_ASHMEM); + } } /** @@ -497,8 +475,7 @@ public class NotificationRankingUpdateTest { parcel.setDataPosition(0); NotificationRankingUpdate nru1 = NotificationRankingUpdate.CREATOR.createFromParcel(parcel); // The rankingUpdate file descriptor is only non-null in the new path. - if (SystemUiSystemPropertiesFlags.getResolver().isEnabled( - SystemUiSystemPropertiesFlags.NotificationFlags.RANKING_UPDATE_ASHMEM)) { + if (Flags.rankingUpdateAshmem()) { assertTrue(nru1.isFdNotNullAndClosed()); } detailedAssertEquals(nru, nru1); @@ -636,7 +613,7 @@ public class NotificationRankingUpdateTest { @Test public void testRankingUpdate_writesSmartActionToParcel() { - if (!mRankingUpdateAshmem) { + if (!Flags.rankingUpdateAshmem()) { return; } ArrayList<Notification.Action> actions = new ArrayList<>(); @@ -674,7 +651,7 @@ public class NotificationRankingUpdateTest { @Test public void testRankingUpdate_handlesEmptySmartActionList() { - if (!mRankingUpdateAshmem) { + if (!Flags.rankingUpdateAshmem()) { return; } ArrayList<Notification.Action> actions = new ArrayList<>(); @@ -697,7 +674,7 @@ public class NotificationRankingUpdateTest { @Test public void testRankingUpdate_handlesNullSmartActionList() { - if (!mRankingUpdateAshmem) { + if (!Flags.rankingUpdateAshmem()) { return; } NotificationListenerService.Ranking ranking = diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig index e030dad6bf14..9a0a22a08a0c 100644 --- a/graphics/java/android/framework_graphics.aconfig +++ b/graphics/java/android/framework_graphics.aconfig @@ -2,7 +2,7 @@ package: "com.android.graphics.flags" flag { name: "exact_compute_bounds" - namespace: "framework_graphics" + namespace: "core_graphics" description: "Add a function without unused exact param for computeBounds." bug: "304478551" }
\ No newline at end of file diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 7f226939ffaa..d056248273b2 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -785,7 +785,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( has_locale = true; } - // if we don't have a result yet + // if we don't have a result yet if (!final_result || // or this config is better before the locale than the existing result result->config.isBetterThanBeforeLocale(final_result->config, desired_config) || @@ -863,9 +863,12 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( // We can skip calling ResTable_config::match() if the caller does not care for the // configuration to match or if we're using the list of types that have already had their - // configuration matched. + // configuration matched. The exception to this is when the user has multiple locales set + // because the filtered list will then have values from multiple locales and we will need to + // call match() to make sure the current entry matches the config we are currently checking. const ResTable_config& this_config = type_entry->config; - if (!(use_filtered || ignore_configuration || this_config.match(desired_config))) { + if (!((use_filtered && (configurations_.size() == 1)) + || ignore_configuration || this_config.match(desired_config))) { continue; } diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index b00fc699e515..14602ef926d3 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -139,6 +139,7 @@ CanvasContext::~CanvasContext() { mRenderNodes.clear(); mRenderThread.cacheManager().unregisterCanvasContext(this); mRenderThread.renderState().removeContextCallback(this); + mHintSessionWrapper->destroy(); } void CanvasContext::addRenderNode(RenderNode* node, bool placeFront) { diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp index 1c3399a6650d..2362331aca26 100644 --- a/libs/hwui/renderthread/HintSessionWrapper.cpp +++ b/libs/hwui/renderthread/HintSessionWrapper.cpp @@ -158,7 +158,6 @@ void HintSessionWrapper::sendLoadResetHint() { void HintSessionWrapper::sendLoadIncreaseHint() { if (!init()) return; mBinding->sendHint(mHintSession, static_cast<int32_t>(SessionHint::CPU_LOAD_UP)); - mLastFrameNotification = systemTime(); } bool HintSessionWrapper::alive() { diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp index a14ae1cc46ec..10a740a1f803 100644 --- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp +++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp @@ -259,6 +259,31 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesAfterAsyncCreationFinishe TEST_F(HintSessionWrapperTests, delayedDeletionDoesNotKillReusedSession) { EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(0); + EXPECT_CALL(*sMockBinding, fakeReportActualWorkDuration(sessionPtr, 5_ms)).Times(1); + + mWrapper->init(); + waitForWrapperReady(); + // Init a second time just to grab the wrapper from the promise + mWrapper->init(); + EXPECT_EQ(mWrapper->alive(), true); + + // First schedule the deletion + scheduleDelayedDestroyManaged(); + + // Then, report an actual duration + mWrapper->reportActualWorkDuration(5_ms); + + // Then, run the delayed deletion after sending the update + allowDelayedDestructionToStart(); + waitForDelayedDestructionToFinish(); + + // Ensure it didn't close within the timeframe of the test + Mock::VerifyAndClearExpectations(sMockBinding.get()); + EXPECT_EQ(mWrapper->alive(), true); +} + +TEST_F(HintSessionWrapperTests, loadUpDoesNotResetDeletionTimer) { + EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); EXPECT_CALL(*sMockBinding, fakeSendHint(sessionPtr, static_cast<int32_t>(SessionHint::CPU_LOAD_UP))) .Times(1); @@ -272,16 +297,46 @@ TEST_F(HintSessionWrapperTests, delayedDeletionDoesNotKillReusedSession) { // First schedule the deletion scheduleDelayedDestroyManaged(); - // Then, send a hint to update the timestamp + // Then, send a load_up hint mWrapper->sendLoadIncreaseHint(); // Then, run the delayed deletion after sending the update allowDelayedDestructionToStart(); waitForDelayedDestructionToFinish(); - // Ensure it didn't close within the timeframe of the test + // Ensure it closed within the timeframe of the test Mock::VerifyAndClearExpectations(sMockBinding.get()); + EXPECT_EQ(mWrapper->alive(), false); +} + +TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct) { + EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1); + + mWrapper->init(); + waitForWrapperReady(); + // Init a second time just to grab the wrapper from the promise + mWrapper->init(); EXPECT_EQ(mWrapper->alive(), true); + + // First schedule the deletion + scheduleDelayedDestroyManaged(); + + // Then, kill the session + mWrapper->destroy(); + + // Verify it died + Mock::VerifyAndClearExpectations(sMockBinding.get()); + EXPECT_EQ(mWrapper->alive(), false); + + EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(0); + + // Then, run the delayed deletion after manually killing the session + allowDelayedDestructionToStart(); + waitForDelayedDestructionToFinish(); + + // Ensure it didn't close again and is still dead + Mock::VerifyAndClearExpectations(sMockBinding.get()); + EXPECT_EQ(mWrapper->alive(), false); } } // namespace android::uirenderer::renderthread
\ No newline at end of file diff --git a/media/Android.bp b/media/Android.bp index f69dd3cc3206..349340804f1e 100644 --- a/media/Android.bp +++ b/media/Android.bp @@ -23,6 +23,10 @@ aidl_interface { name: "soundtrigger_middleware-aidl", unstable: true, local_include_dir: "aidl", + defaults: [ + "latest_android_media_audio_common_types_import_interface", + "latest_android_media_soundtrigger_types_import_interface", + ], backend: { java: { sdk_version: "module_current", @@ -32,8 +36,6 @@ aidl_interface { "aidl/android/media/soundtrigger_middleware/*.aidl", ], imports: [ - "android.media.audio.common.types-V2", - "android.media.soundtrigger.types-V1", "media_permission-aidl", ], } diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 03f7c9968a1d..de73b77b21a3 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -32,22 +32,6 @@ ] }, { - "name": "SystemUIGoogleScreenshotTests", - "options": [ - { - "exclude-annotation": "org.junit.Ignore" - }, - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "exclude-annotation": "android.platform.test.annotations.Postsubmit" - } - ], - // The test doesn't run on AOSP Cuttlefish - "keywords": ["internal"] - }, - { // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+) "name": "SystemUIGoogleBiometricsScreenshotTests", "options": [ diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig index 0f55f35adc4e..eadcd7c27a18 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig +++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig @@ -1,15 +1,17 @@ package: "com.android.systemui.accessibility.accessibilitymenu" -flag { - name: "a11y_menu_settings_back_button_fix_and_large_button_sizing" - namespace: "accessibility" - description: "Provides/restores back button functionality for the a11yMenu settings page. Also, fixes sizing problems with large shortcut buttons." - bug: "298467628" -} +# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. flag { name: "a11y_menu_hide_before_taking_action" namespace: "accessibility" description: "Hides the AccessibilityMenuService UI before taking action instead of after." bug: "292020123" -}
\ No newline at end of file +} + +flag { + name: "a11y_menu_settings_back_button_fix_and_large_button_sizing" + namespace: "accessibility" + description: "Provides/restores back button functionality for the a11yMenu settings page. Also, fixes sizing problems with large shortcut buttons." + bug: "298467628" +} diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index 8841967b1535..bcf1535b94fa 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -1,5 +1,7 @@ package: "com.android.systemui" +# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. + flag { name: "floating_menu_overlaps_nav_bars_flag" namespace: "accessibility" diff --git a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt index 4ed78b3b95ed..33024f764710 100644 --- a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt +++ b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/CommunalGridLayout.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.layout.ui.compose +import android.util.SizeF import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -54,7 +55,14 @@ fun CommunalGridLayout( Row( modifier = Modifier.height(layoutConfig.cardHeight(cardInfo.size)), ) { - cardInfo.card.Content(Modifier.fillMaxSize()) + cardInfo.card.Content( + modifier = Modifier.fillMaxSize(), + size = + SizeF( + layoutConfig.cardWidth.value, + layoutConfig.cardHeight(cardInfo.size).value, + ), + ) } } } diff --git a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt index ac8aa67fa4bf..4b2a156c1dbd 100644 --- a/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt +++ b/packages/SystemUI/communal/layout/src/com/android/systemui/communal/layout/ui/compose/config/CommunalGridLayoutCard.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.layout.ui.compose.config +import android.util.SizeF import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -26,8 +27,11 @@ abstract class CommunalGridLayoutCard { * * To host non-Compose views, see * https://developer.android.com/jetpack/compose/migrate/interoperability-apis/views-in-compose. + * + * @param size The size given to the card. Content of the card should fill all this space, given + * that margins and paddings have been taken care of by the layout. */ - @Composable abstract fun Content(modifier: Modifier) + @Composable abstract fun Content(modifier: Modifier, size: SizeF) /** * Sizes supported by the card. diff --git a/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt b/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt index fdf65f5d5cc7..c1974caa5628 100644 --- a/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt +++ b/packages/SystemUI/communal/layout/tests/src/com/android/systemui/communal/layout/CommunalLayoutEngineTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.communal.layout +import android.util.SizeF import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -91,7 +92,7 @@ class CommunalLayoutEngineTest { override val supportedSizes = listOf(size) @Composable - override fun Content(modifier: Modifier) { + override fun Content(modifier: Modifier, size: SizeF) { Card(modifier = modifier, content = {}) } } diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index c6e429a79e86..ddd1c67bd5fa 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -72,6 +72,10 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } + override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View { + throwComposeUnavailableError() + } + private fun throwComposeUnavailableError(): Nothing { error( "Compose is not available. Make sure to check isComposeAvailable() before calling any" + diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 1722685f4287..eeda6c63b68f 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -30,6 +30,7 @@ import com.android.compose.theme.PlatformTheme import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider +import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalHub import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.people.ui.compose.PeopleScreen @@ -104,6 +105,12 @@ object ComposeFacade : BaseComposeFacade { } } + override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View { + return ComposeView(context).apply { + setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } } + } + } + // TODO(b/298525212): remove once Compose exposes window inset bounds. private fun displayCutoutFromWindowInsets( scope: CoroutineScope, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt new file mode 100644 index 000000000000..46d418a03c00 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -0,0 +1,123 @@ +package com.android.systemui.communal.ui.compose + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.Edge +import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.SceneTransitionLayout +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.transitions +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel + +object Scenes { + val Blank = SceneKey(name = "blank") + val Communal = SceneKey(name = "communal") +} + +object Communal { + object Elements { + val Content = ElementKey("CommunalContent") + } +} + +val sceneTransitions = transitions { + from(Scenes.Blank, to = Scenes.Communal) { + spec = tween(durationMillis = 500) + + translate(Communal.Elements.Content, Edge.Right) + fade(Communal.Elements.Content) + } +} + +/** + * View containing a [SceneTransitionLayout] that shows the communal UI and handles transitions. + * + * This is a temporary container to allow the communal UI to use [SceneTransitionLayout] for gesture + * handling and transitions before the full Flexiglass layout is ready. + */ +@Composable +fun CommunalContainer(modifier: Modifier = Modifier, viewModel: CommunalViewModel) { + val (currentScene, setCurrentScene) = remember { mutableStateOf(Scenes.Blank) } + + // Failsafe to hide the whole SceneTransitionLayout in case of bugginess. + var showSceneTransitionLayout by remember { mutableStateOf(true) } + if (!showSceneTransitionLayout) { + return + } + + SceneTransitionLayout( + modifier = modifier.fillMaxSize(), + currentScene = currentScene, + onChangeScene = setCurrentScene, + transitions = sceneTransitions, + ) { + scene(Scenes.Blank, userActions = mapOf(Swipe.Left to Scenes.Communal)) { + BlankScene { showSceneTransitionLayout = false } + } + + scene( + Scenes.Communal, + userActions = mapOf(Swipe.Right to Scenes.Blank), + ) { + CommunalScene(viewModel, modifier = modifier) + } + } +} + +/** + * Blank scene that shows over keyguard/dream. This scene will eventually show nothing at all and is + * only used to allow for transitions to the communal scene. + */ +@Composable +private fun BlankScene( + modifier: Modifier = Modifier, + hideSceneTransitionLayout: () -> Unit, +) { + Box(modifier.fillMaxSize()) { + Column( + Modifier.fillMaxHeight() + .width(100.dp) + .align(Alignment.CenterEnd) + .background(Color(0x55e9f2eb)), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Default scene") + + IconButton(onClick = hideSceneTransitionLayout) { + Icon(Icons.Filled.Close, contentDescription = "Close button") + } + } + } +} + +/** Scene containing the glanceable hub UI. */ +@Composable +private fun SceneScope.CommunalScene( + viewModel: CommunalViewModel, + modifier: Modifier = Modifier, +) { + Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 3d827fb5c9a6..b8fb26406801 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1,5 +1,8 @@ package com.android.systemui.communal.ui.compose +import android.appwidget.AppWidgetHostView +import android.os.Bundle +import android.util.SizeF import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -12,9 +15,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.integerResource +import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.communal.layout.ui.compose.CommunalGridLayout import com.android.systemui.communal.layout.ui.compose.config.CommunalGridLayoutCard import com.android.systemui.communal.layout.ui.compose.config.CommunalGridLayoutConfig +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.res.R @@ -24,6 +30,7 @@ fun CommunalHub( viewModel: CommunalViewModel, ) { val showTutorial by viewModel.showTutorialContent.collectAsState(initial = false) + val widgetContent by viewModel.widgetContent.collectAsState(initial = emptyList()) Box( modifier = modifier.fillMaxSize().background(Color.White), ) { @@ -36,7 +43,7 @@ fun CommunalHub( gridHeight = dimensionResource(R.dimen.communal_grid_height), gridColumnsPerCard = integerResource(R.integer.communal_grid_columns_per_card), ), - communalCards = if (showTutorial) tutorialContent else emptyList(), + communalCards = if (showTutorial) tutorialContent else widgetContent.map(::contentCard), ) } } @@ -58,8 +65,37 @@ private fun tutorialCard(size: CommunalGridLayoutCard.Size): CommunalGridLayoutC override val supportedSizes = listOf(size) @Composable - override fun Content(modifier: Modifier) { + override fun Content(modifier: Modifier, size: SizeF) { Card(modifier = modifier, content = {}) } } } + +private fun contentCard(model: CommunalContentUiModel): CommunalGridLayoutCard { + return object : CommunalGridLayoutCard() { + override val supportedSizes = listOf(convertToCardSize(model.size)) + override val priority = model.priority + + @Composable + override fun Content(modifier: Modifier, size: SizeF) { + AndroidView( + modifier = modifier, + factory = { + model.view.apply { + if (this is AppWidgetHostView) { + updateAppWidgetSize(Bundle(), listOf(size)) + } + } + }, + ) + } + } +} + +private fun convertToCardSize(size: CommunalContentSize): CommunalGridLayoutCard.Size { + return when (size) { + CommunalContentSize.FULL -> CommunalGridLayoutCard.Size.FULL + CommunalContentSize.HALF -> CommunalGridLayoutCard.Size.HALF + CommunalContentSize.THIRD -> CommunalGridLayoutCard.Size.THIRD + } +} diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index acee4258b64d..8957903b29d4 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -83,6 +83,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + <!-- Placeholder for the communal UI that will be replaced if the feature is enabled. --> + <ViewStub + android:id="@+id/communal_ui_stub" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + <include layout="@layout/brightness_mirror_container" /> <com.android.systemui.scrim.ScrimView diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt index db46ccf6a827..80f70a0cd2f2 100644 --- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt +++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/shared/model/BiometricModalities.kt @@ -33,6 +33,10 @@ data class BiometricModalities( val hasFingerprint: Boolean get() = fingerprintProperties != null + /** If SFPS authentication is available. */ + val hasSfps: Boolean + get() = hasFingerprint && fingerprintProperties!!.isAnySidefpsType + /** If fingerprint authentication is available (and [faceProperties] is non-null). */ val hasFace: Boolean get() = faceProperties != null diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt deleted file mode 100644 index 3f2da5e144c5..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceIconController.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.Log -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState - -private const val TAG = "AuthBiometricFaceIconController" - -/** Face only icon animator for BiometricPrompt. */ -class AuthBiometricFaceIconController( - context: Context, - iconView: LottieAnimationView -) : AuthIconController(context, iconView) { - - // false = dark to light, true = light to dark - private var lastPulseLightToDark = false - - private var state: BiometricState = BiometricState.STATE_IDLE - - init { - val size = context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) - iconView.layoutParams.width = size - iconView.layoutParams.height = size - showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light) - } - - private fun startPulsing() { - lastPulseLightToDark = false - animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true) - } - - private fun pulseInNextDirection() { - val iconRes = if (lastPulseLightToDark) { - R.drawable.face_dialog_pulse_dark_to_light - } else { - R.drawable.face_dialog_pulse_light_to_dark - } - animateIcon(iconRes, true /* repeat */) - lastPulseLightToDark = !lastPulseLightToDark - } - - override fun handleAnimationEnd(drawable: Drawable) { - if (state == BiometricState.STATE_AUTHENTICATING || state == BiometricState.STATE_HELP) { - pulseInNextDirection() - } - } - - override fun updateIcon(oldState: BiometricState, newState: BiometricState) { - val lastStateIsErrorIcon = (oldState == BiometricState.STATE_ERROR || oldState == BiometricState.STATE_HELP) - if (newState == BiometricState.STATE_AUTHENTICATING_ANIMATING_IN) { - showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_authenticating - ) - } else if (newState == BiometricState.STATE_AUTHENTICATING) { - startPulsing() - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_authenticating - ) - } else if (oldState == BiometricState.STATE_PENDING_CONFIRMATION && newState == BiometricState.STATE_AUTHENTICATED) { - animateIconOnce(R.drawable.face_dialog_dark_to_checkmark) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_confirmed - ) - } else if (lastStateIsErrorIcon && newState == BiometricState.STATE_IDLE) { - animateIconOnce(R.drawable.face_dialog_error_to_idle) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_idle - ) - } else if (lastStateIsErrorIcon && newState == BiometricState.STATE_AUTHENTICATED) { - animateIconOnce(R.drawable.face_dialog_dark_to_checkmark) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_authenticated - ) - } else if (newState == BiometricState.STATE_ERROR && oldState != BiometricState.STATE_ERROR) { - animateIconOnce(R.drawable.face_dialog_dark_to_error) - iconView.contentDescription = context.getString( - R.string.keyguard_face_failed - ) - } else if (oldState == BiometricState.STATE_AUTHENTICATING && newState == BiometricState.STATE_AUTHENTICATED) { - animateIconOnce(R.drawable.face_dialog_dark_to_checkmark) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_authenticated - ) - } else if (newState == BiometricState.STATE_PENDING_CONFIRMATION) { - animateIconOnce(R.drawable.face_dialog_wink_from_dark) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_authenticated - ) - } else if (newState == BiometricState.STATE_IDLE) { - showStaticDrawable(R.drawable.face_dialog_idle_static) - iconView.contentDescription = context.getString( - R.string.biometric_dialog_face_icon_description_idle - ) - } else { - Log.w(TAG, "Unhandled state: $newState") - } - state = newState - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt deleted file mode 100644 index 09eabf2aa430..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintAndFaceIconController.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 - -import android.annotation.RawRes -import android.content.Context -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATED -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_ERROR -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_HELP -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - -/** Face/Fingerprint combined icon animator for BiometricPrompt. */ -open class AuthBiometricFingerprintAndFaceIconController( - context: Context, - iconView: LottieAnimationView, - iconViewOverlay: LottieAnimationView, -) : AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) { - - override val actsAsConfirmButton: Boolean = true - - override fun shouldAnimateIconViewForTransition( - oldState: BiometricState, - newState: BiometricState - ): Boolean = when (newState) { - STATE_PENDING_CONFIRMATION -> true - else -> super.shouldAnimateIconViewForTransition(oldState, newState) - } - - @RawRes - override fun getAnimationForTransition( - oldState: BiometricState, - newState: BiometricState - ): Int? = when (newState) { - STATE_AUTHENTICATED -> { - if (oldState == STATE_PENDING_CONFIRMATION) { - R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie - } else { - super.getAnimationForTransition(oldState, newState) - } - } - STATE_PENDING_CONFIRMATION -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - R.raw.fingerprint_dialogue_error_to_unlock_lottie - } else { - R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie - } - } - else -> super.getAnimationForTransition(oldState, newState) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt deleted file mode 100644 index 0ad3848299f9..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconController.kt +++ /dev/null @@ -1,342 +0,0 @@ -/* - * 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 - -import android.annotation.RawRes -import android.content.Context -import android.content.Context.FINGERPRINT_SERVICE -import android.hardware.fingerprint.FingerprintManager -import android.view.DisplayInfo -import android.view.Surface -import android.view.View -import androidx.annotation.VisibleForTesting -import com.airbnb.lottie.LottieAnimationView -import com.android.settingslib.widget.LottieColorUtils -import com.android.systemui.res.R -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATED -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATING -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_AUTHENTICATING_ANIMATING_IN -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_ERROR -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_HELP -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_IDLE -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - - -/** Fingerprint only icon animator for BiometricPrompt. */ -open class AuthBiometricFingerprintIconController( - context: Context, - iconView: LottieAnimationView, - protected val iconViewOverlay: LottieAnimationView -) : AuthIconController(context, iconView) { - - private val isSideFps: Boolean - private val isReverseDefaultRotation = - context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) - - var iconLayoutParamSize: Pair<Int, Int> = Pair(1, 1) - set(value) { - if (field == value) { - return - } - iconViewOverlay.layoutParams.width = value.first - iconViewOverlay.layoutParams.height = value.second - iconView.layoutParams.width = value.first - iconView.layoutParams.height = value.second - field = value - } - - init { - iconLayoutParamSize = Pair(context.resources.getDimensionPixelSize( - R.dimen.biometric_dialog_fingerprint_icon_width), - context.resources.getDimensionPixelSize( - R.dimen.biometric_dialog_fingerprint_icon_height)) - isSideFps = - (context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager?)?.let { fpm -> - fpm.sensorPropertiesInternal.any { it.isAnySidefpsType } - } ?: false - preloadAssets(context) - val displayInfo = DisplayInfo() - context.display?.getDisplayInfo(displayInfo) - if (isSideFps && getRotationFromDefault(displayInfo.rotation) == Surface.ROTATION_180) { - iconView.rotation = 180f - } - } - - private fun updateIconSideFps(lastState: BiometricState, newState: BiometricState) { - val displayInfo = DisplayInfo() - context.display?.getDisplayInfo(displayInfo) - val rotation = getRotationFromDefault(displayInfo.rotation) - val iconViewOverlayAnimation = - getSideFpsOverlayAnimationForTransition(lastState, newState, rotation) ?: return - - if (!(lastState == STATE_AUTHENTICATING_ANIMATING_IN && newState == STATE_AUTHENTICATING)) { - iconViewOverlay.setAnimation(iconViewOverlayAnimation) - } - - val iconContentDescription = getIconContentDescription(newState) - if (iconContentDescription != null) { - iconView.contentDescription = iconContentDescription - } - - iconView.frame = 0 - iconViewOverlay.frame = 0 - if (shouldAnimateSfpsIconViewForTransition(lastState, newState)) { - iconView.playAnimation() - } - - if (shouldAnimateIconViewOverlayForTransition(lastState, newState)) { - iconViewOverlay.playAnimation() - } - - LottieColorUtils.applyDynamicColors(context, iconView) - LottieColorUtils.applyDynamicColors(context, iconViewOverlay) - } - - private fun updateIconNormal(lastState: BiometricState, newState: BiometricState) { - val icon = getAnimationForTransition(lastState, newState) ?: return - - if (!(lastState == STATE_AUTHENTICATING_ANIMATING_IN && newState == STATE_AUTHENTICATING)) { - iconView.setAnimation(icon) - } - - val iconContentDescription = getIconContentDescription(newState) - if (iconContentDescription != null) { - iconView.contentDescription = iconContentDescription - } - - iconView.frame = 0 - if (shouldAnimateIconViewForTransition(lastState, newState)) { - iconView.playAnimation() - } - LottieColorUtils.applyDynamicColors(context, iconView) - } - - override fun updateIcon(lastState: BiometricState, newState: BiometricState) { - if (isSideFps) { - updateIconSideFps(lastState, newState) - } else { - iconViewOverlay.visibility = View.GONE - updateIconNormal(lastState, newState) - } - } - - @VisibleForTesting - fun getIconContentDescription(newState: BiometricState): CharSequence? { - val id = when (newState) { - STATE_IDLE, - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING, - STATE_AUTHENTICATED -> - if (isSideFps) { - R.string.security_settings_sfps_enroll_find_sensor_message - } else { - R.string.fingerprint_dialog_touch_sensor - } - STATE_PENDING_CONFIRMATION -> - if (isSideFps) { - R.string.security_settings_sfps_enroll_find_sensor_message - } else { - R.string.fingerprint_dialog_authenticated_confirmation - } - STATE_ERROR, - STATE_HELP -> R.string.biometric_dialog_try_again - else -> null - } - return if (id != null) context.getString(id) else null - } - - protected open fun shouldAnimateIconViewForTransition( - oldState: BiometricState, - newState: BiometricState - ) = when (newState) { - STATE_HELP, - STATE_ERROR -> true - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP - STATE_AUTHENTICATED -> true - else -> false - } - - private fun shouldAnimateSfpsIconViewForTransition( - oldState: BiometricState, - newState: BiometricState - ) = when (newState) { - STATE_HELP, - STATE_ERROR -> true - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> - oldState == STATE_ERROR || oldState == STATE_HELP || oldState == STATE_IDLE - STATE_AUTHENTICATED -> true - else -> false - } - - protected open fun shouldAnimateIconViewOverlayForTransition( - oldState: BiometricState, - newState: BiometricState - ) = when (newState) { - STATE_HELP, - STATE_ERROR -> true - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> oldState == STATE_ERROR || oldState == STATE_HELP - STATE_AUTHENTICATED -> true - else -> false - } - - @RawRes - protected open fun getAnimationForTransition( - oldState: BiometricState, - newState: BiometricState - ): Int? { - val id = when (newState) { - STATE_HELP, - STATE_ERROR -> { - R.raw.fingerprint_dialogue_fingerprint_to_error_lottie - } - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - R.raw.fingerprint_dialogue_error_to_fingerprint_lottie - } else { - R.raw.fingerprint_dialogue_fingerprint_to_error_lottie - } - } - STATE_AUTHENTICATED -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - R.raw.fingerprint_dialogue_error_to_success_lottie - } else { - R.raw.fingerprint_dialogue_fingerprint_to_success_lottie - } - } - else -> return null - } - return if (id != null) return id else null - } - - private fun getRotationFromDefault(rotation: Int): Int = - if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation - - @RawRes - private fun getSideFpsOverlayAnimationForTransition( - oldState: BiometricState, - newState: BiometricState, - rotation: Int - ): Int? = when (newState) { - STATE_HELP, - STATE_ERROR -> { - when (rotation) { - Surface.ROTATION_0 -> R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright - else -> R.raw.biometricprompt_fingerprint_to_error_landscape - } - } - STATE_AUTHENTICATING_ANIMATING_IN, - STATE_AUTHENTICATING -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - when (rotation) { - Surface.ROTATION_0 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright - else -> R.raw.biometricprompt_symbol_error_to_fingerprint_landscape - } - } else { - when (rotation) { - Surface.ROTATION_0 -> R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_fingerprint_to_error_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright - else -> R.raw.biometricprompt_fingerprint_to_error_landscape - } - } - } - STATE_AUTHENTICATED -> { - if (oldState == STATE_ERROR || oldState == STATE_HELP) { - when (rotation) { - Surface.ROTATION_0 -> - R.raw.biometricprompt_symbol_error_to_success_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_error_to_success_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_symbol_error_to_success_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright - else -> R.raw.biometricprompt_symbol_error_to_success_landscape - } - } else { - when (rotation) { - Surface.ROTATION_0 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_landscape - Surface.ROTATION_90 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft - Surface.ROTATION_180 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_landscape - Surface.ROTATION_270 -> - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright - else -> R.raw.biometricprompt_symbol_fingerprint_to_success_landscape - } - } - } - else -> null - } - - private fun preloadAssets(context: Context) { - if (isSideFps) { - cacheLottieAssetsInContext( - context, - R.raw.biometricprompt_fingerprint_to_error_landscape, - R.raw.biometricprompt_folded_base_bottomright, - R.raw.biometricprompt_folded_base_default, - R.raw.biometricprompt_folded_base_topleft, - R.raw.biometricprompt_landscape_base, - R.raw.biometricprompt_portrait_base_bottomright, - R.raw.biometricprompt_portrait_base_topleft, - R.raw.biometricprompt_symbol_error_to_fingerprint_landscape, - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright, - R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft, - R.raw.biometricprompt_symbol_error_to_success_landscape, - R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright, - R.raw.biometricprompt_symbol_error_to_success_portrait_topleft, - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright, - R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft, - R.raw.biometricprompt_symbol_fingerprint_to_success_landscape, - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright, - R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft - ) - } else { - cacheLottieAssetsInContext( - context, - R.raw.fingerprint_dialogue_error_to_fingerprint_lottie, - R.raw.fingerprint_dialogue_error_to_success_lottie, - R.raw.fingerprint_dialogue_fingerprint_to_error_lottie, - R.raw.fingerprint_dialogue_fingerprint_to_success_lottie - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt index 054bd082edb7..8d1d90588fc3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2023 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.MainThread @@ -25,7 +41,7 @@ constructor( shadeExpansionCollectorJob = scope.launch { // wait for it to emit true once - shadeInteractorLazy.get().isAnyExpanding.first { it } + shadeInteractorLazy.get().isUserInteracting.first { it } onShadeInteraction.run() } shadeExpansionCollectorJob?.invokeOnCompletion { shadeExpansionCollectorJob = null } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt deleted file mode 100644 index 958213afacdf..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthIconController.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 - -import android.annotation.DrawableRes -import android.content.Context -import android.graphics.drawable.Animatable2 -import android.graphics.drawable.AnimatedVectorDrawable -import android.graphics.drawable.Drawable -import android.util.Log -import com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieCompositionFactory -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState - -private const val TAG = "AuthIconController" - -/** Controller for animating the BiometricPrompt icon/affordance. */ -abstract class AuthIconController( - protected val context: Context, - protected val iconView: LottieAnimationView -) : Animatable2.AnimationCallback() { - - /** If this controller should ignore events and pause. */ - var deactivated: Boolean = false - - /** If the icon view should be treated as an alternate "confirm" button. */ - open val actsAsConfirmButton: Boolean = false - - final override fun onAnimationStart(drawable: Drawable) { - super.onAnimationStart(drawable) - } - - final override fun onAnimationEnd(drawable: Drawable) { - super.onAnimationEnd(drawable) - - if (!deactivated) { - handleAnimationEnd(drawable) - } - } - - /** Set the icon to a static image. */ - protected fun showStaticDrawable(@DrawableRes iconRes: Int) { - iconView.setImageDrawable(context.getDrawable(iconRes)) - } - - /** Animate a resource. */ - protected fun animateIconOnce(@DrawableRes iconRes: Int) { - animateIcon(iconRes, false) - } - - /** Animate a resource. */ - protected fun animateIcon(@DrawableRes iconRes: Int, repeat: Boolean) { - if (!deactivated) { - val icon = context.getDrawable(iconRes) as AnimatedVectorDrawable - iconView.setImageDrawable(icon) - icon.forceAnimationOnUI() - if (repeat) { - icon.registerAnimationCallback(this) - } - icon.start() - } - } - - /** Update the icon to reflect the [newState]. */ - fun updateState(lastState: BiometricState, newState: BiometricState) { - if (deactivated) { - Log.w(TAG, "Ignoring updateState when deactivated: $newState") - } else { - updateIcon(lastState, newState) - } - } - - /** Call during [updateState] if the controller is not [deactivated]. */ - abstract fun updateIcon(lastState: BiometricState, newState: BiometricState) - - /** Called during [onAnimationEnd] if the controller is not [deactivated]. */ - open fun handleAnimationEnd(drawable: Drawable) {} - - // TODO(b/251476085): Migrate this to an extension at the appropriate level? - /** Load the given [rawResources] immediately so they are cached for use in the [context]. */ - protected fun cacheLottieAssetsInContext(context: Context, vararg rawResources: Int) { - for (res in rawResources) { - LottieCompositionFactory.fromRawRes(context, res) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt index c4c52e8b358e..050b399fd3e8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/DisplayStateRepository.kt @@ -42,8 +42,11 @@ import kotlinx.coroutines.flow.stateIn /** Repository for the current state of the display */ interface DisplayStateRepository { /** - * Whether or not the direction rotation is applied to get to an application's requested - * orientation is reversed. + * If true, the direction rotation is applied to get to an application's requested orientation + * is reversed. Normally, the model is that landscape is clockwise from portrait; thus on a + * portrait device an app requesting landscape will cause a clockwise rotation, and on a + * landscape device an app requesting portrait will cause a counter-clockwise rotation. Setting + * true here reverses that logic. See go/natural-orientation for context. */ val isReverseDefaultRotation: Boolean diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt index a317a0684055..427361d4b17a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -57,6 +57,15 @@ interface DisplayStateInteractor { /** Display change event indicating a change to the given displayId has occurred. */ val displayChanges: Flow<Int> + /** + * If true, the direction rotation is applied to get to an application's requested orientation + * is reversed. Normally, the model is that landscape is clockwise from portrait; thus on a + * portrait device an app requesting landscape will cause a clockwise rotation, and on a + * landscape device an app requesting portrait will cause a counter-clockwise rotation. Setting + * true here reverses that logic. See go/natural-orientation for context. + */ + val isReverseDefaultRotation: Boolean + /** Called on configuration changes, used to keep the display state in sync */ fun onConfigurationChanged(newConfig: Configuration) } @@ -112,6 +121,8 @@ constructor( override val currentRotation: StateFlow<DisplayRotation> = displayStateRepository.currentRotation + override val isReverseDefaultRotation: Boolean = displayStateRepository.isReverseDefaultRotation + override fun onConfigurationChanged(newConfig: Configuration) { screenSizeFoldProvider.onConfigurationChange(newConfig) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java index cef0be09d3ce..0d72b9c07d7a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java @@ -29,11 +29,10 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; -import com.android.systemui.res.R; -import com.android.systemui.biometrics.AuthBiometricFingerprintIconController; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.AuthDialog; import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter; +import com.android.systemui.res.R; import kotlin.Pair; @@ -85,13 +84,13 @@ public class BiometricPromptLayout extends LinearLayout { } @Deprecated - public void updateFingerprintAffordanceSize( - @NonNull AuthBiometricFingerprintIconController iconController) { + public Pair<Integer, Integer> getUpdatedFingerprintAffordanceSize() { if (mUdfpsAdapter != null) { final int sensorDiameter = mUdfpsAdapter.getSensorDiameter( mScaleFactorProvider.provide()); - iconController.setIconLayoutParamSize(new Pair(sensorDiameter, sensorDiameter)); + return new Pair(sensorDiameter, sensorDiameter); } + return null; } @NonNull diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index c29efc0fcab9..ac48b6a2b11e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -38,11 +38,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.biometrics.AuthBiometricFaceIconController -import com.android.systemui.biometrics.AuthBiometricFingerprintAndFaceIconController -import com.android.systemui.biometrics.AuthBiometricFingerprintIconController -import com.android.systemui.biometrics.AuthIconController +import com.airbnb.lottie.LottieCompositionFactory import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality @@ -56,6 +52,7 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -101,10 +98,15 @@ object BiometricViewBinder { !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled descriptionView.movementMethod = ScrollingMovementMethod() - val iconViewOverlay = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) + val iconOverlayView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay) val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon) - PromptFingerprintIconViewBinder.bind(iconView, viewModel.fingerprintIconViewModel) + PromptIconViewBinder.bind( + iconView, + iconOverlayView, + view.getUpdatedFingerprintAffordanceSize(), + viewModel.iconViewModel + ) val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator) @@ -128,9 +130,21 @@ object BiometricViewBinder { // bind to prompt var boundSize = false + view.repeatWhenAttached { // these do not change and need to be set before any size transitions val modalities = viewModel.modalities.first() + if (modalities.hasFingerprint) { + /** + * Load the given [rawResources] immediately so they are cached for use in the + * [context]. + */ + val rawResources = viewModel.iconViewModel.getRawAssets(modalities.hasSfps) + for (res in rawResources) { + LottieCompositionFactory.fromRawRes(view.context, res) + } + } + titleView.text = viewModel.title.first() descriptionView.text = viewModel.description.first() subtitleView.text = viewModel.subtitle.first() @@ -148,27 +162,8 @@ object BiometricViewBinder { legacyCallback.onButtonTryAgain() } - // TODO(b/251476085): migrate legacy icon controllers and remove - var legacyState = viewModel.legacyState.value - val iconController = - modalities.asIconController( - view.context, - iconView, - iconViewOverlay, - ) - adapter.attach(this, iconController, modalities, legacyCallback) - if (iconController is AuthBiometricFingerprintIconController) { - view.updateFingerprintAffordanceSize(iconController) - } - if (iconController is HackyCoexIconController) { - iconController.faceMode = !viewModel.isConfirmationRequired.first() - } + adapter.attach(this, modalities, legacyCallback) - // the icon controller must be created before this happens for the legacy - // sizing code in BiometricPromptLayout to work correctly. Simplify this - // when those are also migrated. (otherwise the icon size may not be set to - // a pixel value before the view is measured and WRAP_CONTENT will be incorrectly - // used as part of the measure spec) if (!boundSize) { boundSize = true BiometricViewSizeBinder.bind( @@ -212,14 +207,6 @@ object BiometricViewBinder { ) { legacyCallback.onStartDelayedFingerprintSensor() } - - if (newMode.isStarted) { - // do wonky switch from implicit to explicit flow - (iconController as? HackyCoexIconController)?.faceMode = false - viewModel.showAuthenticating( - modalities.asDefaultHelpMessage(view.context), - ) - } } } @@ -312,7 +299,7 @@ object BiometricViewBinder { viewModel.isIconConfirmButton .map { isPending -> when { - isPending && iconController.actsAsConfirmButton -> + isPending && modalities.hasFaceAndFingerprint -> View.OnTouchListener { _: View, event: MotionEvent -> viewModel.onOverlayTouch(event) } @@ -320,22 +307,11 @@ object BiometricViewBinder { } } .collect { onTouch -> - iconViewOverlay.setOnTouchListener(onTouch) + iconOverlayView.setOnTouchListener(onTouch) iconView.setOnTouchListener(onTouch) } } - // TODO(b/251476085): remove w/ legacy icon controllers - // set icon affordance using legacy states - // like the old code, this causes animations to repeat on config changes :( - // but keep behavior for now as no one has complained... - launch { - viewModel.legacyState.collect { newState -> - iconController.updateState(legacyState, newState) - legacyState = newState - } - } - // dismiss prompt when authenticated and confirmed launch { viewModel.isAuthenticated.collect { authState -> @@ -350,7 +326,7 @@ object BiometricViewBinder { // Allow icon to be used as confirmation button with a11y enabled if (accessibilityManager.isTouchExplorationEnabled) { - iconViewOverlay.setOnClickListener { + iconOverlayView.setOnClickListener { viewModel.confirmAuthenticated() } iconView.setOnClickListener { viewModel.confirmAuthenticated() } @@ -377,7 +353,6 @@ object BiometricViewBinder { launch { viewModel.message.collect { promptMessage -> val isError = promptMessage is PromptMessage.Error - indicatorMessageView.text = promptMessage.message indicatorMessageView.setTextColor( if (isError) textColorError else textColorHint @@ -472,9 +447,6 @@ class Spaghetti( private var modalities: BiometricModalities = BiometricModalities() private var legacyCallback: Callback? = null - var legacyIconController: AuthIconController? = null - private set - // hacky way to suppress lockout errors private val lockoutErrorStrings = listOf( @@ -485,24 +457,20 @@ class Spaghetti( fun attach( lifecycleOwner: LifecycleOwner, - iconController: AuthIconController, activeModalities: BiometricModalities, callback: Callback, ) { modalities = activeModalities - legacyIconController = iconController legacyCallback = callback lifecycleOwner.lifecycle.addObserver( object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { lifecycleScope = owner.lifecycleScope - iconController.deactivated = false } override fun onDestroy(owner: LifecycleOwner) { lifecycleScope = null - iconController.deactivated = true } } ) @@ -626,61 +594,9 @@ private fun BiometricModalities.asDefaultHelpMessage(context: Context): String = else -> "" } -private fun BiometricModalities.asIconController( - context: Context, - iconView: LottieAnimationView, - iconViewOverlay: LottieAnimationView, -): AuthIconController = - when { - hasFaceAndFingerprint -> HackyCoexIconController(context, iconView, iconViewOverlay) - hasFingerprint -> AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) - hasFace -> AuthBiometricFaceIconController(context, iconView) - else -> throw IllegalStateException("unexpected view type :$this") - } - private fun Boolean.asVisibleOrGone(): Int = if (this) View.VISIBLE else View.GONE private fun Boolean.asVisibleOrHidden(): Int = if (this) View.VISIBLE else View.INVISIBLE // TODO(b/251476085): proper type? typealias BiometricJankListener = Animator.AnimatorListener - -// TODO(b/251476085): delete - temporary until the legacy icon controllers are replaced -private class HackyCoexIconController( - context: Context, - iconView: LottieAnimationView, - iconViewOverlay: LottieAnimationView, -) : AuthBiometricFingerprintAndFaceIconController(context, iconView, iconViewOverlay) { - - private var state: Spaghetti.BiometricState? = null - private val faceController = AuthBiometricFaceIconController(context, iconView) - - var faceMode: Boolean = true - set(value) { - if (field != value) { - field = value - - faceController.deactivated = !value - iconView.setImageIcon(null) - iconViewOverlay.setImageIcon(null) - state?.let { updateIcon(Spaghetti.BiometricState.STATE_IDLE, it) } - } - } - - override fun updateIcon( - lastState: Spaghetti.BiometricState, - newState: Spaghetti.BiometricState, - ) { - if (deactivated) { - return - } - - if (faceMode) { - faceController.updateIcon(lastState, newState) - } else { - super.updateIcon(lastState, newState) - } - - state = newState - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt deleted file mode 100644 index d28f1dc78c13..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptFingerprintIconViewBinder.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2023 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.ui.binder - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.biometrics.ui.viewmodel.PromptFingerprintIconViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.launch - -/** Sub-binder for [BiometricPromptLayout.iconView]. */ -object PromptFingerprintIconViewBinder { - - /** Binds [BiometricPromptLayout.iconView] to [PromptFingerprintIconViewModel]. */ - @JvmStatic - fun bind(view: LottieAnimationView, viewModel: PromptFingerprintIconViewModel) { - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.onConfigurationChanged(view.context.resources.configuration) - launch { - viewModel.iconAsset.collect { iconAsset -> - if (iconAsset != -1) { - view.setAnimation(iconAsset) - // TODO: must replace call below once non-sfps asset logic and - // shouldAnimateIconView logic is migrated to this ViewModel. - view.playAnimation() - } - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt new file mode 100644 index 000000000000..475ef18e5099 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2023 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.ui.binder + +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.airbnb.lottie.LottieAnimationView +import com.android.settingslib.widget.LottieColorUtils +import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel +import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.util.kotlin.Utils.Companion.toQuad +import com.android.systemui.util.kotlin.Utils.Companion.toQuint +import com.android.systemui.util.kotlin.Utils.Companion.toTriple +import com.android.systemui.util.kotlin.sample +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Sub-binder for [BiometricPromptLayout.iconView]. */ +object PromptIconViewBinder { + /** + * Binds [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] to + * [PromptIconViewModel]. + */ + @JvmStatic + fun bind( + iconView: LottieAnimationView, + iconOverlayView: LottieAnimationView, + iconViewLayoutParamSizeOverride: Pair<Int, Int>?, + viewModel: PromptIconViewModel + ) { + iconView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.onConfigurationChanged(iconView.context.resources.configuration) + if (iconViewLayoutParamSizeOverride != null) { + iconView.layoutParams.width = iconViewLayoutParamSizeOverride.first + iconView.layoutParams.height = iconViewLayoutParamSizeOverride.second + + iconOverlayView.layoutParams.width = iconViewLayoutParamSizeOverride.first + iconOverlayView.layoutParams.height = iconViewLayoutParamSizeOverride.second + } + + var faceIcon: AnimatedVectorDrawable? = null + val faceIconCallback = + object : Animatable2.AnimationCallback() { + override fun onAnimationStart(drawable: Drawable) { + viewModel.onAnimationStart() + } + + override fun onAnimationEnd(drawable: Drawable) { + viewModel.onAnimationEnd() + } + } + + launch { + viewModel.activeAuthType.collect { activeAuthType -> + if (iconViewLayoutParamSizeOverride == null) { + val width: Int + val height: Int + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> { + width = viewModel.fingerprintIconWidth + height = viewModel.fingerprintIconHeight + } + AuthType.Face -> { + width = viewModel.faceIconWidth + height = viewModel.faceIconHeight + } + } + + iconView.layoutParams.width = width + iconView.layoutParams.height = height + + iconOverlayView.layoutParams.width = width + iconOverlayView.layoutParams.height = height + } + } + } + + launch { + viewModel.iconAsset + .sample( + combine( + viewModel.activeAuthType, + viewModel.shouldAnimateIconView, + viewModel.shouldRepeatAnimation, + viewModel.showingError, + ::toQuad + ), + ::toQuint + ) + .collect { + ( + iconAsset, + activeAuthType, + shouldAnimateIconView, + shouldRepeatAnimation, + showingError) -> + if (iconAsset != -1) { + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> { + iconView.setAnimation(iconAsset) + iconView.frame = 0 + + if (shouldAnimateIconView) { + iconView.playAnimation() + } + } + AuthType.Face -> { + faceIcon?.apply { + unregisterAnimationCallback(faceIconCallback) + stop() + } + faceIcon = + iconView.context.getDrawable(iconAsset) + as AnimatedVectorDrawable + faceIcon?.apply { + iconView.setImageDrawable(this) + if (shouldAnimateIconView) { + forceAnimationOnUI() + if (shouldRepeatAnimation) { + registerAnimationCallback(faceIconCallback) + } + start() + } + } + } + } + LottieColorUtils.applyDynamicColors(iconView.context, iconView) + viewModel.setPreviousIconWasError(showingError) + } + } + } + + launch { + viewModel.iconOverlayAsset + .sample( + combine( + viewModel.shouldAnimateIconOverlay, + viewModel.showingError, + ::Pair + ), + ::toTriple + ) + .collect { (iconOverlayAsset, shouldAnimateIconOverlay, showingError) -> + if (iconOverlayAsset != -1) { + iconOverlayView.setAnimation(iconOverlayAsset) + iconOverlayView.frame = 0 + LottieColorUtils.applyDynamicColors( + iconOverlayView.context, + iconOverlayView + ) + + if (shouldAnimateIconOverlay) { + iconOverlayView.playAnimation() + } + viewModel.setPreviousIconOverlayWasError(showingError) + } + } + } + + launch { + viewModel.shouldFlipIconView.collect { shouldFlipIconView -> + if (shouldFlipIconView) { + iconView.rotation = 180f + } + } + } + + launch { + viewModel.contentDescriptionId.collect { id -> + if (id != -1) { + iconView.contentDescription = iconView.context.getString(id) + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt deleted file mode 100644 index dfd3a9b8aebe..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2023 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.ui.viewmodel - -import android.annotation.RawRes -import android.content.res.Configuration -import com.android.systemui.res.R -import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor -import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor -import com.android.systemui.biometrics.shared.model.DisplayRotation -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine - -/** Models UI of [BiometricPromptLayout.iconView] */ -class PromptFingerprintIconViewModel -@Inject -constructor( - private val displayStateInteractor: DisplayStateInteractor, - promptSelectorInteractor: PromptSelectorInteractor, -) { - /** Current BiometricPromptLayout.iconView asset. */ - val iconAsset: Flow<Int> = - combine( - displayStateInteractor.currentRotation, - displayStateInteractor.isFolded, - displayStateInteractor.isInRearDisplayMode, - promptSelectorInteractor.sensorType, - ) { - rotation: DisplayRotation, - isFolded: Boolean, - isInRearDisplayMode: Boolean, - sensorType: FingerprintSensorType -> - when (sensorType) { - FingerprintSensorType.POWER_BUTTON -> - getSideFpsAnimationAsset(rotation, isFolded, isInRearDisplayMode) - // Replace below when non-SFPS iconAsset logic is migrated to this ViewModel - else -> -1 - } - } - - @RawRes - private fun getSideFpsAnimationAsset( - rotation: DisplayRotation, - isDeviceFolded: Boolean, - isInRearDisplayMode: Boolean, - ): Int = - when (rotation) { - DisplayRotation.ROTATION_90 -> - if (isInRearDisplayMode) { - R.raw.biometricprompt_rear_portrait_reverse_base - } else if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_topleft - } else { - R.raw.biometricprompt_portrait_base_topleft - } - DisplayRotation.ROTATION_270 -> - if (isInRearDisplayMode) { - R.raw.biometricprompt_rear_portrait_base - } else if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_bottomright - } else { - R.raw.biometricprompt_portrait_base_bottomright - } - else -> - if (isInRearDisplayMode) { - R.raw.biometricprompt_rear_landscape_base - } else if (isDeviceFolded) { - R.raw.biometricprompt_folded_base_default - } else { - R.raw.biometricprompt_landscape_base - } - } - - /** Called on configuration changes */ - fun onConfigurationChanged(newConfig: Configuration) { - displayStateInteractor.onConfigurationChanged(newConfig) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt new file mode 100644 index 000000000000..11a5d8b578df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt @@ -0,0 +1,721 @@ +/* + * Copyright (C) 2023 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.ui.viewmodel + +import android.annotation.DrawableRes +import android.annotation.RawRes +import android.content.res.Configuration +import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor +import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor +import com.android.systemui.biometrics.shared.model.DisplayRotation +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.res.R +import com.android.systemui.util.kotlin.combine +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.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +/** + * Models UI of [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay] + */ +class PromptIconViewModel +constructor( + promptViewModel: PromptViewModel, + private val displayStateInteractor: DisplayStateInteractor, + promptSelectorInteractor: PromptSelectorInteractor +) { + + /** Auth types for the UI to display. */ + enum class AuthType { + Fingerprint, + Face, + Coex + } + + /** + * Indicates what auth type the UI currently displays. + * Fingerprint-only auth -> Fingerprint + * Face-only auth -> Face + * Co-ex auth, implicit flow -> Face + * Co-ex auth, explicit flow -> Coex + */ + val activeAuthType: Flow<AuthType> = + combine( + promptViewModel.modalities.distinctUntilChanged(), + promptViewModel.faceMode.distinctUntilChanged() + ) { modalities, faceMode -> + if (modalities.hasFaceAndFingerprint && !faceMode) { + AuthType.Coex + } else if (modalities.hasFaceOnly || faceMode) { + AuthType.Face + } else if (modalities.hasFingerprintOnly) { + AuthType.Fingerprint + } else { + throw IllegalStateException("unexpected modality: $modalities") + } + } + + /** Whether an error message is currently being shown. */ + val showingError = promptViewModel.showingError + + /** Whether the previous icon shown displayed an error. */ + private val _previousIconWasError: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** Whether the previous icon overlay shown displayed an error. */ + private val _previousIconOverlayWasError: MutableStateFlow<Boolean> = MutableStateFlow(false) + + fun setPreviousIconWasError(previousIconWasError: Boolean) { + _previousIconWasError.value = previousIconWasError + } + + fun setPreviousIconOverlayWasError(previousIconOverlayWasError: Boolean) { + _previousIconOverlayWasError.value = previousIconOverlayWasError + } + + /** Called when iconView begins animating. */ + fun onAnimationStart() { + _animationEnded.value = false + } + + /** Called when iconView ends animating. */ + fun onAnimationEnd() { + _animationEnded.value = true + } + + private val _animationEnded: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** + * Whether a face iconView should pulse (i.e. while isAuthenticating and previous animation + * ended). + */ + val shouldPulseAnimation: Flow<Boolean> = + combine(_animationEnded, promptViewModel.isAuthenticating) { + animationEnded, + isAuthenticating -> + animationEnded && isAuthenticating + } + .distinctUntilChanged() + + private val _lastPulseLightToDark: MutableStateFlow<Boolean> = MutableStateFlow(false) + + /** Tracks whether a face iconView last pulsed light to dark (vs. dark to light) */ + val lastPulseLightToDark: Flow<Boolean> = _lastPulseLightToDark.asStateFlow() + + /** Layout params for fingerprint iconView */ + val fingerprintIconWidth: Int = promptViewModel.fingerprintIconWidth + val fingerprintIconHeight: Int = promptViewModel.fingerprintIconHeight + + /** Layout params for face iconView */ + val faceIconWidth: Int = promptViewModel.faceIconWidth + val faceIconHeight: Int = promptViewModel.faceIconHeight + + /** Current BiometricPromptLayout.iconView asset. */ + val iconAsset: Flow<Int> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint -> + combine( + displayStateInteractor.currentRotation, + displayStateInteractor.isFolded, + displayStateInteractor.isInRearDisplayMode, + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + rotation: DisplayRotation, + isFolded: Boolean, + isInRearDisplayMode: Boolean, + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSfpsIconViewAsset(rotation, isFolded, isInRearDisplayMode) + else -> + getFingerprintIconViewAsset( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + } + } + AuthType.Face -> + shouldPulseAnimation.flatMapLatest { shouldPulseAnimation: Boolean -> + if (shouldPulseAnimation) { + val iconAsset = + if (_lastPulseLightToDark.value) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + _lastPulseLightToDark.value = !_lastPulseLightToDark.value + flowOf(iconAsset) + } else { + combine( + promptViewModel.isAuthenticated.distinctUntilChanged(), + promptViewModel.isAuthenticating.distinctUntilChanged(), + promptViewModel.isPendingConfirmation.distinctUntilChanged(), + promptViewModel.showingError.distinctUntilChanged() + ) { + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + getFaceIconViewAsset( + authState, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } + } + AuthType.Coex -> + combine( + displayStateInteractor.currentRotation, + displayStateInteractor.isFolded, + displayStateInteractor.isInRearDisplayMode, + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.isPendingConfirmation, + promptViewModel.showingError, + ) { + rotation: DisplayRotation, + isFolded: Boolean, + isInRearDisplayMode: Boolean, + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSfpsIconViewAsset(rotation, isFolded, isInRearDisplayMode) + else -> + getCoexIconViewAsset( + authState, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } + } + } + + private fun getFingerprintIconViewAsset( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ): Int = + if (isAuthenticated) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_success_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + } + } else if (isAuthenticating) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_fingerprint_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } + } else if (showingError) { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } else { + -1 + } + + @RawRes + private fun getSfpsIconViewAsset( + rotation: DisplayRotation, + isDeviceFolded: Boolean, + isInRearDisplayMode: Boolean, + ): Int = + when (rotation) { + DisplayRotation.ROTATION_90 -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_portrait_reverse_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_topleft + } else { + R.raw.biometricprompt_portrait_base_topleft + } + DisplayRotation.ROTATION_270 -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_portrait_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_bottomright + } else { + R.raw.biometricprompt_portrait_base_bottomright + } + else -> + if (isInRearDisplayMode) { + R.raw.biometricprompt_rear_landscape_base + } else if (isDeviceFolded) { + R.raw.biometricprompt_folded_base_default + } else { + R.raw.biometricprompt_landscape_base + } + } + + @DrawableRes + private fun getFaceIconViewAsset( + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ): Int = + if (authState.isAuthenticated && isPendingConfirmation) { + R.drawable.face_dialog_wink_from_dark + } else if (authState.isAuthenticated) { + R.drawable.face_dialog_dark_to_checkmark + } else if (isAuthenticating) { + _lastPulseLightToDark.value = false + R.drawable.face_dialog_pulse_dark_to_light + } else if (showingError) { + R.drawable.face_dialog_dark_to_error + } else if (_previousIconWasError.value) { + R.drawable.face_dialog_error_to_idle + } else { + R.drawable.face_dialog_idle_static + } + + @RawRes + private fun getCoexIconViewAsset( + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ): Int = + if (authState.isAuthenticatedAndExplicitlyConfirmed) { + R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie + } else if (isPendingConfirmation) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_unlock_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie + } + } else if (authState.isAuthenticated) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_success_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + } + } else if (isAuthenticating) { + if (_previousIconWasError.value) { + R.raw.fingerprint_dialogue_error_to_fingerprint_lottie + } else { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } + } else if (showingError) { + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie + } else { + -1 + } + + /** Current BiometricPromptLayout.biometric_icon_overlay asset. */ + var iconOverlayAsset: Flow<Int> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + displayStateInteractor.currentRotation, + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + rotation: DisplayRotation, + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + getSfpsIconOverlayAsset( + rotation, + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> -1 + } + } + AuthType.Face -> flowOf(-1) + } + } + + @RawRes + private fun getSfpsIconOverlayAsset( + rotation: DisplayRotation, + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ): Int = + if (isAuthenticated) { + if (_previousIconOverlayWasError.value) { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_error_to_success_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_error_to_success_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_error_to_success_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright + } + } else { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright + } + } + } else if (isAuthenticating) { + if (_previousIconOverlayWasError.value) { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright + } + } else { + when (rotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + } + } + } else if (showingError) { + when (rotation) { + DisplayRotation.ROTATION_0 -> R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + } + } else { + -1 + } + + /** Content description for iconView */ + val contentDescriptionId: Flow<Int> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.isPendingConfirmation, + promptViewModel.showingError + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + getFingerprintIconContentDescriptionId( + sensorType, + authState.isAuthenticated, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + AuthType.Face -> + combine( + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError, + ) { authState: PromptAuthState, isAuthenticating: Boolean, showingError: Boolean + -> + getFaceIconContentDescriptionId(authState, isAuthenticating, showingError) + } + } + } + + private fun getFingerprintIconContentDescriptionId( + sensorType: FingerprintSensorType, + isAuthenticated: Boolean, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ): Int = + if (isPendingConfirmation) { + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + R.string.security_settings_sfps_enroll_find_sensor_message + else -> R.string.fingerprint_dialog_authenticated_confirmation + } + } else if (isAuthenticating || isAuthenticated) { + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + R.string.security_settings_sfps_enroll_find_sensor_message + else -> R.string.fingerprint_dialog_touch_sensor + } + } else if (showingError) { + R.string.biometric_dialog_try_again + } else { + -1 + } + + private fun getFaceIconContentDescriptionId( + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean + ): Int = + if (authState.isAuthenticatedAndExplicitlyConfirmed) { + R.string.biometric_dialog_face_icon_description_confirmed + } else if (authState.isAuthenticated) { + R.string.biometric_dialog_face_icon_description_authenticated + } else if (isAuthenticating) { + R.string.biometric_dialog_face_icon_description_authenticating + } else if (showingError) { + R.string.keyguard_face_failed + } else { + R.string.biometric_dialog_face_icon_description_idle + } + + /** Whether the current BiometricPromptLayout.iconView asset animation should be playing. */ + val shouldAnimateIconView: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + shouldAnimateSfpsIconView( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> + shouldAnimateFingerprintIconView( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + } + } + AuthType.Face -> + combine( + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { authState: PromptAuthState, isAuthenticating: Boolean, showingError: Boolean + -> + isAuthenticating || + authState.isAuthenticated || + showingError || + _previousIconWasError.value + } + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.isPendingConfirmation, + promptViewModel.showingError, + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + shouldAnimateSfpsIconView( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> + shouldAnimateCoexIconView( + authState.isAuthenticated, + isAuthenticating, + isPendingConfirmation, + showingError + ) + } + } + } + } + + private fun shouldAnimateFingerprintIconView( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ) = (isAuthenticating && _previousIconWasError.value) || isAuthenticated || showingError + + private fun shouldAnimateSfpsIconView( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ) = isAuthenticated || isAuthenticating || showingError + + private fun shouldAnimateCoexIconView( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + isPendingConfirmation: Boolean, + showingError: Boolean + ) = + (isAuthenticating && _previousIconWasError.value) || + isPendingConfirmation || + isAuthenticated || + showingError + + /** Whether the current iconOverlayAsset animation should be playing. */ + val shouldAnimateIconOverlay: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + promptViewModel.isAuthenticated, + promptViewModel.isAuthenticating, + promptViewModel.showingError + ) { + sensorType: FingerprintSensorType, + authState: PromptAuthState, + isAuthenticating: Boolean, + showingError: Boolean -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + shouldAnimateSfpsIconOverlay( + authState.isAuthenticated, + isAuthenticating, + showingError + ) + else -> false + } + } + AuthType.Face -> flowOf(false) + } + } + + private fun shouldAnimateSfpsIconOverlay( + isAuthenticated: Boolean, + isAuthenticating: Boolean, + showingError: Boolean + ) = (isAuthenticating && _previousIconOverlayWasError.value) || isAuthenticated || showingError + + /** Whether the iconView should be flipped due to a device using reverse default rotation . */ + val shouldFlipIconView: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> + combine( + promptSelectorInteractor.sensorType, + displayStateInteractor.currentRotation + ) { sensorType: FingerprintSensorType, rotation: DisplayRotation -> + when (sensorType) { + FingerprintSensorType.POWER_BUTTON -> + (rotation == DisplayRotation.ROTATION_180) + else -> false + } + } + AuthType.Face -> flowOf(false) + } + } + + /** Whether the current BiometricPromptLayout.iconView asset animation should be repeated. */ + val shouldRepeatAnimation: Flow<Boolean> = + activeAuthType.flatMapLatest { activeAuthType: AuthType -> + when (activeAuthType) { + AuthType.Fingerprint, + AuthType.Coex -> flowOf(false) + AuthType.Face -> promptViewModel.isAuthenticating.map { it } + } + } + + /** Called on configuration changes */ + fun onConfigurationChanged(newConfig: Configuration) { + displayStateInteractor.onConfigurationChanged(newConfig) + } + + /** iconView assets for caching */ + fun getRawAssets(hasSfps: Boolean): List<Int> { + return if (hasSfps) { + listOf( + R.raw.biometricprompt_fingerprint_to_error_landscape, + R.raw.biometricprompt_folded_base_bottomright, + R.raw.biometricprompt_folded_base_default, + R.raw.biometricprompt_folded_base_topleft, + R.raw.biometricprompt_landscape_base, + R.raw.biometricprompt_portrait_base_bottomright, + R.raw.biometricprompt_portrait_base_topleft, + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape, + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright, + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft, + R.raw.biometricprompt_symbol_error_to_success_landscape, + R.raw.biometricprompt_symbol_error_to_success_portrait_bottomright, + R.raw.biometricprompt_symbol_error_to_success_portrait_topleft, + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright, + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft, + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape, + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright, + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft + ) + } else { + listOf( + R.raw.fingerprint_dialogue_error_to_fingerprint_lottie, + R.raw.fingerprint_dialogue_error_to_success_lottie, + R.raw.fingerprint_dialogue_fingerprint_to_error_lottie, + R.raw.fingerprint_dialogue_fingerprint_to_success_lottie + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 267afae118e6..e49b4a7bbce9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -28,10 +28,10 @@ import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.PromptKind -import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import javax.inject.Inject import kotlinx.coroutines.Job @@ -39,7 +39,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -51,25 +50,29 @@ import kotlinx.coroutines.launch class PromptViewModel @Inject constructor( - private val displayStateInteractor: DisplayStateInteractor, - private val promptSelectorInteractor: PromptSelectorInteractor, + displayStateInteractor: DisplayStateInteractor, + promptSelectorInteractor: PromptSelectorInteractor, private val vibrator: VibratorHelper, @Application context: Context, private val featureFlags: FeatureFlags, ) { - /** Models UI of [BiometricPromptLayout.iconView] */ - val fingerprintIconViewModel: PromptFingerprintIconViewModel = - PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor) - /** The set of modalities available for this prompt */ val modalities: Flow<BiometricModalities> = promptSelectorInteractor.prompt .map { it?.modalities ?: BiometricModalities() } .distinctUntilChanged() - // TODO(b/251476085): remove after icon controllers are migrated - do not keep this state - private var _legacyState = MutableStateFlow(Spaghetti.BiometricState.STATE_IDLE) - val legacyState: StateFlow<Spaghetti.BiometricState> = _legacyState.asStateFlow() + /** Layout params for fingerprint iconView */ + val fingerprintIconWidth: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_fingerprint_icon_width) + val fingerprintIconHeight: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_fingerprint_icon_height) + + /** Layout params for face iconView */ + val faceIconWidth: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) + val faceIconHeight: Int = + context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size) private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -82,6 +85,12 @@ constructor( /** If the user has successfully authenticated and confirmed (when explicitly required). */ val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() + /** If the auth is pending confirmation. */ + val isPendingConfirmation: Flow<Boolean> = + isAuthenticated.map { authState -> + authState.isAuthenticated && authState.needsUserConfirmation + } + private val _isOverlayTouched: MutableStateFlow<Boolean> = MutableStateFlow(false) /** The kind of credential the user has. */ @@ -96,6 +105,9 @@ constructor( /** A message to show the user, if there is an error, hint, or help to show. */ val message: Flow<PromptMessage> = _message.asStateFlow() + /** Whether an error message is currently being shown. */ + val showingError: Flow<Boolean> = message.map { it.isError }.distinctUntilChanged() + private val isRetrySupported: Flow<Boolean> = modalities.map { it.hasFace } private val _fingerprintStartMode = MutableStateFlow(FingerprintStartMode.Pending) @@ -141,6 +153,38 @@ constructor( !isOverlayTouched && size.isNotSmall } + /** + * When fingerprint and face modalities are enrolled, indicates whether only face auth has + * started. + * + * True when fingerprint and face modalities are enrolled and implicit flow is active. This + * occurs in co-ex auth when confirmation is not required and only face auth is started, then + * becomes false when device transitions to explicit flow after a first error, when the + * fingerprint sensor is started. + * + * False when the dialog opens in explicit flow (fingerprint and face modalities enrolled but + * confirmation is required), or if user has only fingerprint enrolled, or only face enrolled. + */ + val faceMode: Flow<Boolean> = + combine(modalities, isConfirmationRequired, fingerprintStartMode) { + modalities: BiometricModalities, + isConfirmationRequired: Boolean, + fingerprintStartMode: FingerprintStartMode -> + if (modalities.hasFaceAndFingerprint) { + if (isConfirmationRequired) { + false + } else { + !fingerprintStartMode.isStarted + } + } else { + false + } + } + .distinctUntilChanged() + + val iconViewModel: PromptIconViewModel = + PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor) + /** Padding for prompt UI elements */ val promptPadding: Flow<Rect> = combine(size, displayStateInteractor.currentRotation) { size, rotation -> @@ -184,9 +228,9 @@ constructor( val isConfirmButtonVisible: Flow<Boolean> = combine( size, - isAuthenticated, - ) { size, authState -> - size.isNotSmall && authState.isAuthenticated && authState.needsUserConfirmation + isPendingConfirmation, + ) { size, isPendingConfirmation -> + size.isNotSmall && isPendingConfirmation } .distinctUntilChanged() @@ -293,7 +337,6 @@ constructor( _isAuthenticated.value = PromptAuthState(false) _forceMediumSize.value = true _message.value = PromptMessage.Error(message) - _legacyState.value = Spaghetti.BiometricState.STATE_ERROR if (hapticFeedback) { vibrator.error(failedModality) @@ -305,7 +348,7 @@ constructor( if (authenticateAfterError) { showAuthenticating(messageAfterError) } else { - showInfo(messageAfterError) + showHelp(messageAfterError) } } } @@ -325,15 +368,12 @@ constructor( private fun supportsRetry(failedModality: BiometricModality) = failedModality == BiometricModality.Face - suspend fun showHelp(message: String) = showHelp(message, clearIconError = false) - suspend fun showInfo(message: String) = showHelp(message, clearIconError = true) - /** * Show a persistent help message. * * Will be show even if the user has already authenticated. */ - private suspend fun showHelp(message: String, clearIconError: Boolean) { + suspend fun showHelp(message: String) { val alreadyAuthenticated = _isAuthenticated.value.isAuthenticated if (!alreadyAuthenticated) { _isAuthenticating.value = false @@ -343,16 +383,6 @@ constructor( _message.value = if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true - _legacyState.value = - if (alreadyAuthenticated && isConfirmationRequired.first()) { - Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - } else if (alreadyAuthenticated && !isConfirmationRequired.first()) { - Spaghetti.BiometricState.STATE_AUTHENTICATED - } else if (clearIconError) { - Spaghetti.BiometricState.STATE_IDLE - } else { - Spaghetti.BiometricState.STATE_HELP - } messageJob?.cancel() messageJob = null @@ -376,7 +406,6 @@ constructor( _message.value = if (message.isNotBlank()) PromptMessage.Help(message) else PromptMessage.Empty _forceMediumSize.value = true - _legacyState.value = Spaghetti.BiometricState.STATE_HELP messageJob?.cancel() messageJob = launch { @@ -396,7 +425,6 @@ constructor( _isAuthenticating.value = true _isAuthenticated.value = PromptAuthState(false) _message.value = if (message.isBlank()) PromptMessage.Empty else PromptMessage.Help(message) - _legacyState.value = Spaghetti.BiometricState.STATE_AUTHENTICATING // reset the try again button(s) after the user attempts a retry if (isRetry) { @@ -427,12 +455,6 @@ constructor( _isAuthenticated.value = PromptAuthState(true, modality, needsUserConfirmation, dismissAfterDelay) _message.value = PromptMessage.Empty - _legacyState.value = - if (needsUserConfirmation) { - Spaghetti.BiometricState.STATE_PENDING_CONFIRMATION - } else { - Spaghetti.BiometricState.STATE_AUTHENTICATED - } if (!needsUserConfirmation) { vibrator.success(modality) @@ -472,7 +494,6 @@ constructor( _isAuthenticated.value = authState.asExplicitlyConfirmed() _message.value = PromptMessage.Empty - _legacyState.value = Spaghetti.BiometricState.STATE_AUTHENTICATED vibrator.success(authState.authenticatedModality) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt index 2bd62587834d..21578f491de7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt @@ -146,6 +146,16 @@ constructor( /** Show the bouncer if necessary and set the relevant states. */ @JvmOverloads fun show(isScrimmed: Boolean) { + if (primaryBouncerView.delegate == null) { + Log.d( + TAG, + "PrimaryBouncerInteractor#show is being called before the " + + "primaryBouncerDelegate is set. Let's exit early so we don't set the wrong " + + "primaryBouncer state." + ) + return + } + // Reset some states as we show the bouncer. repository.setKeyguardAuthenticatedBiometrics(null) repository.setPrimaryStartingToHide(false) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt index f9c4f29afee9..1a214ba5a3d9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/model/CommunalWidgetMetadata.kt @@ -16,7 +16,7 @@ package com.android.systemui.communal.data.model -import com.android.systemui.communal.shared.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalContentSize /** Metadata for the default widgets */ data class CommunalWidgetMetadata( diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index f13b62fbfeb9..77025dc8839a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -28,11 +29,12 @@ import android.os.UserManager import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.CommunalAppWidgetInfo -import com.android.systemui.communal.shared.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger @@ -43,6 +45,7 @@ import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** Encapsulates the state of widgets for communal mode. */ @@ -52,6 +55,9 @@ interface CommunalWidgetRepository { /** Widgets that are allowed to render in the glanceable hub */ val communalWidgetAllowlist: List<CommunalWidgetMetadata> + + /** A flow of information about all the communal widgets to show. */ + val communalWidgets: Flow<List<CommunalWidgetContentModel>> } @SysUISingleton @@ -67,7 +73,7 @@ constructor( private val userManager: UserManager, private val userTracker: UserTracker, @CommunalLog logBuffer: LogBuffer, - featureFlags: FeatureFlags, + featureFlags: FeatureFlagsClassic, ) : CommunalWidgetRepository { companion object { const val TAG = "CommunalWidgetRepository" @@ -88,49 +94,59 @@ constructor( // Widgets that should be rendered in communal mode. private val widgets: HashMap<Int, CommunalAppWidgetInfo> = hashMapOf() - private val isUserUnlocked: Flow<Boolean> = callbackFlow { - if (!featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) { - awaitClose() - } - - fun isUserUnlockingOrUnlocked(): Boolean { - return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) - } - - fun send() { - trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG) - } + private val isUserUnlocked: Flow<Boolean> = + callbackFlow { + if (!communalRepository.isCommunalEnabled) { + awaitClose() + } - if (isUserUnlockingOrUnlocked()) { - send() - awaitClose() - } else { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - send() - } + fun isUserUnlockingOrUnlocked(): Boolean { + return userManager.isUserUnlockingOrUnlocked(userTracker.userHandle) } - broadcastDispatcher.registerReceiver( - receiver, - IntentFilter(Intent.ACTION_USER_UNLOCKED), - ) + fun send() { + trySendWithFailureLogging(isUserUnlockingOrUnlocked(), TAG) + } - awaitClose { broadcastDispatcher.unregisterReceiver(receiver) } + if (isUserUnlockingOrUnlocked()) { + send() + awaitClose() + } else { + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + send() + } + } + + broadcastDispatcher.registerReceiver( + receiver, + IntentFilter(Intent.ACTION_USER_UNLOCKED), + ) + + awaitClose { broadcastDispatcher.unregisterReceiver(receiver) } + } + } + .distinctUntilChanged() + + private val isHostActive: Flow<Boolean> = + isUserUnlocked.map { + if (it) { + startListening() + true + } else { + stopListening() + clearWidgets() + false + } } - } override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = - isUserUnlocked.map { isUserUnlocked -> - if (!isUserUnlocked) { - clearWidgets() - stopListening() + isHostActive.map { isHostActive -> + if (!isHostActive || !featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)) { return@map null } - startListening() - val providerInfo = appWidgetManager.installedProviders.find { it.loadLabel(packageManager).equals(WIDGET_LABEL) @@ -144,6 +160,42 @@ constructor( return@map addWidget(providerInfo) } + override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = + isHostActive.map { isHostActive -> + if (!isHostActive) { + return@map emptyList() + } + + // The allowlist should be fetched from the local database with all the metadata tied to + // a widget, including an appWidgetId if it has been bound. Before the database is set + // up, we are going to use the app widget host as the source of truth for bound widgets, + // and rebind each time on boot. + + // Remove all previously bound widgets. + appWidgetHost.appWidgetIds.forEach { appWidgetHost.deleteAppWidgetId(it) } + + val inventory = mutableListOf<CommunalWidgetContentModel>() + + // Bind all widgets from the allowlist. + communalWidgetAllowlist.forEach { + val id = appWidgetHost.allocateAppWidgetId() + appWidgetManager.bindAppWidgetId( + id, + ComponentName.unflattenFromString(it.componentName), + ) + + inventory.add( + CommunalWidgetContentModel( + appWidgetId = id, + providerInfo = appWidgetManager.getAppWidgetInfo(id), + priority = it.priority, + ) + ) + } + + return@map inventory.toList() + } + private fun getWidgetAllowlist(): List<CommunalWidgetMetadata> { val componentNames = applicationContext.resources.getStringArray(R.array.config_communalWidgetAllowlist) @@ -151,7 +203,7 @@ constructor( CommunalWidgetMetadata( componentName = name, priority = componentNames.size - index, - sizes = listOf(CommunalContentSize.HALF) + sizes = listOf(CommunalContentSize.HALF), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 04bb6ae75e60..62387079ab35 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -18,7 +18,8 @@ package com.android.systemui.communal.domain.interactor import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -38,4 +39,12 @@ constructor( /** A flow of info about the widget to be displayed, or null if widget is unavailable. */ val appWidgetInfo: Flow<CommunalAppWidgetInfo?> = widgetRepository.stopwatchAppWidgetInfo + + /** + * A flow of information about widgets to be shown in communal hub. + * + * Currently only showing persistent widgets that have been bound to the app widget service + * (have an allocated id). + */ + val widgetContent: Flow<List<CommunalWidgetContentModel>> = widgetRepository.communalWidgets } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalContentSize.kt deleted file mode 100644 index 0bd7d86c972d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalContentSize.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.android.systemui.communal.shared - -/** Supported sizes for communal content in the layout grid. */ -enum class CommunalContentSize { - FULL, - HALF, - THIRD, -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalAppWidgetInfo.kt index 0803a01b93b8..109ed2da7e48 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalAppWidgetInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalAppWidgetInfo.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.communal.shared +package com.android.systemui.communal.shared.model import android.appwidget.AppWidgetProviderInfo diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentCategory.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentCategory.kt new file mode 100644 index 000000000000..7f05b9cd4943 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentCategory.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 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.communal.shared.model + +enum class CommunalContentCategory { + /** The content persists in the communal hub until removed by the user. */ + PERSISTENT, + + /** The content temporarily shows up in the communal hub when certain conditions are met. */ + TRANSIENT, +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt new file mode 100644 index 000000000000..39a6476929ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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.communal.shared.model + +/** Supported sizes for communal content in the layout grid. */ +enum class CommunalContentSize { + /** Content takes the full height of the column. */ + FULL, + + /** Content takes half of the height of the column. */ + HALF, + + /** Content takes a third of the height of the column. */ + THIRD, +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt new file mode 100644 index 000000000000..e141dc40477c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 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.communal.shared.model + +import android.appwidget.AppWidgetProviderInfo + +/** Encapsulates data for a communal widget. */ +data class CommunalWidgetContentModel( + val appWidgetId: Int, + val providerInfo: AppWidgetProviderInfo, + val priority: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt index 2a08d7f6bc28..0daf7b5610e8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt @@ -20,7 +20,7 @@ import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.Context import android.util.SizeF -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.communal.ui.view.CommunalWidgetWrapper import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/model/CommunalContentUiModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/model/CommunalContentUiModel.kt new file mode 100644 index 000000000000..98060dc1dceb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/model/CommunalContentUiModel.kt @@ -0,0 +1,15 @@ +package com.android.systemui.communal.ui.model + +import android.view.View +import com.android.systemui.communal.shared.model.CommunalContentSize + +/** + * Encapsulates data for a communal content that holds a view. + * + * This model stays in the UI layer. + */ +data class CommunalContentUiModel( + val view: View, + val size: CommunalContentSize, + val priority: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index ddeb1d67b945..25c64eafe255 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -16,17 +16,43 @@ package com.android.systemui.communal.ui.viewmodel +import android.appwidget.AppWidgetHost +import android.content.Context +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.ui.model.CommunalContentUiModel import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map @SysUISingleton class CommunalViewModel @Inject constructor( + @Application private val context: Context, + private val appWidgetHost: AppWidgetHost, + communalInteractor: CommunalInteractor, tutorialInteractor: CommunalTutorialInteractor, ) { /** Whether communal hub should show tutorial content. */ val showTutorialContent: Flow<Boolean> = tutorialInteractor.isTutorialAvailable + + /** List of widgets to be displayed in the communal hub. */ + val widgetContent: Flow<List<CommunalContentUiModel>> = + communalInteractor.widgetContent.map { + it.map { + // TODO(b/306406256): As adding and removing widgets functionalities are + // supported, cache the host views so they're not recreated each time. + val hostView = appWidgetHost.createView(context, it.appWidgetId, it.providerInfo) + return@map CommunalContentUiModel( + view = hostView, + priority = it.priority, + // All widgets have HALF size. + size = CommunalContentSize.HALF, + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt index 8fba342c49be..d7bbea64332e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalWidgetViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.communal.ui.viewmodel import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 9221832b4ba1..4bdea75d9d71 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -79,4 +79,7 @@ interface BaseComposeFacade { context: Context, viewModel: CommunalViewModel, ): View + + /** Creates a container that hosts the communal UI and handles gesture transitions. */ + fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java index 1b0d5f9a9fdf..2f8fe42672c2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/SensorPrivacyToggleTile.java @@ -32,7 +32,6 @@ import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import com.android.internal.logging.MetricsLogger; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; @@ -43,6 +42,7 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -55,7 +55,7 @@ public abstract class SensorPrivacyToggleTile extends QSTileImpl<QSTile.BooleanS private final KeyguardStateController mKeyguard; protected IndividualSensorPrivacyController mSensorPrivacyController; - private final SafetyCenterManager mSafetyCenterManager; + private final Boolean mIsSafetyCenterEnabled; /** * @return Id of the sensor that will be toggled @@ -89,7 +89,7 @@ public abstract class SensorPrivacyToggleTile extends QSTileImpl<QSTile.BooleanS statusBarStateController, activityStarter, qsLogger); mSensorPrivacyController = sensorPrivacyController; mKeyguard = keyguardStateController; - mSafetyCenterManager = safetyCenterManager; + mIsSafetyCenterEnabled = safetyCenterManager.isSafetyCenterEnabled(); mSensorPrivacyController.observe(getLifecycle(), this); } @@ -138,7 +138,7 @@ public abstract class SensorPrivacyToggleTile extends QSTileImpl<QSTile.BooleanS @Override public Intent getLongClickIntent() { - if (mSafetyCenterManager.isSafetyCenterEnabled()) { + if (mIsSafetyCenterEnabled) { return new Intent(Settings.ACTION_PRIVACY_CONTROLS); } else { return new Intent(Settings.ACTION_PRIVACY_SETTINGS); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 09514404d4db..d8692397edda 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -35,6 +35,7 @@ import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.LockIconViewController; import com.android.keyguard.dagger.KeyguardBouncerComponent; import com.android.systemui.Dumpable; +import com.android.systemui.FeatureFlags; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; @@ -42,10 +43,12 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel; +import com.android.systemui.compose.ComposeFacade; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; @@ -103,8 +106,10 @@ public class NotificationShadeWindowViewController implements Dumpable { private final PulsingGestureListener mPulsingGestureListener; private final LockscreenHostedDreamGestureListener mLockscreenHostedDreamGestureListener; private final NotificationInsetsController mNotificationInsetsController; + private final CommunalViewModel mCommunalViewModel; private final boolean mIsTrackpadCommonEnabled; private final FeatureFlags mFeatureFlags; + private final FeatureFlagsClassic mFeatureFlagsClassic; private final SysUIKeyEventHandler mSysUIKeyEventHandler; private final PrimaryBouncerInteractor mPrimaryBouncerInteractor; private final AlternateBouncerInteractor mAlternateBouncerInteractor; @@ -175,7 +180,9 @@ public class NotificationShadeWindowViewController implements Dumpable { KeyguardMessageAreaController.Factory messageAreaControllerFactory, KeyguardTransitionInteractor keyguardTransitionInteractor, PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, + CommunalViewModel communalViewModel, NotificationExpansionRepository notificationExpansionRepository, + FeatureFlagsClassic featureFlagsClassic, FeatureFlags featureFlags, SystemClock clock, BouncerMessageInteractor bouncerMessageInteractor, @@ -206,8 +213,10 @@ public class NotificationShadeWindowViewController implements Dumpable { mPulsingGestureListener = pulsingGestureListener; mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener; mNotificationInsetsController = notificationInsetsController; - mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON); + mCommunalViewModel = communalViewModel; + mIsTrackpadCommonEnabled = featureFlagsClassic.isEnabled(TRACKPAD_GESTURE_COMMON); mFeatureFlags = featureFlags; + mFeatureFlagsClassic = featureFlagsClassic; mSysUIKeyEventHandler = sysUIKeyEventHandler; mPrimaryBouncerInteractor = primaryBouncerInteractor; mAlternateBouncerInteractor = alternateBouncerInteractor; @@ -223,7 +232,7 @@ public class NotificationShadeWindowViewController implements Dumpable { messageAreaControllerFactory, bouncerMessageInteractor, bouncerLogger, - featureFlags, + featureFlagsClassic, selectedUserInteractor); collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(), @@ -234,7 +243,7 @@ public class NotificationShadeWindowViewController implements Dumpable { this::setExpandAnimationRunning); mClock = clock; - if (featureFlags.isEnabled(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION)) { + if (featureFlagsClassic.isEnabled(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION)) { unfoldTransitionProgressProvider.ifPresent( progressProvider -> progressProvider.addCallback( mDisableSubpixelTextTransitionListener)); @@ -263,7 +272,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller); mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(), mPulsingGestureListener); - if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { + if (mFeatureFlagsClassic.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { mDreamingWakeupGestureHandler = new GestureDetector(mView.getContext(), mLockscreenHostedDreamGestureListener); } @@ -435,7 +444,7 @@ public class NotificationShadeWindowViewController implements Dumpable { } boolean bouncerShowing; - if (mFeatureFlags.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) { + if (mFeatureFlagsClassic.isEnabled(Flags.ALTERNATE_BOUNCER_VIEW)) { bouncerShowing = mPrimaryBouncerInteractor.isBouncerShowing() || mAlternateBouncerInteractor.isVisibleState(); } else { @@ -447,7 +456,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (mDragDownHelper.isDragDownEnabled()) { // This handles drag down over lockscreen boolean result = mDragDownHelper.onInterceptTouchEvent(ev); - if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + if (mFeatureFlagsClassic.isEnabled(Flags.MIGRATE_NSSL)) { if (result) { mLastInterceptWasDragDownHelper = true; if (ev.getAction() == MotionEvent.ACTION_DOWN) { @@ -479,7 +488,7 @@ public class NotificationShadeWindowViewController implements Dumpable { MotionEvent cancellation = MotionEvent.obtain(ev); cancellation.setAction(MotionEvent.ACTION_CANCEL); mStackScrollLayout.onInterceptTouchEvent(cancellation); - if (!mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + if (!mFeatureFlagsClassic.isEnabled(Flags.MIGRATE_NSSL)) { mNotificationPanelViewController.handleExternalInterceptTouch(cancellation); } cancellation.recycle(); @@ -494,7 +503,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (mStatusBarKeyguardViewManager.onTouch(ev)) { return true; } - if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + if (mFeatureFlagsClassic.isEnabled(Flags.MIGRATE_NSSL)) { if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) { // we still want to finish our drag down gesture when locking the screen handled |= mDragDownHelper.onTouchEvent(ev) || handled; @@ -559,8 +568,27 @@ public class NotificationShadeWindowViewController implements Dumpable { mDepthController.onPanelExpansionChanged(currentState); } + /** + * Sets up the communal hub UI if the {@link com.android.systemui.Flags#FLAG_COMMUNAL_HUB} flag + * is enabled. + * + * The layout lives in {@link R.id.communal_ui_container}. + */ + public void setupCommunalHubLayout() { + if (!mFeatureFlags.communalHub() || !ComposeFacade.INSTANCE.isComposeAvailable()) { + return; + } + + // Replace the placeholder view with the communal UI. + View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub); + int index = mView.indexOfChild(communalPlaceholder); + mView.removeView(communalPlaceholder); + mView.addView(ComposeFacade.INSTANCE.createCommunalContainer(mView.getContext(), + mCommunalViewModel), index); + } + private boolean didNotificationPanelInterceptEvent(MotionEvent ev) { - if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + if (mFeatureFlagsClassic.isEnabled(Flags.MIGRATE_NSSL)) { // Since NotificationStackScrollLayout is now a sibling of notification_panel, we need // to also ask NotificationPanelViewController directly, in order to process swipe up // events originating from notifications diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index a8a20cc8559b..2f684762a13a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -35,7 +35,6 @@ import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository import com.android.systemui.user.domain.interactor.UserSwitcherInteractor -import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineScope @@ -140,13 +139,6 @@ constructor( /** Whether either the shade or QS is fully expanded. */ val isAnyFullyExpanded: Flow<Boolean> = anyExpansion.map { it >= 1f }.distinctUntilChanged() - /** Whether either the shade or QS is expanding from a fully collapsed state. */ - val isAnyExpanding: Flow<Boolean> = - anyExpansion - .pairwise(1f) - .map { (prev, curr) -> curr > 0f && curr < 1f && prev < 1f } - .distinctUntilChanged() - /** * Whether either the shade or QS is partially or fully expanded, i.e. not fully collapsed. At * this time, this is not simply a matter of checking if either value in shadeExpansion and diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java index 57d20246ea14..8957f298f160 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PropertyAnimator.java @@ -114,18 +114,21 @@ public class PropertyAnimator { || previousAnimator.getAnimatedFraction() == 0)) { animator.setStartDelay(properties.delay); } - if (listener != null) { - animator.addListener(listener); - } // remove the tag when the animation is finished animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - view.setTag(animatorTag, null); - view.setTag(animationStartTag, null); - view.setTag(animationEndTag, null); + Animator existing = (Animator) view.getTag(animatorTag); + if (existing == animation) { + view.setTag(animatorTag, null); + view.setTag(animationStartTag, null); + view.setTag(animationEndTag, null); + } } }); + if (listener != null) { + animator.addListener(listener); + } ViewState.startAnimator(animator, listener); view.setTag(animatorTag, animator); view.setTag(animationStartTag, currentValue); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 4573d5989faa..cfe9fbe3af29 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -748,7 +748,11 @@ public final class NotificationEntry extends ListEntry { return row != null && row.getGuts() != null && row.getGuts().isExposed(); } - public boolean isChildInGroup() { + /** + * @return Whether the notification row is a child of a group notification view; false if the + * row is null + */ + public boolean rowIsChildInGroup() { return row != null && row.isChildInGroup(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 9fb6c1bb2fd0..8d9fd12356a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1525,6 +1525,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // regressions, we'll continue standing up the root view in CentralSurfaces. mNotificationShadeWindowController.fetchWindowRootView(); getNotificationShadeWindowViewController().setupExpandedStatusBar(); + getNotificationShadeWindowViewController().setupCommunalHubLayout(); mShadeController.setNotificationShadeWindowViewController( getNotificationShadeWindowViewController()); mBackActionInteractor.setup(mQsController, mShadeSurface); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 6b4382f731ea..f4862c73606f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -175,7 +175,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp if (!hasPinnedHeadsUp() || topEntry == null) { return null; } else { - if (topEntry.isChildInGroup()) { + if (topEntry.rowIsChildInGroup()) { final NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(topEntry); if (groupSummary != null) { diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt index 83ff78980880..b3834f58be2f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt @@ -269,3 +269,108 @@ inline fun <T> CoroutineScope.stateFlow( crossinline getValue: () -> T, ): StateFlow<T> = changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue()) + +inline fun <T1, T2, T3, T4, T5, T6, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6 + ) + } +} + +inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7 + ) + } +} + +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8 + ) + } +} + +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + flow9: Flow<T9>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9 + ) { args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[6] as T8, + args[6] as T9, + ) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconControllerTest.kt deleted file mode 100644 index 215d63508306..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricFingerprintIconControllerTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2023 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.content.Context -import android.hardware.biometrics.SensorProperties -import android.hardware.fingerprint.FingerprintManager -import android.hardware.fingerprint.FingerprintSensorProperties -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal -import android.view.ViewGroup.LayoutParams -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.airbnb.lottie.LottieAnimationView -import com.android.systemui.res.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` as whenEver -import org.mockito.junit.MockitoJUnit - -private const val SENSOR_ID = 1 - -@SmallTest -@RunWith(AndroidJUnit4::class) -class AuthBiometricFingerprintIconControllerTest : SysuiTestCase() { - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var iconView: LottieAnimationView - @Mock private lateinit var iconViewOverlay: LottieAnimationView - @Mock private lateinit var layoutParam: LayoutParams - @Mock private lateinit var fingerprintManager: FingerprintManager - - private lateinit var controller: AuthBiometricFingerprintIconController - - @Before - fun setUp() { - context.addMockSystemService(Context.FINGERPRINT_SERVICE, fingerprintManager) - whenEver(iconView.layoutParams).thenReturn(layoutParam) - whenEver(iconViewOverlay.layoutParams).thenReturn(layoutParam) - } - - @Test - fun testIconContentDescription_SfpsDevice() { - setupFingerprintSensorProperties(FingerprintSensorProperties.TYPE_POWER_BUTTON) - controller = AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) - - assertThat(controller.getIconContentDescription(BiometricState.STATE_AUTHENTICATING)) - .isEqualTo( - context.resources.getString( - R.string.security_settings_sfps_enroll_find_sensor_message - ) - ) - } - - @Test - fun testIconContentDescription_NonSfpsDevice() { - setupFingerprintSensorProperties(FingerprintSensorProperties.TYPE_UDFPS_OPTICAL) - controller = AuthBiometricFingerprintIconController(context, iconView, iconViewOverlay) - - assertThat(controller.getIconContentDescription(BiometricState.STATE_AUTHENTICATING)) - .isEqualTo(context.resources.getString(R.string.fingerprint_dialog_touch_sensor)) - } - - private fun setupFingerprintSensorProperties(sensorType: Int) { - whenEver(fingerprintManager.sensorPropertiesInternal) - .thenReturn( - listOf( - FingerprintSensorPropertiesInternal( - SENSOR_ID, - SensorProperties.STRENGTH_STRONG, - 5 /* maxEnrollmentsPerUser */, - listOf() /* componentInfo */, - sensorType, - true /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - listOf() /* sensorLocations */ - ) - ) - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt index d68a3131da4c..8c26776a1ef8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt @@ -72,6 +72,7 @@ class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() { runCurrent() // WHEN shade expands + shadeRepository.setLegacyShadeTracking(true) shadeRepository.setLegacyShadeExpansion(.5f) runCurrent() @@ -108,6 +109,7 @@ class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() { // WHEN detector is disabled and shade opens detector.disable() + shadeRepository.setLegacyShadeTracking(true) shadeRepository.setLegacyShadeExpansion(.5f) runCurrent() 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 9f24a9f553a1..15633d1baed1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -30,6 +30,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal internal fun fingerprintSensorPropertiesInternal( ids: List<Int> = listOf(0), strong: Boolean = true, + sensorType: Int = FingerprintSensorProperties.TYPE_REAR ): List<FingerprintSensorPropertiesInternal> { val componentInfo = listOf( @@ -54,7 +55,7 @@ internal fun fingerprintSensorPropertiesInternal( if (strong) SensorProperties.STRENGTH_STRONG else SensorProperties.STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */, componentInfo, - FingerprintSensorProperties.TYPE_REAR, + sensorType, false /* resetLockoutRequiresHardwareAuthToken */ ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt deleted file mode 100644 index fd86486aeff8..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModelTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.android.systemui.biometrics.ui.viewmodel - -import android.content.res.Configuration -import androidx.test.filters.SmallTest -import com.android.internal.widget.LockPatternUtils -import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.biometrics.data.repository.FakePromptRepository -import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor -import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl -import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor -import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import com.android.systemui.biometrics.shared.model.SensorStrength -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.display.data.repository.FakeDisplayRepository -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -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.Mock -import org.mockito.junit.MockitoJUnit - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class PromptFingerprintIconViewModelTest : SysuiTestCase() { - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - - @Mock private lateinit var lockPatternUtils: LockPatternUtils - - private lateinit var displayRepository: FakeDisplayRepository - private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository - private lateinit var promptRepository: FakePromptRepository - private lateinit var displayStateRepository: FakeDisplayStateRepository - - private val testScope = TestScope(StandardTestDispatcher()) - private val fakeExecutor = FakeExecutor(FakeSystemClock()) - - private lateinit var promptSelectorInteractor: PromptSelectorInteractor - private lateinit var displayStateInteractor: DisplayStateInteractor - private lateinit var viewModel: PromptFingerprintIconViewModel - - @Before - fun setup() { - displayRepository = FakeDisplayRepository() - fingerprintRepository = FakeFingerprintPropertyRepository() - promptRepository = FakePromptRepository() - displayStateRepository = FakeDisplayStateRepository() - - promptSelectorInteractor = - PromptSelectorInteractorImpl(fingerprintRepository, promptRepository, lockPatternUtils) - displayStateInteractor = - DisplayStateInteractorImpl( - testScope.backgroundScope, - mContext, - fakeExecutor, - displayStateRepository, - displayRepository, - ) - viewModel = PromptFingerprintIconViewModel(displayStateInteractor, promptSelectorInteractor) - } - - @Test - fun sfpsIconUpdates_onConfigurationChanged() { - testScope.runTest { - runCurrent() - configureFingerprintPropertyRepository(FingerprintSensorType.POWER_BUTTON) - val testConfig = Configuration() - val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1 - val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1 - val currentIcon = collectLastValue(viewModel.iconAsset) - - testConfig.smallestScreenWidthDp = folded - viewModel.onConfigurationChanged(testConfig) - val foldedIcon = currentIcon() - - testConfig.smallestScreenWidthDp = unfolded - viewModel.onConfigurationChanged(testConfig) - val unfoldedIcon = currentIcon() - - assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) - } - } - - private fun configureFingerprintPropertyRepository(sensorType: FingerprintSensorType) { - fingerprintRepository.setProperties(0, SensorStrength.STRONG, sensorType, mapOf()) - } -} - -internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index ca6df4027ea9..b695a0ee1fa3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -16,8 +16,10 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.res.Configuration import android.hardware.biometrics.PromptInfo import android.hardware.face.FaceSensorPropertiesInternal +import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.view.HapticFeedbackConstants import android.view.MotionEvent @@ -36,12 +38,15 @@ import com.android.systemui.biometrics.faceSensorPropertiesInternal import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricModality -import com.android.systemui.biometrics.ui.binder.Spaghetti.BiometricState +import com.android.systemui.biometrics.shared.model.DisplayRotation +import com.android.systemui.biometrics.shared.model.toSensorStrength +import com.android.systemui.biometrics.shared.model.toSensorType import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -66,6 +71,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 4 private const val CHALLENGE = 2L +private const val DELAY = 1000L @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -88,11 +94,22 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private lateinit var selector: PromptSelectorInteractor private lateinit var viewModel: PromptViewModel + private lateinit var iconViewModel: PromptIconViewModel private val featureFlags = FakeFeatureFlags() @Before fun setup() { fingerprintRepository = FakeFingerprintPropertyRepository() + testCase.fingerprint?.let { + fingerprintRepository.setProperties( + it.sensorId, + it.sensorStrength.toSensorStrength(), + it.sensorType.toSensorType(), + it.allLocations.associateBy { sensorLocationInternal -> + sensorLocationInternal.displayId + } + ) + } promptRepository = FakePromptRepository() displayStateRepository = FakeDisplayStateRepository() displayRepository = FakeDisplayRepository() @@ -110,6 +127,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel = PromptViewModel(displayStateInteractor, selector, vibrator, mContext, featureFlags) + iconViewModel = viewModel.iconViewModel featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) } @@ -123,7 +141,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val modalities by collectLastValue(viewModel.modalities) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) assertThat(authenticating).isFalse() assertThat(authenticated?.isNotAuthenticated).isTrue() @@ -133,7 +150,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } assertThat(message).isEqualTo(PromptMessage.Empty) assertThat(size).isEqualTo(expectedSize) - assertThat(legacyState).isEqualTo(BiometricState.STATE_IDLE) val startMessage = "here we go" viewModel.showAuthenticating(startMessage, isRetry = false) @@ -143,7 +159,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.isNotAuthenticated).isTrue() assertThat(size).isEqualTo(expectedSize) assertButtonsVisible(negative = expectedSize != PromptSize.SMALL) - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATING) } @Test @@ -205,6 +220,472 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(currentConstant).isEqualTo(HapticFeedbackConstants.REJECT) } + @Test + fun start_idle_and_show_authenticating_iconUpdate() = + runGenericTest(doNotStart = true) { + val currentRotation by collectLastValue(displayStateInteractor.currentRotation) + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + val forceExplicitFlow = testCase.isCoex && testCase.authenticatedByFingerprint + if (forceExplicitFlow) { + viewModel.ensureFingerprintHasStarted(isDelayed = true) + } + + val startMessage = "here we go" + viewModel.showAuthenticating(startMessage, isRetry = false) + + if (testCase.isFingerprintOnly) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(false) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } + assertThat(iconAsset).isEqualTo(expectedIconAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) + } + + if (testCase.isCoex) { + if (testCase.confirmationRequested || forceExplicitFlow) { + // explicit flow + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(false) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } else { + // implicit flow + val shouldRepeatAnimation by + collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark) + + val expectedIconAsset = + if (shouldPulseAnimation!!) { + if (lastPulseLightToDark!!) { + R.drawable.face_dialog_pulse_dark_to_light + } else { + R.drawable.face_dialog_pulse_light_to_dark + } + } else { + R.drawable.face_dialog_pulse_dark_to_light + } + assertThat(iconAsset).isEqualTo(expectedIconAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(true) + } + } + } + + @Test + fun start_authenticating_show_and_clear_error_iconUpdate() = runGenericTest { + val currentRotation by collectLastValue(displayStateInteractor.currentRotation) + + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + val forceExplicitFlow = testCase.isCoex && testCase.authenticatedByFingerprint + if (forceExplicitFlow) { + viewModel.ensureFingerprintHasStarted(isDelayed = true) + } + + val errorJob = launch { + viewModel.showTemporaryError( + "so sad", + messageAfterError = "", + authenticateAfterError = testCase.isFingerprintOnly || testCase.isCoex, + ) + // Usually done by binder + iconViewModel.setPreviousIconWasError(true) + iconViewModel.setPreviousIconOverlayWasError(true) + } + + if (testCase.isFingerprintOnly) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_fingerprint_to_error_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_error_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + + // Clear error, restart authenticating + errorJob.join() + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_error_to_fingerprint_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_error) + assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + + // Clear error, go to idle + errorJob.join() + + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_error_to_idle) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_idle) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + + if (testCase.isCoex) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_error_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId).isEqualTo(R.string.biometric_dialog_try_again) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + + // Clear error, restart authenticating + errorJob.join() + + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_error_to_fingerprint_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + } + + @Test + fun shows_authenticated_no_errors_no_confirmation_required_iconUpdate() = runGenericTest { + if (!testCase.confirmationRequested) { + val currentRotation by collectLastValue(displayStateInteractor.currentRotation) + + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + viewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + if (testCase.isFingerprintOnly) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val expectedOverlayAsset = + when (currentRotation) { + DisplayRotation.ROTATION_0 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_90 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_topleft + DisplayRotation.ROTATION_180 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_landscape + DisplayRotation.ROTATION_270 -> + R.raw.biometricprompt_symbol_fingerprint_to_success_portrait_bottomright + else -> throw Exception("invalid rotation") + } + assertThat(iconOverlayAsset).isEqualTo(expectedOverlayAsset) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.security_settings_sfps_enroll_find_sensor_message) + assertThat(shouldAnimateIconOverlay).isEqualTo(true) + } else { + val isAuthenticated by collectLastValue(viewModel.isAuthenticated) + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_success_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + + // If co-ex, using implicit flow (explicit flow always requires confirmation) + if (testCase.isFaceOnly || testCase.isCoex) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + } + } + + @Test + fun shows_pending_confirmation_iconUpdate() = runGenericTest { + if ( + (testCase.isFaceOnly || testCase.isCoex) && + testCase.authenticatedByFace && + testCase.confirmationRequested + ) { + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + viewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_wink_from_dark) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + + // explicit flow because confirmation requested + if (testCase.isCoex) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_fingerprint_to_unlock_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_authenticated_confirmation) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + } + } + + @Test + fun shows_authenticated_explicitly_confirmed_iconUpdate() = runGenericTest { + if ( + (testCase.isFaceOnly || testCase.isCoex) && + testCase.authenticatedByFace && + testCase.confirmationRequested + ) { + val iconAsset by collectLastValue(iconViewModel.iconAsset) + val iconContentDescriptionId by collectLastValue(iconViewModel.contentDescriptionId) + val shouldAnimateIconView by collectLastValue(iconViewModel.shouldAnimateIconView) + + viewModel.showAuthenticated( + modality = testCase.authenticatedModality, + dismissAfterDelay = DELAY + ) + + viewModel.confirmAuthenticated() + + if (testCase.isFaceOnly) { + val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation) + val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation) + + assertThat(shouldPulseAnimation!!).isEqualTo(false) + assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldRepeatAnimation).isEqualTo(false) + } + + // explicit flow because confirmation requested + if (testCase.isCoex) { + val iconOverlayAsset by collectLastValue(iconViewModel.iconOverlayAsset) + val shouldAnimateIconOverlay by + collectLastValue(iconViewModel.shouldAnimateIconOverlay) + + // TODO: Update when SFPS co-ex is implemented + if (testCase.sensorType != FingerprintSensorProperties.TYPE_POWER_BUTTON) { + assertThat(iconAsset) + .isEqualTo(R.raw.fingerprint_dialogue_unlocked_to_checkmark_success_lottie) + assertThat(iconOverlayAsset).isEqualTo(-1) + assertThat(iconContentDescriptionId) + .isEqualTo(R.string.fingerprint_dialog_touch_sensor) + assertThat(shouldAnimateIconView).isEqualTo(true) + assertThat(shouldAnimateIconOverlay).isEqualTo(false) + } + } + } + } + + @Test + fun sfpsIconUpdates_onConfigurationChanged() = runGenericTest { + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val testConfig = Configuration() + val folded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP - 1 + val unfolded = INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP + 1 + val currentIcon by collectLastValue(iconViewModel.iconAsset) + + testConfig.smallestScreenWidthDp = folded + iconViewModel.onConfigurationChanged(testConfig) + val foldedIcon = currentIcon + + testConfig.smallestScreenWidthDp = unfolded + iconViewModel.onConfigurationChanged(testConfig) + val unfoldedIcon = currentIcon + + assertThat(foldedIcon).isNotEqualTo(unfoldedIcon) + } + } + + @Test + fun sfpsIconUpdates_onRotation() = runGenericTest { + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val currentIcon by collectLastValue(iconViewModel.iconAsset) + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) + val iconRotation0 = currentIcon + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + val iconRotation90 = currentIcon + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) + val iconRotation180 = currentIcon + + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + val iconRotation270 = currentIcon + + assertThat(iconRotation0).isEqualTo(iconRotation180) + assertThat(iconRotation0).isNotEqualTo(iconRotation90) + assertThat(iconRotation0).isNotEqualTo(iconRotation270) + } + } + + @Test + fun sfpsIconUpdates_onRearDisplayMode() = runGenericTest { + if (testCase.sensorType == FingerprintSensorProperties.TYPE_POWER_BUTTON) { + val currentIcon by collectLastValue(iconViewModel.iconAsset) + + displayStateRepository.setIsInRearDisplayMode(false) + val iconNotRearDisplayMode = currentIcon + + displayStateRepository.setIsInRearDisplayMode(true) + val iconRearDisplayMode = currentIcon + + assertThat(iconNotRearDisplayMode).isNotEqualTo(iconRearDisplayMode) + } + } + private suspend fun TestScope.showAuthenticated( authenticatedModality: BiometricModality, expectConfirmation: Boolean, @@ -213,7 +694,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val fpStartMode by collectLastValue(viewModel.fingerprintStartMode) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val authWithSmallPrompt = testCase.shouldStartAsImplicitFlow && @@ -221,14 +701,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isTrue() assertThat(authenticated?.isNotAuthenticated).isTrue() assertThat(size).isEqualTo(if (authWithSmallPrompt) PromptSize.SMALL else PromptSize.MEDIUM) - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATING) assertButtonsVisible(negative = !authWithSmallPrompt) - val delay = 1000L - viewModel.showAuthenticated(authenticatedModality, delay) + viewModel.showAuthenticated(authenticatedModality, DELAY) assertThat(authenticated?.isAuthenticated).isTrue() - assertThat(authenticated?.delay).isEqualTo(delay) + assertThat(authenticated?.delay).isEqualTo(DELAY) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) assertThat(size) .isEqualTo( @@ -238,14 +716,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa PromptSize.SMALL } ) - assertThat(legacyState) - .isEqualTo( - if (expectConfirmation) { - BiometricState.STATE_PENDING_CONFIRMATION - } else { - BiometricState.STATE_AUTHENTICATED - } - ) + assertButtonsVisible( cancel = expectConfirmation, confirm = expectConfirmation, @@ -298,7 +769,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val message by collectLastValue(viewModel.message) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgainNow by collectLastValue(viewModel.canTryAgainNow) val errorJob = launch { @@ -312,7 +782,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(size).isEqualTo(PromptSize.MEDIUM) assertThat(message).isEqualTo(PromptMessage.Error(errorMessage)) assertThat(messageVisible).isTrue() - assertThat(legacyState).isEqualTo(BiometricState.STATE_ERROR) // temporary error should disappear after a delay errorJob.join() @@ -323,17 +792,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(message).isEqualTo(PromptMessage.Empty) assertThat(messageVisible).isFalse() } - val clearIconError = !restart - assertThat(legacyState) - .isEqualTo( - if (restart) { - BiometricState.STATE_AUTHENTICATING - } else if (clearIconError) { - BiometricState.STATE_IDLE - } else { - BiometricState.STATE_HELP - } - ) assertThat(authenticating).isEqualTo(restart) assertThat(authenticated?.isNotAuthenticated).isTrue() @@ -488,7 +946,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) @@ -506,7 +963,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) assertThat(canTryAgain).isFalse() } @@ -524,7 +980,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticating).isFalse() @@ -532,8 +987,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.isAuthenticated).isTrue() if (testCase.isFaceOnly && expectConfirmation) { - assertThat(legacyState).isEqualTo(BiometricState.STATE_PENDING_CONFIRMATION) - assertThat(size).isEqualTo(PromptSize.MEDIUM) assertButtonsVisible( cancel = true, @@ -543,8 +996,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) assertButtonsVisible() - } else { - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) } } @@ -563,7 +1014,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val authenticated by collectLastValue(viewModel.isAuthenticated) val message by collectLastValue(viewModel.message) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val canTryAgain by collectLastValue(viewModel.canTryAgainNow) assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) @@ -581,7 +1031,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) assertThat(canTryAgain).isFalse() } @@ -610,12 +1059,10 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val message by collectLastValue(viewModel.message) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertThat(legacyState).isEqualTo(BiometricState.STATE_HELP) assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() @@ -632,7 +1079,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa val message by collectLastValue(viewModel.message) val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) val size by collectLastValue(viewModel.size) - val legacyState by collectLastValue(viewModel.legacyState) val confirmationRequired by collectLastValue(viewModel.isConfirmationRequired) if (testCase.isCoex && testCase.authenticatedByFingerprint) { @@ -642,11 +1088,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa viewModel.showHelp(helpMessage) assertThat(size).isEqualTo(PromptSize.MEDIUM) - if (confirmationRequired == true) { - assertThat(legacyState).isEqualTo(BiometricState.STATE_PENDING_CONFIRMATION) - } else { - assertThat(legacyState).isEqualTo(BiometricState.STATE_AUTHENTICATED) - } + assertThat(message).isEqualTo(PromptMessage.Help(helpMessage)) assertThat(messageVisible).isTrue() assertThat(authenticating).isFalse() @@ -785,6 +1227,15 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa authenticatedModality = BiometricModality.Fingerprint, ), TestCase( + fingerprint = + fingerprintSensorPropertiesInternal( + strong = true, + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + ) + .first(), + authenticatedModality = BiometricModality.Fingerprint, + ), + TestCase( face = faceSensorPropertiesInternal(strong = true).first(), authenticatedModality = BiometricModality.Face, confirmationRequested = true, @@ -794,6 +1245,16 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa authenticatedModality = BiometricModality.Fingerprint, confirmationRequested = true, ), + TestCase( + fingerprint = + fingerprintSensorPropertiesInternal( + strong = true, + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + ) + .first(), + authenticatedModality = BiometricModality.Fingerprint, + confirmationRequested = true, + ), ) private val coexTestCases = @@ -834,7 +1295,9 @@ internal data class TestCase( val modality = when { fingerprint != null && face != null -> "coex" - fingerprint != null -> "fingerprint only" + fingerprint != null && fingerprint.isAnySidefpsType -> "fingerprint only, sideFps" + fingerprint != null && !fingerprint.isAnySidefpsType -> + "fingerprint only, non-sideFps" face != null -> "face only" else -> "?" } @@ -864,6 +1327,8 @@ internal data class TestCase( val isCoex: Boolean get() = face != null && fingerprint != null + @FingerprintSensorProperties.SensorType val sensorType: Int? = fingerprint?.sensorType + val shouldStartAsImplicitFlow: Boolean get() = (isFaceOnly || isCoex) && !confirmationRequested } @@ -890,3 +1355,5 @@ private fun PromptSelectorInteractor.initializePrompt( BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), ) } + +internal const val INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP = 600 diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt index 9373ada75003..f6b284fffa3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -111,6 +111,27 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { } @Test + fun show_nullDelegate() { + testScope.run { + whenever(bouncerView.delegate).thenReturn(null) + mainHandler.setMode(FakeHandler.Mode.QUEUEING) + + // WHEN bouncer show is requested + underTest.show(true) + + // WHEN all queued messages are dispatched + mainHandler.dispatchQueuedMessages() + + // THEN primary bouncer state doesn't update to show since delegate was null + verify(repository, never()).setPrimaryShow(true) + verify(repository, never()).setPrimaryShowingSoon(false) + verify(mPrimaryBouncerCallbackInteractor, never()).dispatchStartingToShow() + verify(mPrimaryBouncerCallbackInteractor, never()) + .dispatchVisibilityChanged(View.VISIBLE) + } + } + + @Test fun testShow_isScrimmed() { underTest.show(true) verify(repository).setKeyguardAuthenticatedBiometrics(null) diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index 91409a376556..fcb191b4cbd6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -12,9 +12,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.FakeLogBuffer @@ -38,6 +39,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @@ -58,10 +60,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker - @Mock private lateinit var featureFlags: FeatureFlags + @Mock private lateinit var featureFlags: FeatureFlagsClassic @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo + @Mock private lateinit var providerInfoA: AppWidgetProviderInfo + + @Mock private lateinit var providerInfoB: AppWidgetProviderInfo + + @Mock private lateinit var providerInfoC: AppWidgetProviderInfo + private lateinit var communalRepository: FakeCommunalRepository private lateinit var logBuffer: LogBuffer @@ -70,29 +78,34 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) + private val fakeAllowlist = + listOf( + "com.android.fake/WidgetProviderA", + "com.android.fake/WidgetProviderB", + "com.android.fake/WidgetProviderC", + ) + @Before fun setUp() { MockitoAnnotations.initMocks(this) logBuffer = FakeLogBuffer.Factory.create() - - featureFlagEnabled(true) communalRepository = FakeCommunalRepository() - communalRepository.setIsCommunalEnabled(true) - overrideResource( - R.array.config_communalWidgetAllowlist, - arrayOf(componentName1, componentName2) - ) + communalEnabled(true) + widgetOnKeyguardEnabled(true) + setAppWidgetIds(emptyList()) + + overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") whenever(userTracker.userHandle).thenReturn(userHandle) } @Test - fun broadcastReceiver_featureDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = + fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() = testScope.runTest { - featureFlagEnabled(false) + communalEnabled(false) val repository = initCommunalWidgetRepository() collectLastValue(repository.stopwatchAppWidgetInfo)() verifyBroadcastReceiverNeverRegistered() @@ -129,7 +142,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { job.cancel() runCurrent() - Mockito.verify(broadcastDispatcher).unregisterReceiver(receiver) + verify(broadcastDispatcher).unregisterReceiver(receiver) } @Test @@ -166,7 +179,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { installedProviders(listOf(stopwatchProviderInfo)) val repository = initCommunalWidgetRepository() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).allocateAppWidgetId() + verify(appWidgetHost).allocateAppWidgetId() } @Test @@ -185,8 +198,8 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { // Verify app widget id allocated assertThat(lastStopwatchProviderInfo()?.appWidgetId).isEqualTo(123456) - Mockito.verify(appWidgetHost).allocateAppWidgetId() - Mockito.verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt()) + verify(appWidgetHost).allocateAppWidgetId() + verify(appWidgetHost, Mockito.never()).deleteAppWidgetId(anyInt()) // User locked again userUnlocked(false) @@ -194,7 +207,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { // Verify app widget id deleted assertThat(lastStopwatchProviderInfo()).isNull() - Mockito.verify(appWidgetHost).deleteAppWidgetId(123456) + verify(appWidgetHost).deleteAppWidgetId(123456) } @Test @@ -203,13 +216,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { userUnlocked(false) val repository = initCommunalWidgetRepository() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost, Mockito.never()).startListening() + verify(appWidgetHost, Mockito.never()).startListening() userUnlocked(true) broadcastReceiverUpdate() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).startListening() + verify(appWidgetHost).startListening() } @Test @@ -223,14 +236,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { broadcastReceiverUpdate() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).startListening() - Mockito.verify(appWidgetHost, Mockito.never()).stopListening() + verify(appWidgetHost).startListening() + verify(appWidgetHost, Mockito.never()).stopListening() userUnlocked(false) broadcastReceiverUpdate() collectLastValue(repository.stopwatchAppWidgetInfo)() - Mockito.verify(appWidgetHost).stopListening() + verify(appWidgetHost).stopListening() } @Test @@ -241,21 +254,80 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { assertThat( listOf( CommunalWidgetMetadata( - componentName = componentName1, + componentName = fakeAllowlist[0], + priority = 3, + sizes = listOf(CommunalContentSize.HALF), + ), + CommunalWidgetMetadata( + componentName = fakeAllowlist[1], priority = 2, - sizes = listOf(CommunalContentSize.HALF) + sizes = listOf(CommunalContentSize.HALF), ), CommunalWidgetMetadata( - componentName = componentName2, + componentName = fakeAllowlist[2], priority = 1, - sizes = listOf(CommunalContentSize.HALF) - ) + sizes = listOf(CommunalContentSize.HALF), + ), ) ) .containsExactly(*communalWidgetAllowlist.toTypedArray()) } } + // This behavior is temporary before the local database is set up. + @Test + fun communalWidgets_withPreviouslyBoundWidgets_removeEachBinding() = + testScope.runTest { + whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(1, 2, 3) + setAppWidgetIds(listOf(1, 2, 3)) + whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) + userUnlocked(true) + + val repository = initCommunalWidgetRepository() + + collectLastValue(repository.communalWidgets)() + + verify(appWidgetHost).deleteAppWidgetId(1) + verify(appWidgetHost).deleteAppWidgetId(2) + verify(appWidgetHost).deleteAppWidgetId(3) + } + + @Test + fun communalWidgets_allowlistNotEmpty_bindEachWidgetFromTheAllowlist() = + testScope.runTest { + whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(0, 1, 2) + userUnlocked(true) + + whenever(appWidgetManager.getAppWidgetInfo(0)).thenReturn(providerInfoA) + whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfoB) + whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfoC) + + val repository = initCommunalWidgetRepository() + + val inventory by collectLastValue(repository.communalWidgets) + + assertThat( + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + providerInfo = providerInfoA, + priority = 3, + ), + CommunalWidgetContentModel( + appWidgetId = 1, + providerInfo = providerInfoB, + priority = 2, + ), + CommunalWidgetContentModel( + appWidgetId = 2, + providerInfo = providerInfoC, + priority = 1, + ), + ) + ) + .containsExactly(*inventory!!.toTypedArray()) + } + private fun initCommunalWidgetRepository(): CommunalWidgetRepositoryImpl { return CommunalWidgetRepositoryImpl( context, @@ -272,7 +344,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } private fun verifyBroadcastReceiverRegistered() { - Mockito.verify(broadcastDispatcher) + verify(broadcastDispatcher) .registerReceiver( any(), any(), @@ -284,7 +356,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } private fun verifyBroadcastReceiverNeverRegistered() { - Mockito.verify(broadcastDispatcher, Mockito.never()) + verify(broadcastDispatcher, Mockito.never()) .registerReceiver( any(), any(), @@ -297,7 +369,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { private fun broadcastReceiverUpdate(): BroadcastReceiver { val broadcastReceiverCaptor = kotlinArgumentCaptor<BroadcastReceiver>() - Mockito.verify(broadcastDispatcher) + verify(broadcastDispatcher) .registerReceiver( broadcastReceiverCaptor.capture(), any(), @@ -310,7 +382,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { return broadcastReceiverCaptor.value } - private fun featureFlagEnabled(enabled: Boolean) { + private fun communalEnabled(enabled: Boolean) { + communalRepository.setIsCommunalEnabled(enabled) + } + + private fun widgetOnKeyguardEnabled(enabled: Boolean) { whenever(featureFlags.isEnabled(Flags.WIDGET_ON_KEYGUARD)).thenReturn(enabled) } @@ -322,8 +398,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { whenever(appWidgetManager.installedProviders).thenReturn(providers) } - companion object { - const val componentName1 = "component name 1" - const val componentName2 = "component name 2" + private fun setAppWidgetIds(ids: List<Int>) { + whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index cdc42e096830..8e21f294a361 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -22,7 +22,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo import com.android.systemui.coroutines.collectLastValue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt index 0552ced1d678..0e4b113f57ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CameraToggleTileTest.kt @@ -124,11 +124,44 @@ class CameraToggleTileTest : SysuiTestCase() { } @Test - fun testLongClickIntent() { + fun testLongClickIntent_safetyCenterEnabled() { whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true) - assertThat(tile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_CONTROLS) + val cameraTile = CameraToggleTile( + host, + uiEventLogger, + testableLooper.looper, + Handler(testableLooper.looper), + metricsLogger, + FalsingManagerFake(), + statusBarStateController, + activityStarter, + qsLogger, + privacyController, + keyguardStateController, + safetyCenterManager) + assertThat(cameraTile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_CONTROLS) + cameraTile.destroy() + testableLooper.processAllMessages() + } + @Test + fun testLongClickIntent_safetyCenterDisabled() { whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false) + val cameraTile = CameraToggleTile( + host, + uiEventLogger, + testableLooper.looper, + Handler(testableLooper.looper), + metricsLogger, + FalsingManagerFake(), + statusBarStateController, + activityStarter, + qsLogger, + privacyController, + keyguardStateController, + safetyCenterManager) assertThat(tile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_SETTINGS) + cameraTile.destroy() + testableLooper.processAllMessages() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt index 0fcfdb6f318f..b98a7570bb6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/MicrophoneToggleTileTest.kt @@ -123,11 +123,44 @@ class MicrophoneToggleTileTest : SysuiTestCase() { } @Test - fun testLongClickIntent() { + fun testLongClickIntent_safetyCenterEnabled() { whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true) - assertThat(tile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_CONTROLS) + val micTile = MicrophoneToggleTile( + host, + uiEventLogger, + testableLooper.looper, + Handler(testableLooper.looper), + metricsLogger, + FalsingManagerFake(), + statusBarStateController, + activityStarter, + qsLogger, + privacyController, + keyguardStateController, + safetyCenterManager) + assertThat(micTile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_CONTROLS) + micTile.destroy() + testableLooper.processAllMessages() + } + @Test + fun testLongClickIntent_safetyCenterDisabled() { whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false) - assertThat(tile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_SETTINGS) + val micTile = MicrophoneToggleTile( + host, + uiEventLogger, + testableLooper.looper, + Handler(testableLooper.looper), + metricsLogger, + FalsingManagerFake(), + statusBarStateController, + activityStarter, + qsLogger, + privacyController, + keyguardStateController, + safetyCenterManager) + assertThat(micTile.longClickIntent?.action).isEqualTo(Settings.ACTION_PRIVACY_SETTINGS) + micTile.destroy() + testableLooper.processAllMessages() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 4f3216da7370..da49230417b8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -21,7 +21,9 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.KeyEvent import android.view.MotionEvent +import android.view.View import android.view.ViewGroup +import androidx.core.view.contains import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardMessageAreaController import com.android.keyguard.KeyguardSecurityContainerController @@ -29,6 +31,8 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardBouncerComponent +import com.android.systemui.FakeFeatureFlagsImpl +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository @@ -43,11 +47,19 @@ import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.compose.ComposeFacade.isComposeAvailable import com.android.systemui.dock.DockManager import com.android.systemui.dump.DumpManager import com.android.systemui.dump.logcatLogBuffer -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.flags.Flags.ALTERNATE_BOUNCER_VIEW +import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED +import com.android.systemui.flags.Flags.MIGRATE_NSSL +import com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES +import com.android.systemui.flags.Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION +import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON +import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_FEATURES import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler import com.android.systemui.keyguard.DismissCallbackRegistry @@ -83,6 +95,7 @@ import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -97,6 +110,7 @@ import org.mockito.Mock import org.mockito.Mockito.anyFloat import org.mockito.Mockito.mock import org.mockito.Mockito.never +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations import java.util.Optional @@ -135,6 +149,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener @Mock private lateinit var notificationInsetsController: NotificationInsetsController + @Mock private lateinit var mCommunalViewModel: CommunalViewModel @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController @@ -159,7 +174,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { private lateinit var testScope: TestScope - private lateinit var featureFlags: FakeFeatureFlags + private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic + private lateinit var featureFlags: FakeFeatureFlagsImpl @Before fun setUp() { @@ -174,14 +190,16 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition) .thenReturn(emptyFlow<TransitionStep>()) - featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true) - featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false) - featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) - featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) - featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) - featureFlags.set(Flags.MIGRATE_NSSL, false) - featureFlags.set(Flags.ALTERNATE_BOUNCER_VIEW, false) + featureFlagsClassic = FakeFeatureFlagsClassic() + featureFlagsClassic.set(TRACKPAD_GESTURE_COMMON, true) + featureFlagsClassic.set(TRACKPAD_GESTURE_FEATURES, false) + featureFlagsClassic.set(SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) + featureFlagsClassic.set(REVAMPED_BOUNCER_MESSAGES, true) + featureFlagsClassic.set(LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) + featureFlagsClassic.set(MIGRATE_NSSL, false) + featureFlagsClassic.set(ALTERNATE_BOUNCER_VIEW, false) + + featureFlags = FakeFeatureFlagsImpl() testScope = TestScope() fakeClock = FakeSystemClock() @@ -216,14 +234,16 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { mock(KeyguardMessageAreaController.Factory::class.java), keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, + mCommunalViewModel, notificationExpansionRepository, + featureFlagsClassic, featureFlags, fakeClock, BouncerMessageInteractor( repository = BouncerMessageRepositoryImpl(), userRepository = FakeUserRepository(), countDownTimerUtil = mock(CountDownTimerUtil::class.java), - featureFlags = featureFlags, + featureFlags = featureFlagsClassic, updateMonitor = mock(KeyguardUpdateMonitor::class.java), biometricSettingsRepository = FakeBiometricSettingsRepository(), applicationScope = testScope.backgroundScope, @@ -443,7 +463,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)) .thenReturn(true) - featureFlags.set(Flags.MIGRATE_NSSL, true) + featureFlagsClassic.set(MIGRATE_NSSL, true) // THEN touch should NOT be intercepted by NotificationShade assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isFalse() @@ -460,7 +480,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { whenever(lockIconViewController.willHandleTouchWhileDozing(DOWN_EVENT)) .thenReturn(false) - featureFlags.set(Flags.MIGRATE_NSSL, true) + featureFlagsClassic.set(MIGRATE_NSSL, true) // THEN touch should NOT be intercepted by NotificationShade assertThat(interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)).isTrue() @@ -474,6 +494,48 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } @Test + fun setsUpCommunalHubLayout_whenFlagEnabled() { + if (!isComposeAvailable()) { + return + } + + featureFlags.setFlag(FLAG_COMMUNAL_HUB, true) + + val mockCommunalPlaceholder = mock(View::class.java) + val fakeViewIndex = 20 + whenever(view.findViewById<View>(R.id.communal_ui_stub)).thenReturn(mockCommunalPlaceholder) + whenever(view.indexOfChild(mockCommunalPlaceholder)).thenReturn(fakeViewIndex) + whenever(view.context).thenReturn(context) + + underTest.setupCommunalHubLayout() + + // Communal view added as a child of the container at the proper index, the stub is removed. + verify(view).removeView(mockCommunalPlaceholder) + verify(view).addView(any(), eq(fakeViewIndex)) + } + + @Test + fun doesNotSetupCommunalHubLayout_whenFlagDisabled() { + if (!isComposeAvailable()) { + return + } + + featureFlags.setFlag(FLAG_COMMUNAL_HUB, false) + + val mockCommunalPlaceholder = mock(View::class.java) + val fakeViewIndex = 20 + whenever(view.findViewById<View>(R.id.communal_ui_stub)).thenReturn(mockCommunalPlaceholder) + whenever(view.indexOfChild(mockCommunalPlaceholder)).thenReturn(fakeViewIndex) + whenever(view.context).thenReturn(context) + + underTest.setupCommunalHubLayout() + + // No adding or removing of views occurs. + verify(view, times(0)).removeView(mockCommunalPlaceholder) + verify(view, times(0)).addView(any(), eq(fakeViewIndex)) + } + + @Test fun forwardsDispatchKeyEvent() { val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_B) interactionEventHandler.dispatchKeyEvent(keyEvent) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 4d3eab45d001..c94741fa5320 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -28,6 +28,7 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardBouncerComponent +import com.android.systemui.FakeFeatureFlagsImpl import com.android.systemui.SysuiTestCase import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository @@ -42,6 +43,7 @@ import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.dock.DockManager import com.android.systemui.dump.DumpManager import com.android.systemui.dump.logcatLogBuffer @@ -142,6 +144,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> @Mock private lateinit var notificationInsetsController: NotificationInsetsController + @Mock private lateinit var mCommunalViewModel: CommunalViewModel @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor @Mock lateinit var alternateBouncerInteractor: AlternateBouncerInteractor @@ -218,8 +221,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { Mockito.mock(KeyguardMessageAreaController.Factory::class.java), keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, + mCommunalViewModel, NotificationExpansionRepository(), featureFlags, + FakeFeatureFlagsImpl(), FakeSystemClock(), BouncerMessageInteractor( repository = BouncerMessageRepositoryImpl(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt index 81382a44def6..3a260ae374c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt @@ -445,105 +445,6 @@ class ShadeInteractorTest : SysuiTestCase() { } @Test - fun expanding_shadeDraggedDown_expandingTrue() = - testScope.runTest() { - val actual by collectLastValue(underTest.isAnyExpanding) - - // GIVEN shade and QS collapsed - shadeRepository.setLegacyShadeExpansion(0f) - shadeRepository.setQsExpansion(0f) - runCurrent() - - // WHEN shade partially expanded - shadeRepository.setLegacyShadeExpansion(.5f) - runCurrent() - - // THEN anyExpanding is true - assertThat(actual).isTrue() - } - - @Test - fun expanding_qsDraggedDown_expandingTrue() = - testScope.runTest() { - val actual by collectLastValue(underTest.isAnyExpanding) - - // GIVEN shade and QS collapsed - shadeRepository.setLegacyShadeExpansion(0f) - shadeRepository.setQsExpansion(0f) - runCurrent() - - // WHEN shade partially expanded - shadeRepository.setQsExpansion(.5f) - runCurrent() - - // THEN anyExpanding is true - assertThat(actual).isTrue() - } - - @Test - fun expanding_shadeDraggedUpAndDown() = - testScope.runTest() { - val actual by collectLastValue(underTest.isAnyExpanding) - - // WHEN shade starts collapsed then partially expanded - shadeRepository.setLegacyShadeExpansion(0f) - shadeRepository.setLegacyShadeExpansion(.5f) - shadeRepository.setQsExpansion(0f) - runCurrent() - - // THEN anyExpanding is true - assertThat(actual).isTrue() - - // WHEN shade dragged up a bit - shadeRepository.setLegacyShadeExpansion(.2f) - runCurrent() - - // THEN anyExpanding is still true - assertThat(actual).isTrue() - - // WHEN shade dragged down a bit - shadeRepository.setLegacyShadeExpansion(.7f) - runCurrent() - - // THEN anyExpanding is still true - assertThat(actual).isTrue() - - // WHEN shade fully expanded - shadeRepository.setLegacyShadeExpansion(1f) - runCurrent() - - // THEN anyExpanding is now false - assertThat(actual).isFalse() - - // WHEN shade dragged up a bit - shadeRepository.setLegacyShadeExpansion(.7f) - runCurrent() - - // THEN anyExpanding is still false - assertThat(actual).isFalse() - } - - @Test - fun expanding_shadeDraggedDownThenUp_expandingFalse() = - testScope.runTest() { - val actual by collectLastValue(underTest.isAnyExpanding) - - // GIVEN shade starts collapsed - shadeRepository.setLegacyShadeExpansion(0f) - shadeRepository.setQsExpansion(0f) - runCurrent() - - // WHEN shade expands but doesn't complete - shadeRepository.setLegacyShadeExpansion(.5f) - runCurrent() - shadeRepository.setLegacyShadeExpansion(0f) - runCurrent() - - // THEN anyExpanding is false - assertThat(actual).isFalse() - } - - @Test fun lockscreenShadeExpansion_idle_onScene() = testScope.runTest() { // GIVEN an expansion flow based on transitions to and from a scene diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/PropertyAnimatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/PropertyAnimatorTest.java index c664c39f4432..2ef4374ce13a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/PropertyAnimatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/PropertyAnimatorTest.java @@ -18,10 +18,16 @@ package com.android.systemui.statusbar.notification; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.test.suitebuilder.annotation.SmallTest; @@ -33,8 +39,8 @@ import android.view.View; import android.view.animation.Interpolator; import com.android.app.animation.Interpolators; -import com.android.systemui.res.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.stack.AnimationFilter; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ViewState; @@ -85,7 +91,7 @@ public class PropertyAnimatorTest extends SysuiTestCase { return mEffectiveProperty; } }; - private AnimatorListenerAdapter mFinishListener = mock(AnimatorListenerAdapter.class); + private AnimatorListenerAdapter mFinishListener; private AnimationProperties mAnimationProperties = new AnimationProperties() { @Override public AnimationFilter getAnimationFilter() { @@ -104,6 +110,7 @@ public class PropertyAnimatorTest extends SysuiTestCase { @Before public void setUp() { mView = new View(getContext()); + mFinishListener = mock(AnimatorListenerAdapter.class); } @Test @@ -229,6 +236,32 @@ public class PropertyAnimatorTest extends SysuiTestCase { } @Test + public void testListenerCallbackOrderAndTagState() { + mAnimationFilter.reset(); + mAnimationFilter.animate(mProperty.getProperty()); + mAnimationProperties.setCustomInterpolator(mEffectiveProperty, mTestInterpolator); + mAnimationProperties.setDuration(500); + + // Validates that the onAnimationEnd function set by PropertyAnimator was run first. + doAnswer(invocation -> { + assertNull(mView.getTag(mProperty.getAnimatorTag())); + return null; + }) + .when(mFinishListener) + .onAnimationEnd(any(Animator.class), anyBoolean()); + + // Begin the animation and verify it set state correctly + PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties); + ValueAnimator animator = ViewState.getChildTag(mView, mProperty.getAnimatorTag()); + assertNotNull(animator); + assertNotNull(mView.getTag(mProperty.getAnimatorTag())); + + // Terminate the animation to run end runners, and validate they executed. + animator.end(); + verify(mFinishListener).onAnimationEnd(animator, false); + } + + @Test public void testIsAnimating() { mAnimationFilter.reset(); mAnimationFilter.animate(mProperty.getProperty()); @@ -236,4 +269,4 @@ public class PropertyAnimatorTest extends SysuiTestCase { PropertyAnimator.startAnimation(mView, mProperty, 200f, mAnimationProperties); assertTrue(PropertyAnimator.isAnimating(mView, mProperty)); } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 30132f7747b7..08adda32eb6d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -1,7 +1,8 @@ package com.android.systemui.communal.data.repository import com.android.systemui.communal.data.model.CommunalWidgetMetadata -import com.android.systemui.communal.shared.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -11,7 +12,14 @@ class FakeCommunalWidgetRepository : CommunalWidgetRepository { override val stopwatchAppWidgetInfo: Flow<CommunalAppWidgetInfo?> = _stopwatchAppWidgetInfo override var communalWidgetAllowlist: List<CommunalWidgetMetadata> = emptyList() + private val _communalWidgets = MutableStateFlow<List<CommunalWidgetContentModel>>(emptyList()) + override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = _communalWidgets + fun setStopwatchAppWidgetInfo(appWidgetInfo: CommunalAppWidgetInfo) { _stopwatchAppWidgetInfo.value = appWidgetInfo } + + fun setCommunalWidgets(inventory: List<CommunalWidgetContentModel>) { + _communalWidgets.value = inventory + } } diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 1a735f89a2bc..75ecdb78fe00 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -1,38 +1,40 @@ package: "com.android.server.accessibility" +# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors. + flag { - name: "proxy_use_apps_on_virtual_device_listener" + name: "add_window_token_without_lock" namespace: "accessibility" - description: "Fixes race condition described in b/286587811" - bug: "286587811" + description: "Calls WMS.addWindowToken without holding A11yManagerService#mLock" + bug: "297972548" } flag { - name: "enable_magnification_multiple_finger_multiple_tap_gesture" + name: "deprecate_package_list_observer" namespace: "accessibility" - description: "Whether to enable multi-finger-multi-tap gesture for magnification" - bug: "257274411" + description: "Stops using the deprecated PackageListObserver." + bug: "304561459" } flag { - name: "enable_magnification_joystick" + name: "disable_continuous_shortcut_on_force_stop" namespace: "accessibility" - description: "Whether to enable joystick controls for magnification" - bug: "297211257" + description: "When a package is force stopped, remove the button shortcuts of any continuously-running shortcuts." + bug: "198018180" } flag { - name: "send_a11y_events_based_on_state" + name: "enable_magnification_joystick" namespace: "accessibility" - description: "Sends accessibility events in TouchExplorer#onAccessibilityEvent based on internal state to keep it consistent. This reduces test flakiness." - bug: "295575684" + description: "Whether to enable joystick controls for magnification" + bug: "297211257" } flag { - name: "add_window_token_without_lock" + name: "enable_magnification_multiple_finger_multiple_tap_gesture" namespace: "accessibility" - description: "Calls WMS.addWindowToken without holding A11yManagerService#mLock" - bug: "297972548" + description: "Whether to enable multi-finger-multi-tap gesture for magnification" + bug: "257274411" } flag { @@ -43,17 +45,17 @@ flag { } flag { - name: "disable_continuous_shortcut_on_force_stop" + name: "proxy_use_apps_on_virtual_device_listener" namespace: "accessibility" - description: "When a package is force stopped, remove the button shortcuts of any continuously-running shortcuts." - bug: "198018180" + description: "Fixes race condition described in b/286587811" + bug: "286587811" } flag { - name: "deprecate_package_list_observer" + name: "reduce_touch_exploration_sensitivity" namespace: "accessibility" - description: "Stops using the deprecated PackageListObserver." - bug: "304561459" + description: "Reduces touch exploration sensitivity by only sending a hover event when the ifnger has moved the amount of pixels defined by the system's touch slop." + bug: "303677860" } flag { @@ -64,8 +66,8 @@ flag { } flag { - name: "reduce_touch_exploration_sensitivity" + name: "send_a11y_events_based_on_state" namespace: "accessibility" - description: "Reduces touch exploration sensitivity by only sending a hover event when the ifnger has moved the amount of pixels defined by the system's touch slop." - bug: "303677860" + description: "Sends accessibility events in TouchExplorer#onAccessibilityEvent based on internal state to keep it consistent. This reduces test flakiness." + bug: "295575684" } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index e65a185adfd2..87f9cf10f824 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4929,8 +4929,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mTouchExplorationEnabledUri = Settings.Secure.getUriFor( Settings.Secure.TOUCH_EXPLORATION_ENABLED); - private final Uri mDisplayMagnificationEnabledUri = Settings.Secure.getUriFor( - Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); + private final Uri mMagnificationmSingleFingerTripleTapEnabledUri = Settings.Secure + .getUriFor(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); private final Uri mAutoclickEnabledUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED); @@ -4987,7 +4987,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void register(ContentResolver contentResolver) { contentResolver.registerContentObserver(mTouchExplorationEnabledUri, false, this, UserHandle.USER_ALL); - contentResolver.registerContentObserver(mDisplayMagnificationEnabledUri, + contentResolver.registerContentObserver(mMagnificationmSingleFingerTripleTapEnabledUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver(mAutoclickEnabledUri, false, this, UserHandle.USER_ALL); @@ -5035,7 +5035,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (readTouchExplorationEnabledSettingLocked(userState)) { onUserStateChangedLocked(userState); } - } else if (mDisplayMagnificationEnabledUri.equals(uri)) { + } else if (mMagnificationmSingleFingerTripleTapEnabledUri.equals(uri)) { if (readMagnificationEnabledSettingsLocked(userState)) { onUserStateChangedLocked(userState); } diff --git a/services/core/Android.bp b/services/core/Android.bp index 898cdcc30e0f..b69699800efb 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -136,6 +136,7 @@ java_library_static { ":display-device-config", ":display-layout-config", ":device-state-config", + ":framework-core-nfc-infcadapter-sources", "java/com/android/server/EventLogTags.logtags", "java/com/android/server/am/EventLogTags.logtags", "java/com/android/server/wm/EventLogTags.logtags", @@ -195,6 +196,7 @@ java_library_static { "android.hardware.rebootescrow-V1-java", "android.hardware.power.stats-V2-java", "android.hidl.manager-V1.2-java", + "android.nfc.flags-aconfig-java", "cbor-java", "icu4j_calendar_astronomer", "android.security.aaid_aidl-java", diff --git a/services/core/java/com/android/server/am/AppWaitingForDebuggerDialog.java b/services/core/java/com/android/server/am/AppWaitingForDebuggerDialog.java index 9b5f18caf71a..710278d6b3c6 100644 --- a/services/core/java/com/android/server/am/AppWaitingForDebuggerDialog.java +++ b/services/core/java/com/android/server/am/AppWaitingForDebuggerDialog.java @@ -16,6 +16,8 @@ package com.android.server.am; +import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + import android.content.Context; import android.content.DialogInterface; import android.os.Handler; @@ -54,6 +56,7 @@ final class AppWaitingForDebuggerDialog extends BaseErrorDialog { setButton(DialogInterface.BUTTON_POSITIVE, "Force Close", mHandler.obtainMessage(1, app)); setTitle("Waiting For Debugger"); WindowManager.LayoutParams attrs = getWindow().getAttributes(); + attrs.privateFlags |= SYSTEM_FLAG_SHOW_FOR_ALL_USERS; attrs.setTitle("Waiting For Debugger: " + app.info.processName); getWindow().setAttributes(attrs); } diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index bb9ea285385b..cbaf05bb4e23 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -15,3 +15,10 @@ flag { description: "Feature flag for the ANR timer service" bug: "282428924" } + +flag { + name: "fgs_abuse_detection" + namespace: "backstage_power" + description: "Detect abusive FGS behavior for certain types (camera, mic, media, location)." + bug: "295545575" +} diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 35260ed6f148..7abd9c7f750b 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -39,10 +39,9 @@ import android.media.ISpatializerHeadTrackingCallback; import android.media.ISpatializerHeadTrackingModeCallback; import android.media.ISpatializerOutputCallback; import android.media.MediaMetrics; -import android.media.SpatializationLevel; -import android.media.SpatializationMode; import android.media.Spatializer; -import android.media.SpatializerHeadTrackingMode; +import android.media.audio.common.HeadTracking; +import android.media.audio.common.Spatialization; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.text.TextUtils; @@ -84,22 +83,22 @@ public class SpatializerHelper { /*package*/ static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(14) { { - append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_WIRED_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); - append(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_WIRED_HEADSET, Spatialization.Mode.BINAURAL); + append(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, Spatialization.Mode.BINAURAL); // assumption for A2DP: mostly headsets - append(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, SpatializationMode.SPATIALIZER_BINAURAL); - append(AudioDeviceInfo.TYPE_DOCK, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_USB_ACCESSORY, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_USB_DEVICE, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_USB_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); - append(AudioDeviceInfo.TYPE_LINE_ANALOG, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_LINE_DIGITAL, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_AUX_LINE, SpatializationMode.SPATIALIZER_TRANSAURAL); - append(AudioDeviceInfo.TYPE_BLE_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); - append(AudioDeviceInfo.TYPE_BLE_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, Spatialization.Mode.BINAURAL); + append(AudioDeviceInfo.TYPE_DOCK, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_USB_ACCESSORY, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_USB_DEVICE, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_USB_HEADSET, Spatialization.Mode.BINAURAL); + append(AudioDeviceInfo.TYPE_LINE_ANALOG, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_LINE_DIGITAL, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_AUX_LINE, Spatialization.Mode.TRANSAURAL); + append(AudioDeviceInfo.TYPE_BLE_HEADSET, Spatialization.Mode.BINAURAL); + append(AudioDeviceInfo.TYPE_BLE_SPEAKER, Spatialization.Mode.TRANSAURAL); // assumption that BLE broadcast would be mostly consumed on headsets - append(AudioDeviceInfo.TYPE_BLE_BROADCAST, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_BLE_BROADCAST, Spatialization.Mode.BINAURAL); } }; @@ -226,12 +225,12 @@ public class SpatializerHelper { ArrayList<Integer> list = new ArrayList<>(0); for (byte value : values) { switch (value) { - case SpatializerHeadTrackingMode.OTHER: - case SpatializerHeadTrackingMode.DISABLED: + case HeadTracking.Mode.OTHER: + case HeadTracking.Mode.DISABLED: // not expected here, skip break; - case SpatializerHeadTrackingMode.RELATIVE_WORLD: - case SpatializerHeadTrackingMode.RELATIVE_SCREEN: + case HeadTracking.Mode.RELATIVE_WORLD: + case HeadTracking.Mode.RELATIVE_SCREEN: list.add(headTrackingModeTypeToSpatializerInt(value)); break; default: @@ -254,10 +253,10 @@ public class SpatializerHelper { byte[] spatModes = spat.getSupportedModes(); for (byte mode : spatModes) { switch (mode) { - case SpatializationMode.SPATIALIZER_BINAURAL: + case Spatialization.Mode.BINAURAL: mBinauralSupported = true; break; - case SpatializationMode.SPATIALIZER_TRANSAURAL: + case Spatialization.Mode.TRANSAURAL: mTransauralSupported = true; break; default: @@ -274,8 +273,8 @@ public class SpatializerHelper { // initialize list of compatible devices for (int i = 0; i < SPAT_MODE_FOR_DEVICE_TYPE.size(); i++) { int mode = SPAT_MODE_FOR_DEVICE_TYPE.valueAt(i); - if ((mode == (int) SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported) - || (mode == (int) SpatializationMode.SPATIALIZER_TRANSAURAL + if ((mode == (int) Spatialization.Mode.BINAURAL && mBinauralSupported) + || (mode == (int) Spatialization.Mode.TRANSAURAL && mTransauralSupported)) { mSACapableDeviceTypes.add(SPAT_MODE_FOR_DEVICE_TYPE.keyAt(i)); } @@ -577,9 +576,9 @@ public class SpatializerHelper { int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(device.getDeviceType(), Integer.MIN_VALUE); - device.setSAEnabled(spatMode == SpatializationMode.SPATIALIZER_BINAURAL + device.setSAEnabled(spatMode == Spatialization.Mode.BINAURAL ? mBinauralEnabledDefault - : spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL + : spatMode == Spatialization.Mode.TRANSAURAL ? mTransauralEnabledDefault : false); device.setHeadTrackerEnabled(mHeadTrackingEnabledDefault); @@ -629,9 +628,9 @@ public class SpatializerHelper { if (isBluetoothDevice(internalDeviceType)) return deviceType; final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE); - if (spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL) { + if (spatMode == Spatialization.Mode.TRANSAURAL) { return AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; - } else if (spatMode == SpatializationMode.SPATIALIZER_BINAURAL) { + } else if (spatMode == Spatialization.Mode.BINAURAL) { return AudioDeviceInfo.TYPE_WIRED_HEADPHONES; } return AudioDeviceInfo.TYPE_UNKNOWN; @@ -690,8 +689,7 @@ public class SpatializerHelper { // since their physical characteristics are unknown if (deviceState.getAudioDeviceCategory() == AUDIO_DEVICE_CATEGORY_UNKNOWN || deviceState.getAudioDeviceCategory() == AUDIO_DEVICE_CATEGORY_HEADPHONES) { - available = (spatMode == SpatializationMode.SPATIALIZER_BINAURAL) - && mBinauralSupported; + available = (spatMode == Spatialization.Mode.BINAURAL) && mBinauralSupported; } else { available = false; } @@ -804,8 +802,8 @@ public class SpatializerHelper { // not be included. final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(), /*default when type not found*/ -1); - if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported) - || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL + if ((modeForDevice == Spatialization.Mode.BINAURAL && mBinauralSupported) + || (modeForDevice == Spatialization.Mode.TRANSAURAL && mTransauralSupported)) { return true; } @@ -1479,7 +1477,7 @@ public class SpatializerHelper { } synchronized void onInitSensors() { - final boolean init = mFeatureEnabled && (mSpatLevel != SpatializationLevel.NONE); + final boolean init = mFeatureEnabled && (mSpatLevel != Spatialization.Level.NONE); final String action = init ? "initializing" : "releasing"; if (mSpat == null) { logloge("not " + action + " sensors, null spatializer"); @@ -1545,13 +1543,13 @@ public class SpatializerHelper { // SDK <-> AIDL converters private static int headTrackingModeTypeToSpatializerInt(byte mode) { switch (mode) { - case SpatializerHeadTrackingMode.OTHER: + case HeadTracking.Mode.OTHER: return Spatializer.HEAD_TRACKING_MODE_OTHER; - case SpatializerHeadTrackingMode.DISABLED: + case HeadTracking.Mode.DISABLED: return Spatializer.HEAD_TRACKING_MODE_DISABLED; - case SpatializerHeadTrackingMode.RELATIVE_WORLD: + case HeadTracking.Mode.RELATIVE_WORLD: return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD; - case SpatializerHeadTrackingMode.RELATIVE_SCREEN: + case HeadTracking.Mode.RELATIVE_SCREEN: return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE; default: throw (new IllegalArgumentException("Unexpected head tracking mode:" + mode)); @@ -1561,13 +1559,13 @@ public class SpatializerHelper { private static byte spatializerIntToHeadTrackingModeType(int sdkMode) { switch (sdkMode) { case Spatializer.HEAD_TRACKING_MODE_OTHER: - return SpatializerHeadTrackingMode.OTHER; + return HeadTracking.Mode.OTHER; case Spatializer.HEAD_TRACKING_MODE_DISABLED: - return SpatializerHeadTrackingMode.DISABLED; + return HeadTracking.Mode.DISABLED; case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD: - return SpatializerHeadTrackingMode.RELATIVE_WORLD; + return HeadTracking.Mode.RELATIVE_WORLD; case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE: - return SpatializerHeadTrackingMode.RELATIVE_SCREEN; + return HeadTracking.Mode.RELATIVE_SCREEN; default: throw (new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode)); } @@ -1575,11 +1573,11 @@ public class SpatializerHelper { private static int spatializationLevelToSpatializerInt(byte level) { switch (level) { - case SpatializationLevel.NONE: + case Spatialization.Level.NONE: return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; - case SpatializationLevel.SPATIALIZER_MULTICHANNEL: + case Spatialization.Level.MULTICHANNEL: return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL; - case SpatializationLevel.SPATIALIZER_MCHAN_BED_PLUS_OBJECTS: + case Spatialization.Level.BED_PLUS_OBJECTS: return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS; default: throw (new IllegalArgumentException("Unexpected spatializer level:" + level)); diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java index 3d347bea6bae..f9bc8dcc7d0b 100644 --- a/services/core/java/com/android/server/camera/CameraServiceProxy.java +++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java @@ -53,6 +53,8 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.media.AudioManager; import android.nfc.INfcAdapter; +import android.nfc.NfcAdapter; +import android.nfc.NfcManager; import android.os.Binder; import android.os.Handler; import android.os.HandlerExecutor; @@ -163,10 +165,6 @@ public class CameraServiceProxy extends SystemService * SCALER_ROTATE_AND_CROP_NONE -> Always return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE */ - // Flags arguments to NFC adapter to enable/disable NFC - public static final int DISABLE_POLLING_FLAGS = 0x1000; - public static final int ENABLE_POLLING_FLAGS = 0x0000; - // Handler message codes private static final int MSG_SWITCH_USER = 1; private static final int MSG_NOTIFY_DEVICE_STATE = 2; @@ -216,7 +214,6 @@ public class CameraServiceProxy extends SystemService private final List<CameraUsageEvent> mCameraUsageHistory = new ArrayList<>(); private static final String NFC_NOTIFICATION_PROP = "ro.camera.notify_nfc"; - private static final String NFC_SERVICE_BINDER_NAME = "nfc"; private static final IBinder nfcInterfaceToken = new Binder(); private final boolean mNotifyNfc; @@ -1274,8 +1271,13 @@ public class CameraServiceProxy extends SystemService } } - private void notifyNfcService(boolean enablePolling) { - + // TODO(b/303286040): Remove the raw INfcAdapter usage once |ENABLE_NFC_MAINLINE_FLAG| is + // rolled out. + private static final String NFC_SERVICE_BINDER_NAME = "nfc"; + // Flags arguments to NFC adapter to enable/disable NFC + public static final int DISABLE_POLLING_FLAGS = 0x1000; + public static final int ENABLE_POLLING_FLAGS = 0x0000; + private void setNfcReaderModeUsingINfcAdapter(boolean enablePolling) { IBinder nfcServiceBinder = getBinderService(NFC_SERVICE_BINDER_NAME); if (nfcServiceBinder == null) { Slog.w(TAG, "Could not connect to NFC service to notify it of camera state"); @@ -1291,6 +1293,25 @@ public class CameraServiceProxy extends SystemService } } + private void notifyNfcService(boolean enablePolling) { + if (android.nfc.Flags.enableNfcMainline()) { + NfcManager nfcManager = mContext.getSystemService(NfcManager.class); + if (nfcManager == null) { + Slog.w(TAG, "Could not connect to NFC service to notify it of camera state"); + return; + } + NfcAdapter nfcAdapter = nfcManager.getDefaultAdapter(); + if (nfcAdapter == null) { + Slog.w(TAG, "Could not connect to NFC service to notify it of camera state"); + return; + } + if (DEBUG) Slog.v(TAG, "Setting NFC reader mode. enablePolling: " + enablePolling); + nfcAdapter.setReaderMode(enablePolling); + } else { + setNfcReaderModeUsingINfcAdapter(enablePolling); + } + } + private static int[] toArray(Collection<Integer> c) { int len = c.size(); int[] ret = new int[len]; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index cb2302a60248..e5f01df75fcd 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -586,7 +586,8 @@ public final class DisplayManagerService extends SystemService { mSystemReady = false; mConfigParameterProvider = new DeviceConfigParameterProvider(DeviceConfigInterface.REAL); mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null); - mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName); + // TODO: b/306170135 - return TextUtils package name check instead + mExtraDisplayEventLogging = true; } public void setupSchedulerPolicies() { @@ -2306,8 +2307,10 @@ public final class DisplayManagerService extends SystemService { @GuardedBy("mSyncRoot") private boolean hdrConversionIntroducesLatencyLocked() { + HdrConversionMode mode = getHdrConversionModeSettingInternal(); final int preferredHdrOutputType = - getHdrConversionModeSettingInternal().getPreferredHdrOutputType(); + mode.getConversionMode() == HdrConversionMode.HDR_CONVERSION_SYSTEM + ? mSystemPreferredHdrOutputType : mode.getPreferredHdrOutputType(); if (preferredHdrOutputType != Display.HdrCapabilities.HDR_TYPE_INVALID) { int[] hdrTypesWithLatency = mInjector.getHdrOutputTypesWithLatency(); return ArrayUtils.contains(hdrTypesWithLatency, preferredHdrOutputType); @@ -2588,16 +2591,14 @@ public final class DisplayManagerService extends SystemService { // TODO(b/202378408) set minimal post-processing only if it's supported once we have a // separate API for disabling on-device processing. boolean mppRequest = isMinimalPostProcessingAllowed() && preferMinimalPostProcessing; - boolean disableHdrConversionForLatency = false; + // If HDR conversion introduces latency, disable that in case minimal + // post-processing is requested + boolean disableHdrConversionForLatency = + mppRequest ? hdrConversionIntroducesLatencyLocked() : false; if (display.getRequestedMinimalPostProcessingLocked() != mppRequest) { display.setRequestedMinimalPostProcessingLocked(mppRequest); shouldScheduleTraversal = true; - // If HDR conversion introduces latency, disable that in case minimal - // post-processing is requested - if (mppRequest) { - disableHdrConversionForLatency = hdrConversionIntroducesLatencyLocked(); - } } if (shouldScheduleTraversal) { @@ -2933,8 +2934,15 @@ public final class DisplayManagerService extends SystemService { // Send a display event if the display is enabled private void sendDisplayEventIfEnabledLocked(@NonNull LogicalDisplay display, @DisplayEvent int event) { + final boolean displayIsEnabled = display.isEnabledLocked(); + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.instant(Trace.TRACE_TAG_POWER, + "sendDisplayEventLocked#event=" + event + ",displayEnabled=" + + displayIsEnabled); + } + // Only send updates outside of DisplayManagerService for enabled displays - if (display.isEnabledLocked()) { + if (displayIsEnabled) { sendDisplayEventLocked(display, event); } else if (mExtraDisplayEventLogging) { Slog.i(TAG, "Not Sending Display Event; display is not enabled: " + display); @@ -2991,7 +2999,11 @@ public final class DisplayManagerService extends SystemService { + displayId + ", event=" + event + (uids != null ? ", uids=" + uids : "")); } - + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.instant(Trace.TRACE_TAG_POWER, + "deliverDisplayEvent#event=" + event + ",displayId=" + + displayId + (uids != null ? ", uids=" + uids : "")); + } // Grab the lock and copy the callbacks. final int count; synchronized (mSyncRoot) { @@ -3031,7 +3043,8 @@ public final class DisplayManagerService extends SystemService { } private boolean extraLogging(String packageName) { - return mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals(packageName); + // TODO: b/306170135 - return mExtraDisplayLoggingPackageName & package name check instead + return true; } // Runs on Handler thread. @@ -3498,10 +3511,13 @@ public final class DisplayManagerService extends SystemService { @Override public void binderDied() { - if (DEBUG || mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals( - mPackageName)) { + if (DEBUG || extraLogging(mPackageName)) { Slog.d(TAG, "Display listener for pid " + mPid + " died."); } + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.instant(Trace.TRACE_TAG_POWER, + "displayManagerBinderDied#mPid=" + mPid); + } onCallbackDied(this); } @@ -3510,11 +3526,15 @@ public final class DisplayManagerService extends SystemService { */ public boolean notifyDisplayEventAsync(int displayId, @DisplayEvent int event) { if (!shouldSendEvent(event)) { - if (mExtraDisplayEventLogging && mExtraDisplayLoggingPackageName.equals( - mPackageName)) { + if (extraLogging(mPackageName)) { Slog.i(TAG, "Not sending displayEvent: " + event + " due to mask:" + mEventsMask); } + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.instant(Trace.TRACE_TAG_POWER, + "notifyDisplayEventAsync#notSendingEvent=" + event + ",mEventsMask=" + + mEventsMask); + } return true; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 7ca56990f2d0..452be2f435f5 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -290,6 +290,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.compat.IPlatformCompat; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; +import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags; import com.android.internal.logging.InstanceId; import com.android.internal.logging.InstanceIdSequence; import com.android.internal.logging.MetricsLogger; @@ -1179,7 +1180,7 @@ public class NotificationManagerService extends SystemService { @Override public void onSetDisabled(int status) { synchronized (mNotificationLock) { - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.updateDisableNotificationEffectsLocked(status); } else { mDisableNotificationEffects = @@ -1325,7 +1326,7 @@ public class NotificationManagerService extends SystemService { public void clearEffects() { synchronized (mNotificationLock) { if (DBG) Slog.d(TAG, "clearEffects"); - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.clearAttentionEffects(); } else { clearSoundLocked(); @@ -1554,7 +1555,8 @@ public class NotificationManagerService extends SystemService { int changedFlags = data.getFlags() ^ flags; if ((changedFlags & FLAG_SUPPRESS_NOTIFICATION) != 0) { // Suppress notification flag changed, clear any effects - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled( + NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.clearEffectsLocked(key); } else { clearEffectsLocked(key); @@ -1903,7 +1905,7 @@ public class NotificationManagerService extends SystemService { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (!Flags.refactorAttentionHelper()) { + if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { if (action.equals(Intent.ACTION_SCREEN_ON)) { // Keep track of screen on/off state, but do not turn off the notification light // until user passes through the lock screen or views the notification. @@ -2017,7 +2019,7 @@ public class NotificationManagerService extends SystemService { ContentResolver resolver = getContext().getContentResolver(); resolver.registerContentObserver(NOTIFICATION_BADGING_URI, false, this, UserHandle.USER_ALL); - if (!Flags.refactorAttentionHelper()) { + if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { resolver.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI, false, this, UserHandle.USER_ALL); } @@ -2043,7 +2045,7 @@ public class NotificationManagerService extends SystemService { public void update(Uri uri) { ContentResolver resolver = getContext().getContentResolver(); - if (!Flags.refactorAttentionHelper()) { + if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { if (uri == null || NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) { boolean pulseEnabled = Settings.System.getIntForUser(resolver, Settings.System.NOTIFICATION_LIGHT_PULSE, 0, UserHandle.USER_CURRENT) @@ -2536,7 +2538,7 @@ public class NotificationManagerService extends SystemService { mToastRateLimiter = toastRateLimiter; - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper = new NotificationAttentionHelper(getContext(), lightsManager, mAccessibilityManager, mPackageManagerClient, userManager, usageStats, mNotificationManagerPrivate, mZenModeHelper, flagResolver); @@ -2546,7 +2548,7 @@ public class NotificationManagerService extends SystemService { // If this is called within a test, make sure to unregister the intent receivers by // calling onDestroy() IntentFilter filter = new IntentFilter(); - if (!Flags.refactorAttentionHelper()) { + if (!mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); @@ -2874,7 +2876,7 @@ public class NotificationManagerService extends SystemService { } registerNotificationPreferencesPullers(); new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker); - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.onSystemReady(); } } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) { @@ -6499,7 +6501,7 @@ public class NotificationManagerService extends SystemService { pw.println(" mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate); pw.println(" hideSilentStatusBar=" + mPreferencesHelper.shouldHideSilentStatusIcons()); - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.dump(pw, " ", filter); } } @@ -7765,7 +7767,7 @@ public class NotificationManagerService extends SystemService { boolean wasPosted = removeFromNotificationListsLocked(r); cancelNotificationLocked(r, false, REASON_SNOOZED, wasPosted, null, SystemClock.elapsedRealtime()); - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.updateLightsLocked(); } else { updateLightsLocked(); @@ -7898,7 +7900,7 @@ public class NotificationManagerService extends SystemService { cancelGroupChildrenLocked(r, mCallingUid, mCallingPid, listenerName, mSendDelete, childrenFlagChecker, mReason, mCancellationElapsedTimeMs); - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.updateLightsLocked(); } else { updateLightsLocked(); @@ -8195,7 +8197,7 @@ public class NotificationManagerService extends SystemService { int buzzBeepBlinkLoggingCode = 0; if (!r.isHidden()) { - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { buzzBeepBlinkLoggingCode = mAttentionHelper.buzzBeepBlinkLocked(r, new NotificationAttentionHelper.Signals( mUserProfiles.isCurrentProfile(r.getUserId()), @@ -9182,7 +9184,7 @@ public class NotificationManagerService extends SystemService { || interruptiveChanged; if (interceptBefore && !record.isIntercepted() && record.isNewEnoughForAlerting(System.currentTimeMillis())) { - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.buzzBeepBlinkLocked(record, new NotificationAttentionHelper.Signals( mUserProfiles.isCurrentProfile(record.getUserId()), mListenerHints)); @@ -9562,7 +9564,7 @@ public class NotificationManagerService extends SystemService { }); } - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.clearEffectsLocked(canceledKey); } else { // sound @@ -9926,7 +9928,7 @@ public class NotificationManagerService extends SystemService { cancellationElapsedTimeMs); } } - if (Flags.refactorAttentionHelper()) { + if (mFlagResolver.isEnabled(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR)) { mAttentionHelper.updateLightsLocked(); } else { updateLightsLocked(); diff --git a/services/core/java/com/android/server/wm/SnapshotController.java b/services/core/java/com/android/server/wm/SnapshotController.java index 2e7ff7a6b9b8..f68e39207b21 100644 --- a/services/core/java/com/android/server/wm/SnapshotController.java +++ b/services/core/java/com/android/server/wm/SnapshotController.java @@ -85,7 +85,7 @@ class SnapshotController { if (info.mWindowingMode == WINDOWING_MODE_PINNED) continue; if (info.mContainer.isActivityTypeHome()) continue; final Task task = info.mContainer.asTask(); - if (task != null && !task.isVisibleRequested()) { + if (task != null && !task.mCreatedByOrganizer && !task.isVisibleRequested()) { mTaskSnapshotController.recordSnapshot(task, info); } // Won't need to capture activity snapshot in close transition. diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index cf8548cfe689..9bd938f2e0a7 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -80,9 +80,7 @@ import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; - import androidx.test.runner.AndroidJUnit4; - import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags; import com.android.internal.config.sysui.TestableFlagResolver; import com.android.internal.logging.InstanceIdSequence; @@ -95,7 +93,6 @@ import com.android.server.pm.PackageManagerService; import java.util.List; import java.util.Objects; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -193,7 +190,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { assertTrue(mAccessibilityManager.isEnabled()); // TODO (b/291907312): remove feature flag - mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); + mTestFlagResolver.setFlagOverride(NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR, true); // Disable feature flags by default. Tests should enable as needed. mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS, Flags.FLAG_EXPIRE_BITMAPS); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 16c39b440ba5..16f97f09dd0c 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -85,6 +85,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; +import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.ENABLE_ATTENTION_HELPER_REFACTOR; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.FSI_FORCE_DEMOTE; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.SHOW_STICKY_HUN_FOR_DENIED_FSI; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; @@ -327,9 +328,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private TestableNotificationManagerService mService; private INotificationManager mBinderService; private NotificationManagerInternal mInternalService; @@ -616,8 +614,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { }); // TODO (b/291907312): remove feature flag - mSetFlagsRule.disableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER, - Flags.FLAG_POLITE_NOTIFICATIONS); + mTestFlagResolver.setFlagOverride(ENABLE_ATTENTION_HELPER_REFACTOR, false); initNMS(); } @@ -658,7 +655,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mHistoryManager).onBootPhaseAppsCanStart(); // TODO b/291907312: remove feature flag - if (Flags.refactorAttentionHelper()) { + if (mTestFlagResolver.isEnabled(ENABLE_ATTENTION_HELPER_REFACTOR)) { mService.mAttentionHelper.setAudioManager(mAudioManager); } else { mService.setAudioManager(mAudioManager); @@ -1695,7 +1692,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testEnqueueNotificationWithTag_WritesExpectedLogs_NAHRefactor() throws Exception { // TODO b/291907312: remove feature flag - mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); + mTestFlagResolver.setFlagOverride(ENABLE_ATTENTION_HELPER_REFACTOR, true); // Cleanup NMS before re-initializing if (mService != null) { try { @@ -9158,7 +9155,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped_NAHRefactor() throws Exception { // TODO b/291907312: remove feature flag - mSetFlagsRule.enableFlags(Flags.FLAG_REFACTOR_ATTENTION_HELPER); + mTestFlagResolver.setFlagOverride(ENABLE_ATTENTION_HELPER_REFACTOR, true); // Cleanup NMS before re-initializing if (mService != null) { try { diff --git a/tests/FsVerityTest/AndroidTest.xml b/tests/FsVerityTest/AndroidTest.xml index 49cbde0d4611..d2537f6410e8 100644 --- a/tests/FsVerityTest/AndroidTest.xml +++ b/tests/FsVerityTest/AndroidTest.xml @@ -24,7 +24,7 @@ <!-- This test requires root to write against block device. --> <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> - <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="test-file-name" value="FsVerityTestApp.apk"/> <option name="cleanup-apks" value="true"/> </target_preparer> |